use std::{
any::{Any, TypeId},
cmp::Ordering,
collections::{
hash_map::{DefaultHasher, Entry, HashMap},
BTreeSet,
},
hash::{Hash, Hasher},
mem::discriminant,
num::NonZeroUsize,
sync::{Arc, Mutex},
};
pub use azul_core::selection::{ContentIndex, GraphemeClusterId};
use azul_core::{
dom::NodeId,
geom::{LogicalPosition, LogicalRect, LogicalSize},
resources::ImageRef,
selection::{CursorAffinity, SelectionRange, TextCursor},
ui_solver::GlyphInstance,
};
use azul_css::{
corety::LayoutDebugMessage, props::basic::ColorU, props::style::StyleBackgroundContent,
};
#[cfg(feature = "text_layout_hyphenation")]
use hyphenation::{Hyphenator, Language as HyphenationLanguage, Load, Standard};
use rust_fontconfig::{FcFontCache, FcPattern, FcWeight, FontId, PatternMatch, UnicodeRange};
use unicode_bidi::{BidiInfo, Level, TextSource};
use unicode_segmentation::UnicodeSegmentation;
#[cfg(not(feature = "text_layout_hyphenation"))]
pub struct Standard;
#[cfg(not(feature = "text_layout_hyphenation"))]
impl Standard {
pub fn hyphenate<'a>(&'a self, _word: &'a str) -> StubHyphenationBreaks {
StubHyphenationBreaks { breaks: Vec::new() }
}
}
#[cfg(not(feature = "text_layout_hyphenation"))]
pub struct StubHyphenationBreaks {
pub breaks: alloc::vec::Vec<usize>,
}
use crate::text3::script::{script_to_language, Language, Script};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AvailableSpace {
Definite(f32),
MinContent,
MaxContent,
}
impl Default for AvailableSpace {
fn default() -> Self {
AvailableSpace::Definite(0.0)
}
}
impl AvailableSpace {
pub fn is_definite(&self) -> bool {
matches!(self, AvailableSpace::Definite(_))
}
pub fn is_indefinite(&self) -> bool {
!self.is_definite()
}
pub fn unwrap_or(self, fallback: f32) -> f32 {
match self {
AvailableSpace::Definite(v) => v,
_ => fallback,
}
}
pub fn to_f32_for_layout(self) -> f32 {
match self {
AvailableSpace::Definite(v) => v,
AvailableSpace::MinContent => f32::MAX / 2.0,
AvailableSpace::MaxContent => f32::MAX / 2.0,
}
}
pub fn from_f32(value: f32) -> Self {
if value.is_infinite() || value >= f32::MAX / 2.0 {
AvailableSpace::MaxContent
} else if value <= 0.0 {
AvailableSpace::MinContent
} else {
AvailableSpace::Definite(value)
}
}
}
impl Hash for AvailableSpace {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
if let AvailableSpace::Definite(v) = self {
(v.round() as usize).hash(state);
}
}
}
pub use crate::font_traits::{ParsedFontTrait, ShallowClone};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FontChainKey {
pub font_families: Vec<String>,
pub weight: FcWeight,
pub italic: bool,
pub oblique: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FontChainKeyOrRef {
Chain(FontChainKey),
Ref(usize),
}
impl FontChainKeyOrRef {
pub fn from_font_stack(font_stack: &FontStack) -> Self {
match font_stack {
FontStack::Stack(selectors) => FontChainKeyOrRef::Chain(FontChainKey::from_selectors(selectors)),
FontStack::Ref(font_ref) => FontChainKeyOrRef::Ref(font_ref.parsed as usize),
}
}
pub fn is_ref(&self) -> bool {
matches!(self, FontChainKeyOrRef::Ref(_))
}
pub fn as_ref_ptr(&self) -> Option<usize> {
match self {
FontChainKeyOrRef::Ref(ptr) => Some(*ptr),
_ => None,
}
}
pub fn as_chain(&self) -> Option<&FontChainKey> {
match self {
FontChainKeyOrRef::Chain(key) => Some(key),
_ => None,
}
}
}
impl FontChainKey {
pub fn from_selectors(font_stack: &[FontSelector]) -> Self {
let font_families: Vec<String> = font_stack
.iter()
.map(|s| s.family.clone())
.filter(|f| !f.is_empty())
.collect();
let font_families = if font_families.is_empty() {
vec!["serif".to_string()]
} else {
font_families
};
let weight = font_stack
.first()
.map(|s| s.weight)
.unwrap_or(FcWeight::Normal);
let is_italic = font_stack
.first()
.map(|s| s.style == FontStyle::Italic)
.unwrap_or(false);
let is_oblique = font_stack
.first()
.map(|s| s.style == FontStyle::Oblique)
.unwrap_or(false);
FontChainKey {
font_families,
weight,
italic: is_italic,
oblique: is_oblique,
}
}
}
#[derive(Debug, Clone)]
pub struct LoadedFonts<T> {
pub fonts: HashMap<FontId, T>,
hash_to_id: HashMap<u64, FontId>,
}
impl<T: ParsedFontTrait> LoadedFonts<T> {
pub fn new() -> Self {
Self {
fonts: HashMap::new(),
hash_to_id: HashMap::new(),
}
}
pub fn insert(&mut self, font_id: FontId, font: T) {
let hash = font.get_hash();
self.hash_to_id.insert(hash, font_id.clone());
self.fonts.insert(font_id, font);
}
pub fn get(&self, font_id: &FontId) -> Option<&T> {
self.fonts.get(font_id)
}
pub fn get_by_hash(&self, hash: u64) -> Option<&T> {
self.hash_to_id.get(&hash).and_then(|id| self.fonts.get(id))
}
pub fn get_font_id_by_hash(&self, hash: u64) -> Option<&FontId> {
self.hash_to_id.get(&hash)
}
pub fn contains_key(&self, font_id: &FontId) -> bool {
self.fonts.contains_key(font_id)
}
pub fn contains_hash(&self, hash: u64) -> bool {
self.hash_to_id.contains_key(&hash)
}
pub fn iter(&self) -> impl Iterator<Item = (&FontId, &T)> {
self.fonts.iter()
}
pub fn len(&self) -> usize {
self.fonts.len()
}
pub fn is_empty(&self) -> bool {
self.fonts.is_empty()
}
}
impl<T: ParsedFontTrait> Default for LoadedFonts<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: ParsedFontTrait> FromIterator<(FontId, T)> for LoadedFonts<T> {
fn from_iter<I: IntoIterator<Item = (FontId, T)>>(iter: I) -> Self {
let mut loaded = LoadedFonts::new();
for (id, font) in iter {
loaded.insert(id, font);
}
loaded
}
}
#[derive(Debug, Clone)]
pub enum FontOrRef<T> {
Font(T),
Ref(azul_css::props::basic::FontRef),
}
impl<T: ParsedFontTrait> ShallowClone for FontOrRef<T> {
fn shallow_clone(&self) -> Self {
match self {
FontOrRef::Font(f) => FontOrRef::Font(f.shallow_clone()),
FontOrRef::Ref(r) => FontOrRef::Ref(r.clone()),
}
}
}
impl<T: ParsedFontTrait> ParsedFontTrait for FontOrRef<T> {
fn shape_text(
&self,
text: &str,
script: Script,
language: Language,
direction: BidiDirection,
style: &StyleProperties,
) -> Result<Vec<Glyph>, LayoutError> {
match self {
FontOrRef::Font(f) => f.shape_text(text, script, language, direction, style),
FontOrRef::Ref(r) => r.shape_text(text, script, language, direction, style),
}
}
fn get_hash(&self) -> u64 {
match self {
FontOrRef::Font(f) => f.get_hash(),
FontOrRef::Ref(r) => r.get_hash(),
}
}
fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
match self {
FontOrRef::Font(f) => f.get_glyph_size(glyph_id, font_size),
FontOrRef::Ref(r) => r.get_glyph_size(glyph_id, font_size),
}
}
fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
match self {
FontOrRef::Font(f) => f.get_hyphen_glyph_and_advance(font_size),
FontOrRef::Ref(r) => r.get_hyphen_glyph_and_advance(font_size),
}
}
fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
match self {
FontOrRef::Font(f) => f.get_kashida_glyph_and_advance(font_size),
FontOrRef::Ref(r) => r.get_kashida_glyph_and_advance(font_size),
}
}
fn has_glyph(&self, codepoint: u32) -> bool {
match self {
FontOrRef::Font(f) => f.has_glyph(codepoint),
FontOrRef::Ref(r) => r.has_glyph(codepoint),
}
}
fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
match self {
FontOrRef::Font(f) => f.get_vertical_metrics(glyph_id),
FontOrRef::Ref(r) => r.get_vertical_metrics(glyph_id),
}
}
fn get_font_metrics(&self) -> LayoutFontMetrics {
match self {
FontOrRef::Font(f) => f.get_font_metrics(),
FontOrRef::Ref(r) => r.get_font_metrics(),
}
}
fn num_glyphs(&self) -> u16 {
match self {
FontOrRef::Font(f) => f.num_glyphs(),
FontOrRef::Ref(r) => r.num_glyphs(),
}
}
}
#[derive(Debug)]
pub struct FontManager<T> {
pub fc_cache: Arc<FcFontCache>,
pub parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
pub embedded_fonts: Mutex<HashMap<u64, azul_css::props::basic::FontRef>>,
}
impl<T: ParsedFontTrait> FontManager<T> {
pub fn new(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
Ok(Self {
fc_cache: Arc::new(fc_cache),
parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
font_chain_cache: HashMap::new(), embedded_fonts: Mutex::new(HashMap::new()),
})
}
pub fn from_arc(fc_cache: Arc<FcFontCache>) -> Result<Self, LayoutError> {
Ok(Self {
fc_cache,
parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
font_chain_cache: HashMap::new(),
embedded_fonts: Mutex::new(HashMap::new()),
})
}
pub fn from_arc_shared(
fc_cache: Arc<FcFontCache>,
parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
) -> Result<Self, LayoutError> {
Ok(Self {
fc_cache,
parsed_fonts,
font_chain_cache: HashMap::new(),
embedded_fonts: Mutex::new(HashMap::new()),
})
}
pub fn shared_parsed_fonts(&self) -> Arc<Mutex<HashMap<FontId, T>>> {
Arc::clone(&self.parsed_fonts)
}
pub fn set_font_chain_cache(
&mut self,
chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
) {
self.font_chain_cache = chains;
}
pub fn merge_font_chain_cache(
&mut self,
chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
) {
self.font_chain_cache.extend(chains);
}
pub fn get_font_chain_cache(
&self,
) -> &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain> {
&self.font_chain_cache
}
pub fn get_embedded_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef> {
let embedded = self.embedded_fonts.lock().unwrap();
embedded.get(&font_hash).cloned()
}
pub fn get_font_by_hash(&self, font_hash: u64) -> Option<T> {
let parsed = self.parsed_fonts.lock().unwrap();
for (_, font) in parsed.iter() {
if font.get_hash() == font_hash {
return Some(font.clone());
}
}
None
}
pub fn register_embedded_font(&self, font_ref: &azul_css::props::basic::FontRef) {
let hash = font_ref.get_hash();
let mut embedded = self.embedded_fonts.lock().unwrap();
embedded.insert(hash, font_ref.clone());
}
pub fn get_loaded_fonts(&self) -> LoadedFonts<T> {
let parsed = self.parsed_fonts.lock().unwrap();
parsed
.iter()
.map(|(id, font)| (id.clone(), font.shallow_clone()))
.collect()
}
pub fn get_loaded_font_ids(&self) -> std::collections::HashSet<FontId> {
let parsed = self.parsed_fonts.lock().unwrap();
parsed.keys().cloned().collect()
}
pub fn insert_font(&self, font_id: FontId, font: T) -> Option<T> {
let mut parsed = self.parsed_fonts.lock().unwrap();
parsed.insert(font_id, font)
}
pub fn insert_fonts(&self, fonts: impl IntoIterator<Item = (FontId, T)>) {
let mut parsed = self.parsed_fonts.lock().unwrap();
for (font_id, font) in fonts {
parsed.insert(font_id, font);
}
}
pub fn remove_font(&self, font_id: &FontId) -> Option<T> {
let mut parsed = self.parsed_fonts.lock().unwrap();
parsed.remove(font_id)
}
}
#[derive(Debug, thiserror::Error)]
pub enum LayoutError {
#[error("Bidi analysis failed: {0}")]
BidiError(String),
#[error("Shaping failed: {0}")]
ShapingError(String),
#[error("Font not found: {0:?}")]
FontNotFound(FontSelector),
#[error("Invalid text input: {0}")]
InvalidText(String),
#[error("Hyphenation failed: {0}")]
HyphenationError(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextBoundary {
Top,
Bottom,
Start,
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CursorBoundsError {
pub boundary: TextBoundary,
pub cursor: TextCursor,
}
#[derive(Debug, Clone)]
pub struct UnifiedConstraints {
pub shape_boundaries: Vec<ShapeBoundary>,
pub shape_exclusions: Vec<ShapeBoundary>,
pub available_width: AvailableSpace,
pub available_height: Option<f32>,
pub writing_mode: Option<WritingMode>,
pub direction: Option<BidiDirection>,
pub text_orientation: TextOrientation,
pub text_align: TextAlign,
pub text_justify: JustifyContent,
pub line_height: f32,
pub vertical_align: VerticalAlign,
pub overflow: OverflowBehavior,
pub segment_alignment: SegmentAlignment,
pub text_combine_upright: Option<TextCombineUpright>,
pub exclusion_margin: f32,
pub hyphenation: bool,
pub hyphenation_language: Option<Language>,
pub text_indent: f32,
pub initial_letter: Option<InitialLetter>,
pub line_clamp: Option<NonZeroUsize>,
pub text_wrap: TextWrap,
pub columns: u32,
pub column_gap: f32,
pub hanging_punctuation: bool,
}
impl Default for UnifiedConstraints {
fn default() -> Self {
Self {
shape_boundaries: Vec::new(),
shape_exclusions: Vec::new(),
available_width: AvailableSpace::MaxContent,
available_height: None,
writing_mode: None,
direction: None, text_orientation: TextOrientation::default(),
text_align: TextAlign::default(),
text_justify: JustifyContent::default(),
line_height: 16.0, vertical_align: VerticalAlign::default(),
overflow: OverflowBehavior::default(),
segment_alignment: SegmentAlignment::default(),
text_combine_upright: None,
exclusion_margin: 0.0,
hyphenation: false,
hyphenation_language: None,
columns: 1,
column_gap: 0.0,
hanging_punctuation: false,
text_indent: 0.0,
initial_letter: None,
line_clamp: None,
text_wrap: TextWrap::default(),
}
}
}
impl Hash for UnifiedConstraints {
fn hash<H: Hasher>(&self, state: &mut H) {
self.shape_boundaries.hash(state);
self.shape_exclusions.hash(state);
self.available_width.hash(state);
self.available_height
.map(|h| h.round() as usize)
.hash(state);
self.writing_mode.hash(state);
self.direction.hash(state);
self.text_orientation.hash(state);
self.text_align.hash(state);
self.text_justify.hash(state);
(self.line_height.round() as usize).hash(state);
self.vertical_align.hash(state);
self.overflow.hash(state);
self.text_combine_upright.hash(state);
(self.exclusion_margin.round() as usize).hash(state);
self.hyphenation.hash(state);
self.hyphenation_language.hash(state);
self.columns.hash(state);
(self.column_gap.round() as usize).hash(state);
self.hanging_punctuation.hash(state);
}
}
impl PartialEq for UnifiedConstraints {
fn eq(&self, other: &Self) -> bool {
self.shape_boundaries == other.shape_boundaries
&& self.shape_exclusions == other.shape_exclusions
&& self.available_width == other.available_width
&& match (self.available_height, other.available_height) {
(None, None) => true,
(Some(h1), Some(h2)) => round_eq(h1, h2),
_ => false,
}
&& self.writing_mode == other.writing_mode
&& self.direction == other.direction
&& self.text_orientation == other.text_orientation
&& self.text_align == other.text_align
&& self.text_justify == other.text_justify
&& round_eq(self.line_height, other.line_height)
&& self.vertical_align == other.vertical_align
&& self.overflow == other.overflow
&& self.text_combine_upright == other.text_combine_upright
&& round_eq(self.exclusion_margin, other.exclusion_margin)
&& self.hyphenation == other.hyphenation
&& self.hyphenation_language == other.hyphenation_language
&& self.columns == other.columns
&& round_eq(self.column_gap, other.column_gap)
&& self.hanging_punctuation == other.hanging_punctuation
}
}
impl Eq for UnifiedConstraints {}
impl UnifiedConstraints {
fn direction(&self, fallback: BidiDirection) -> BidiDirection {
match self.writing_mode {
Some(s) => s.get_direction().unwrap_or(fallback),
None => fallback,
}
}
fn is_vertical(&self) -> bool {
matches!(
self.writing_mode,
Some(WritingMode::VerticalRl) | Some(WritingMode::VerticalLr)
)
}
}
#[derive(Debug, Clone)]
pub struct LineConstraints {
pub segments: Vec<LineSegment>,
pub total_available: f32,
}
impl WritingMode {
fn get_direction(&self) -> Option<BidiDirection> {
match self {
WritingMode::HorizontalTb => None,
WritingMode::VerticalRl => Some(BidiDirection::Rtl),
WritingMode::VerticalLr => Some(BidiDirection::Ltr),
WritingMode::SidewaysRl => Some(BidiDirection::Rtl),
WritingMode::SidewaysLr => Some(BidiDirection::Ltr),
}
}
}
#[derive(Debug, Clone, Hash)]
pub struct StyledRun {
pub text: String,
pub style: Arc<StyleProperties>,
pub logical_start_byte: usize,
pub source_node_id: Option<NodeId>,
}
#[derive(Debug, Clone)]
pub struct VisualRun<'a> {
pub text_slice: &'a str,
pub style: Arc<StyleProperties>,
pub logical_start_byte: usize,
pub bidi_level: BidiLevel,
pub script: Script,
pub language: Language,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FontSelector {
pub family: String,
pub weight: FcWeight,
pub style: FontStyle,
pub unicode_ranges: Vec<UnicodeRange>,
}
impl Default for FontSelector {
fn default() -> Self {
Self {
family: "serif".to_string(),
weight: FcWeight::Normal,
style: FontStyle::Normal,
unicode_ranges: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub enum FontStack {
Stack(Vec<FontSelector>),
Ref(azul_css::props::basic::font::FontRef),
}
impl Default for FontStack {
fn default() -> Self {
FontStack::Stack(vec![FontSelector::default()])
}
}
impl FontStack {
pub fn is_ref(&self) -> bool {
matches!(self, FontStack::Ref(_))
}
pub fn as_ref(&self) -> Option<&azul_css::props::basic::font::FontRef> {
match self {
FontStack::Ref(r) => Some(r),
_ => None,
}
}
pub fn as_stack(&self) -> Option<&[FontSelector]> {
match self {
FontStack::Stack(s) => Some(s),
_ => None,
}
}
pub fn first_selector(&self) -> Option<&FontSelector> {
match self {
FontStack::Stack(s) => s.first(),
FontStack::Ref(_) => None,
}
}
pub fn first_family(&self) -> &str {
match self {
FontStack::Stack(s) => s.first().map(|f| f.family.as_str()).unwrap_or("serif"),
FontStack::Ref(_) => "<embedded-font>",
}
}
}
impl PartialEq for FontStack {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(FontStack::Stack(a), FontStack::Stack(b)) => a == b,
(FontStack::Ref(a), FontStack::Ref(b)) => a.parsed == b.parsed,
_ => false,
}
}
}
impl Eq for FontStack {}
impl Hash for FontStack {
fn hash<H: Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
FontStack::Stack(s) => s.hash(state),
FontStack::Ref(r) => (r.parsed as usize).hash(state),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FontHash {
pub font_hash: u64,
}
impl FontHash {
pub fn invalid() -> Self {
Self { font_hash: 0 }
}
pub fn from_hash(font_hash: u64) -> Self {
Self { font_hash }
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum FontStyle {
Normal,
Italic,
Oblique,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SegmentAlignment {
#[default]
First,
Total,
}
#[derive(Debug, Clone)]
pub struct VerticalMetrics {
pub advance: f32,
pub bearing_x: f32,
pub bearing_y: f32,
pub origin_y: f32,
}
#[derive(Debug, Clone)]
pub struct LayoutFontMetrics {
pub ascent: f32,
pub descent: f32,
pub line_gap: f32,
pub units_per_em: u16,
}
impl LayoutFontMetrics {
pub fn baseline_scaled(&self, font_size: f32) -> f32 {
let scale = font_size / self.units_per_em as f32;
self.ascent * scale
}
pub fn from_font_metrics(metrics: &azul_css::props::basic::FontMetrics) -> Self {
Self {
ascent: metrics.ascender as f32,
descent: metrics.descender as f32,
line_gap: metrics.line_gap as f32,
units_per_em: metrics.units_per_em,
}
}
}
#[derive(Debug, Clone)]
pub struct LineSegment {
pub start_x: f32,
pub width: f32,
pub priority: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum TextWrap {
#[default]
Wrap,
Balance,
NoWrap,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct InitialLetter {
pub size: f32,
pub sink: u32,
pub count: NonZeroUsize,
}
impl Eq for InitialLetter {}
impl Hash for InitialLetter {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.size.round() as usize).hash(state);
self.sink.hash(state);
self.count.hash(state);
}
}
#[derive(Debug, Clone, PartialOrd)]
pub enum PathSegment {
MoveTo(Point),
LineTo(Point),
CurveTo {
control1: Point,
control2: Point,
end: Point,
},
QuadTo {
control: Point,
end: Point,
},
Arc {
center: Point,
radius: f32,
start_angle: f32,
end_angle: f32,
},
Close,
}
impl Hash for PathSegment {
fn hash<H: Hasher>(&self, state: &mut H) {
discriminant(self).hash(state);
match self {
PathSegment::MoveTo(p) => p.hash(state),
PathSegment::LineTo(p) => p.hash(state),
PathSegment::CurveTo {
control1,
control2,
end,
} => {
control1.hash(state);
control2.hash(state);
end.hash(state);
}
PathSegment::QuadTo { control, end } => {
control.hash(state);
end.hash(state);
}
PathSegment::Arc {
center,
radius,
start_angle,
end_angle,
} => {
center.hash(state);
(radius.round() as usize).hash(state);
(start_angle.round() as usize).hash(state);
(end_angle.round() as usize).hash(state);
}
PathSegment::Close => {} }
}
}
impl PartialEq for PathSegment {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(PathSegment::MoveTo(a), PathSegment::MoveTo(b)) => a == b,
(PathSegment::LineTo(a), PathSegment::LineTo(b)) => a == b,
(
PathSegment::CurveTo {
control1: c1a,
control2: c2a,
end: ea,
},
PathSegment::CurveTo {
control1: c1b,
control2: c2b,
end: eb,
},
) => c1a == c1b && c2a == c2b && ea == eb,
(
PathSegment::QuadTo {
control: ca,
end: ea,
},
PathSegment::QuadTo {
control: cb,
end: eb,
},
) => ca == cb && ea == eb,
(
PathSegment::Arc {
center: ca,
radius: ra,
start_angle: sa_a,
end_angle: ea_a,
},
PathSegment::Arc {
center: cb,
radius: rb,
start_angle: sa_b,
end_angle: ea_b,
},
) => ca == cb && round_eq(*ra, *rb) && round_eq(*sa_a, *sa_b) && round_eq(*ea_a, *ea_b),
(PathSegment::Close, PathSegment::Close) => true,
_ => false, }
}
}
impl Eq for PathSegment {}
#[derive(Debug, Clone, Hash)]
pub enum InlineContent {
Text(StyledRun),
Image(InlineImage),
Shape(InlineShape),
Space(InlineSpace),
LineBreak(InlineBreak),
Tab {
style: Arc<StyleProperties>,
},
Marker {
run: StyledRun,
position_outside: bool,
},
Ruby {
base: Vec<InlineContent>,
text: Vec<InlineContent>,
style: Arc<StyleProperties>,
},
}
#[derive(Debug, Clone)]
pub struct InlineImage {
pub source: ImageSource,
pub intrinsic_size: Size,
pub display_size: Option<Size>,
pub baseline_offset: f32,
pub alignment: VerticalAlign,
pub object_fit: ObjectFit,
}
impl PartialEq for InlineImage {
fn eq(&self, other: &Self) -> bool {
self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
&& self.source == other.source
&& self.intrinsic_size == other.intrinsic_size
&& self.display_size == other.display_size
&& self.alignment == other.alignment
&& self.object_fit == other.object_fit
}
}
impl Eq for InlineImage {}
impl Hash for InlineImage {
fn hash<H: Hasher>(&self, state: &mut H) {
self.source.hash(state);
self.intrinsic_size.hash(state);
self.display_size.hash(state);
self.baseline_offset.to_bits().hash(state);
self.alignment.hash(state);
self.object_fit.hash(state);
}
}
impl PartialOrd for InlineImage {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for InlineImage {
fn cmp(&self, other: &Self) -> Ordering {
self.source
.cmp(&other.source)
.then_with(|| self.intrinsic_size.cmp(&other.intrinsic_size))
.then_with(|| self.display_size.cmp(&other.display_size))
.then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
.then_with(|| self.alignment.cmp(&other.alignment))
.then_with(|| self.object_fit.cmp(&other.object_fit))
}
}
#[derive(Debug, Clone)]
pub struct Glyph {
pub glyph_id: u16,
pub codepoint: char,
pub font_hash: u64,
pub font_metrics: LayoutFontMetrics,
pub style: Arc<StyleProperties>,
pub source: GlyphSource,
pub logical_byte_index: usize,
pub logical_byte_len: usize,
pub content_index: usize,
pub cluster: u32,
pub advance: f32,
pub kerning: f32,
pub offset: Point,
pub vertical_advance: f32,
pub vertical_origin_y: f32, pub vertical_bearing: Point,
pub orientation: GlyphOrientation,
pub script: Script,
pub bidi_level: BidiLevel,
}
impl Glyph {
#[inline]
fn bounds(&self) -> Rect {
Rect {
x: 0.0,
y: 0.0,
width: self.advance,
height: self.style.line_height,
}
}
#[inline]
fn character_class(&self) -> CharacterClass {
classify_character(self.codepoint as u32)
}
#[inline]
fn is_whitespace(&self) -> bool {
self.character_class() == CharacterClass::Space
}
#[inline]
fn can_justify(&self) -> bool {
!self.codepoint.is_whitespace() && self.character_class() != CharacterClass::Combining
}
#[inline]
fn justification_priority(&self) -> u8 {
get_justification_priority(self.character_class())
}
#[inline]
fn break_opportunity_after(&self) -> bool {
let is_whitespace = self.codepoint.is_whitespace();
let is_soft_hyphen = self.codepoint == '\u{00AD}';
is_whitespace || is_soft_hyphen
}
}
#[derive(Debug, Clone)]
pub struct TextRunInfo<'a> {
pub text: &'a str,
pub style: Arc<StyleProperties>,
pub logical_start: usize,
pub content_index: usize,
}
#[derive(Debug, Clone)]
pub enum ImageSource {
Ref(ImageRef),
Url(String),
Data(Arc<[u8]>),
Svg(Arc<str>),
Placeholder(Size),
}
impl PartialEq for ImageSource {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash() == b.get_hash(),
(ImageSource::Url(a), ImageSource::Url(b)) => a == b,
(ImageSource::Data(a), ImageSource::Data(b)) => Arc::ptr_eq(a, b),
(ImageSource::Svg(a), ImageSource::Svg(b)) => Arc::ptr_eq(a, b),
(ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
a.width.to_bits() == b.width.to_bits() && a.height.to_bits() == b.height.to_bits()
}
_ => false,
}
}
}
impl Eq for ImageSource {}
impl std::hash::Hash for ImageSource {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
ImageSource::Ref(r) => r.get_hash().hash(state),
ImageSource::Url(s) => s.hash(state),
ImageSource::Data(d) => (Arc::as_ptr(d) as *const u8 as usize).hash(state),
ImageSource::Svg(s) => (Arc::as_ptr(s) as *const u8 as usize).hash(state),
ImageSource::Placeholder(sz) => {
sz.width.to_bits().hash(state);
sz.height.to_bits().hash(state);
}
}
}
}
impl PartialOrd for ImageSource {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ImageSource {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
fn variant_index(s: &ImageSource) -> u8 {
match s {
ImageSource::Ref(_) => 0,
ImageSource::Url(_) => 1,
ImageSource::Data(_) => 2,
ImageSource::Svg(_) => 3,
ImageSource::Placeholder(_) => 4,
}
}
match (self, other) {
(ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash().cmp(&b.get_hash()),
(ImageSource::Url(a), ImageSource::Url(b)) => a.cmp(b),
(ImageSource::Data(a), ImageSource::Data(b)) => {
(Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
}
(ImageSource::Svg(a), ImageSource::Svg(b)) => {
(Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
}
(ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
(a.width.to_bits(), a.height.to_bits())
.cmp(&(b.width.to_bits(), b.height.to_bits()))
}
_ => variant_index(self).cmp(&variant_index(other)),
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum VerticalAlign {
#[default]
Baseline,
Bottom,
Top,
Middle,
TextTop,
TextBottom,
Sub,
Super,
Offset(f32),
}
impl std::hash::Hash for VerticalAlign {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
if let VerticalAlign::Offset(f) = self {
f.to_bits().hash(state);
}
}
}
impl Eq for VerticalAlign {}
impl Ord for VerticalAlign {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum ObjectFit {
Fill,
Contain,
Cover,
None,
ScaleDown,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InlineBorderInfo {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
pub top_color: ColorU,
pub right_color: ColorU,
pub bottom_color: ColorU,
pub left_color: ColorU,
pub radius: Option<f32>,
pub padding_top: f32,
pub padding_right: f32,
pub padding_bottom: f32,
pub padding_left: f32,
}
impl Default for InlineBorderInfo {
fn default() -> Self {
Self {
top: 0.0,
right: 0.0,
bottom: 0.0,
left: 0.0,
top_color: ColorU::TRANSPARENT,
right_color: ColorU::TRANSPARENT,
bottom_color: ColorU::TRANSPARENT,
left_color: ColorU::TRANSPARENT,
radius: None,
padding_top: 0.0,
padding_right: 0.0,
padding_bottom: 0.0,
padding_left: 0.0,
}
}
}
impl InlineBorderInfo {
pub fn has_border(&self) -> bool {
self.top > 0.0 || self.right > 0.0 || self.bottom > 0.0 || self.left > 0.0
}
pub fn has_chrome(&self) -> bool {
self.has_border()
|| self.padding_top > 0.0
|| self.padding_right > 0.0
|| self.padding_bottom > 0.0
|| self.padding_left > 0.0
}
pub fn left_inset(&self) -> f32 { self.left + self.padding_left }
pub fn right_inset(&self) -> f32 { self.right + self.padding_right }
pub fn top_inset(&self) -> f32 { self.top + self.padding_top }
pub fn bottom_inset(&self) -> f32 { self.bottom + self.padding_bottom }
}
#[derive(Debug, Clone)]
pub struct InlineShape {
pub shape_def: ShapeDefinition,
pub fill: Option<ColorU>,
pub stroke: Option<Stroke>,
pub baseline_offset: f32,
pub alignment: VerticalAlign,
pub source_node_id: Option<azul_core::dom::NodeId>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum OverflowBehavior {
Visible,
Hidden,
Scroll,
#[default]
Auto,
Break,
}
#[derive(Debug, Clone)]
pub struct MeasuredImage {
pub source: ImageSource,
pub size: Size,
pub baseline_offset: f32,
pub alignment: VerticalAlign,
pub content_index: usize,
}
#[derive(Debug, Clone)]
pub struct MeasuredShape {
pub shape_def: ShapeDefinition,
pub size: Size,
pub baseline_offset: f32,
pub alignment: VerticalAlign,
pub content_index: usize,
}
#[derive(Debug, Clone)]
pub struct InlineSpace {
pub width: f32,
pub is_breaking: bool, pub is_stretchy: bool, }
impl PartialEq for InlineSpace {
fn eq(&self, other: &Self) -> bool {
self.width.to_bits() == other.width.to_bits()
&& self.is_breaking == other.is_breaking
&& self.is_stretchy == other.is_stretchy
}
}
impl Eq for InlineSpace {}
impl Hash for InlineSpace {
fn hash<H: Hasher>(&self, state: &mut H) {
self.width.to_bits().hash(state);
self.is_breaking.hash(state);
self.is_stretchy.hash(state);
}
}
impl PartialOrd for InlineSpace {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for InlineSpace {
fn cmp(&self, other: &Self) -> Ordering {
self.width
.total_cmp(&other.width)
.then_with(|| self.is_breaking.cmp(&other.is_breaking))
.then_with(|| self.is_stretchy.cmp(&other.is_stretchy))
}
}
impl PartialEq for InlineShape {
fn eq(&self, other: &Self) -> bool {
self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
&& self.shape_def == other.shape_def
&& self.fill == other.fill
&& self.stroke == other.stroke
&& self.alignment == other.alignment
&& self.source_node_id == other.source_node_id
}
}
impl Eq for InlineShape {}
impl Hash for InlineShape {
fn hash<H: Hasher>(&self, state: &mut H) {
self.shape_def.hash(state);
self.fill.hash(state);
self.stroke.hash(state);
self.baseline_offset.to_bits().hash(state);
self.alignment.hash(state);
self.source_node_id.hash(state);
}
}
impl PartialOrd for InlineShape {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(
self.shape_def
.partial_cmp(&other.shape_def)?
.then_with(|| self.fill.cmp(&other.fill))
.then_with(|| {
self.stroke
.partial_cmp(&other.stroke)
.unwrap_or(Ordering::Equal)
})
.then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
.then_with(|| self.alignment.cmp(&other.alignment))
.then_with(|| self.source_node_id.cmp(&other.source_node_id)),
)
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl PartialEq for Rect {
fn eq(&self, other: &Self) -> bool {
round_eq(self.x, other.x)
&& round_eq(self.y, other.y)
&& round_eq(self.width, other.width)
&& round_eq(self.height, other.height)
}
}
impl Eq for Rect {}
impl Hash for Rect {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.x.round() as usize).hash(state);
(self.y.round() as usize).hash(state);
(self.width.round() as usize).hash(state);
(self.height.round() as usize).hash(state);
}
}
#[derive(Debug, Default, Clone, Copy, PartialOrd)]
pub struct Size {
pub width: f32,
pub height: f32,
}
impl Ord for Size {
fn cmp(&self, other: &Self) -> Ordering {
(self.width.round() as usize)
.cmp(&(other.width.round() as usize))
.then_with(|| (self.height.round() as usize).cmp(&(other.height.round() as usize)))
}
}
impl Hash for Size {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.width.round() as usize).hash(state);
(self.height.round() as usize).hash(state);
}
}
impl PartialEq for Size {
fn eq(&self, other: &Self) -> bool {
round_eq(self.width, other.width) && round_eq(self.height, other.height)
}
}
impl Eq for Size {}
impl Size {
pub const fn zero() -> Self {
Self::new(0.0, 0.0)
}
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
}
#[derive(Debug, Default, Clone, Copy, PartialOrd)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Hash for Point {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.x.round() as usize).hash(state);
(self.y.round() as usize).hash(state);
}
}
impl PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
round_eq(self.x, other.x) && round_eq(self.y, other.y)
}
}
impl Eq for Point {}
#[derive(Debug, Clone, PartialOrd)]
pub enum ShapeDefinition {
Rectangle {
size: Size,
corner_radius: Option<f32>,
},
Circle {
radius: f32,
},
Ellipse {
radii: Size,
},
Polygon {
points: Vec<Point>,
},
Path {
segments: Vec<PathSegment>,
},
}
impl Hash for ShapeDefinition {
fn hash<H: Hasher>(&self, state: &mut H) {
discriminant(self).hash(state);
match self {
ShapeDefinition::Rectangle {
size,
corner_radius,
} => {
size.hash(state);
corner_radius.map(|r| r.round() as usize).hash(state);
}
ShapeDefinition::Circle { radius } => {
(radius.round() as usize).hash(state);
}
ShapeDefinition::Ellipse { radii } => {
radii.hash(state);
}
ShapeDefinition::Polygon { points } => {
points.hash(state);
}
ShapeDefinition::Path { segments } => {
segments.hash(state);
}
}
}
}
impl PartialEq for ShapeDefinition {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
ShapeDefinition::Rectangle {
size: s1,
corner_radius: r1,
},
ShapeDefinition::Rectangle {
size: s2,
corner_radius: r2,
},
) => {
s1 == s2
&& match (r1, r2) {
(None, None) => true,
(Some(v1), Some(v2)) => round_eq(*v1, *v2),
_ => false,
}
}
(ShapeDefinition::Circle { radius: r1 }, ShapeDefinition::Circle { radius: r2 }) => {
round_eq(*r1, *r2)
}
(ShapeDefinition::Ellipse { radii: r1 }, ShapeDefinition::Ellipse { radii: r2 }) => {
r1 == r2
}
(ShapeDefinition::Polygon { points: p1 }, ShapeDefinition::Polygon { points: p2 }) => {
p1 == p2
}
(ShapeDefinition::Path { segments: s1 }, ShapeDefinition::Path { segments: s2 }) => {
s1 == s2
}
_ => false,
}
}
}
impl Eq for ShapeDefinition {}
impl ShapeDefinition {
pub fn get_size(&self) -> Size {
match self {
ShapeDefinition::Rectangle { size, .. } => *size,
ShapeDefinition::Circle { radius } => {
let diameter = radius * 2.0;
Size::new(diameter, diameter)
}
ShapeDefinition::Ellipse { radii } => Size::new(radii.width * 2.0, radii.height * 2.0),
ShapeDefinition::Polygon { points } => calculate_bounding_box_size(points),
ShapeDefinition::Path { segments } => {
let mut points = Vec::new();
let mut current_pos = Point { x: 0.0, y: 0.0 };
for segment in segments {
match segment {
PathSegment::MoveTo(p) | PathSegment::LineTo(p) => {
points.push(*p);
current_pos = *p;
}
PathSegment::QuadTo { control, end } => {
points.push(current_pos);
points.push(*control);
points.push(*end);
current_pos = *end;
}
PathSegment::CurveTo {
control1,
control2,
end,
} => {
points.push(current_pos);
points.push(*control1);
points.push(*control2);
points.push(*end);
current_pos = *end;
}
PathSegment::Arc {
center,
radius,
start_angle,
end_angle,
} => {
let start_point = Point {
x: center.x + radius * start_angle.cos(),
y: center.y + radius * start_angle.sin(),
};
let end_point = Point {
x: center.x + radius * end_angle.cos(),
y: center.y + radius * end_angle.sin(),
};
points.push(start_point);
points.push(end_point);
let mut normalized_end = *end_angle;
while normalized_end < *start_angle {
normalized_end += 2.0 * std::f32::consts::PI;
}
let mut check_angle = (*start_angle / std::f32::consts::FRAC_PI_2)
.ceil()
* std::f32::consts::FRAC_PI_2;
while check_angle < normalized_end {
points.push(Point {
x: center.x + radius * check_angle.cos(),
y: center.y + radius * check_angle.sin(),
});
check_angle += std::f32::consts::FRAC_PI_2;
}
current_pos = end_point;
}
PathSegment::Close => {
}
}
}
calculate_bounding_box_size(&points)
}
}
}
}
fn calculate_bounding_box_size(points: &[Point]) -> Size {
if points.is_empty() {
return Size::zero();
}
let mut min_x = f32::MAX;
let mut max_x = f32::MIN;
let mut min_y = f32::MAX;
let mut max_y = f32::MIN;
for point in points {
min_x = min_x.min(point.x);
max_x = max_x.max(point.x);
min_y = min_y.min(point.y);
max_y = max_y.max(point.y);
}
if min_x > max_x || min_y > max_y {
return Size::zero();
}
Size::new(max_x - min_x, max_y - min_y)
}
#[derive(Debug, Clone, PartialOrd)]
pub struct Stroke {
pub color: ColorU,
pub width: f32,
pub dash_pattern: Option<Vec<f32>>,
}
impl Hash for Stroke {
fn hash<H: Hasher>(&self, state: &mut H) {
self.color.hash(state);
(self.width.round() as usize).hash(state);
match &self.dash_pattern {
None => 0u8.hash(state), Some(pattern) => {
1u8.hash(state); pattern.len().hash(state); for &val in pattern {
(val.round() as usize).hash(state); }
}
}
}
}
impl PartialEq for Stroke {
fn eq(&self, other: &Self) -> bool {
if self.color != other.color || !round_eq(self.width, other.width) {
return false;
}
match (&self.dash_pattern, &other.dash_pattern) {
(None, None) => true,
(Some(p1), Some(p2)) => {
p1.len() == p2.len() && p1.iter().zip(p2.iter()).all(|(a, b)| round_eq(*a, *b))
}
_ => false,
}
}
}
impl Eq for Stroke {}
fn round_eq(a: f32, b: f32) -> bool {
(a.round() as isize) == (b.round() as isize)
}
#[derive(Debug, Clone)]
pub enum ShapeBoundary {
Rectangle(Rect),
Circle { center: Point, radius: f32 },
Ellipse { center: Point, radii: Size },
Polygon { points: Vec<Point> },
Path { segments: Vec<PathSegment> },
}
impl ShapeBoundary {
pub fn inflate(&self, margin: f32) -> Self {
if margin == 0.0 {
return self.clone();
}
match self {
Self::Rectangle(rect) => Self::Rectangle(Rect {
x: rect.x - margin,
y: rect.y - margin,
width: (rect.width + margin * 2.0).max(0.0),
height: (rect.height + margin * 2.0).max(0.0),
}),
Self::Circle { center, radius } => Self::Circle {
center: *center,
radius: radius + margin,
},
_ => self.clone(),
}
}
}
impl Hash for ShapeBoundary {
fn hash<H: Hasher>(&self, state: &mut H) {
discriminant(self).hash(state);
match self {
ShapeBoundary::Rectangle(rect) => rect.hash(state),
ShapeBoundary::Circle { center, radius } => {
center.hash(state);
(radius.round() as usize).hash(state);
}
ShapeBoundary::Ellipse { center, radii } => {
center.hash(state);
radii.hash(state);
}
ShapeBoundary::Polygon { points } => points.hash(state),
ShapeBoundary::Path { segments } => segments.hash(state),
}
}
}
impl PartialEq for ShapeBoundary {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ShapeBoundary::Rectangle(r1), ShapeBoundary::Rectangle(r2)) => r1 == r2,
(
ShapeBoundary::Circle {
center: c1,
radius: r1,
},
ShapeBoundary::Circle {
center: c2,
radius: r2,
},
) => c1 == c2 && round_eq(*r1, *r2),
(
ShapeBoundary::Ellipse {
center: c1,
radii: r1,
},
ShapeBoundary::Ellipse {
center: c2,
radii: r2,
},
) => c1 == c2 && r1 == r2,
(ShapeBoundary::Polygon { points: p1 }, ShapeBoundary::Polygon { points: p2 }) => {
p1 == p2
}
(ShapeBoundary::Path { segments: s1 }, ShapeBoundary::Path { segments: s2 }) => {
s1 == s2
}
_ => false,
}
}
}
impl Eq for ShapeBoundary {}
impl ShapeBoundary {
pub fn from_css_shape(
css_shape: &azul_css::shape::CssShape,
reference_box: Rect,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Self {
use azul_css::shape::CssShape;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Input CSS shape: {:?}",
css_shape
)));
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Reference box: {:?}",
reference_box
)));
}
let result = match css_shape {
CssShape::Circle(circle) => {
let center = Point {
x: reference_box.x + circle.center.x,
y: reference_box.y + circle.center.y,
};
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Circle - CSS center: ({}, {}), radius: {}",
circle.center.x, circle.center.y, circle.radius
)));
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Circle - Absolute center: ({}, {}), \
radius: {}",
center.x, center.y, circle.radius
)));
}
ShapeBoundary::Circle {
center,
radius: circle.radius,
}
}
CssShape::Ellipse(ellipse) => {
let center = Point {
x: reference_box.x + ellipse.center.x,
y: reference_box.y + ellipse.center.y,
};
let radii = Size {
width: ellipse.radius_x,
height: ellipse.radius_y,
};
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Ellipse - center: ({}, {}), radii: ({}, \
{})",
center.x, center.y, radii.width, radii.height
)));
}
ShapeBoundary::Ellipse { center, radii }
}
CssShape::Polygon(polygon) => {
let points = polygon
.points
.as_ref()
.iter()
.map(|pt| Point {
x: reference_box.x + pt.x,
y: reference_box.y + pt.y,
})
.collect();
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Polygon - {} points",
polygon.points.as_ref().len()
)));
}
ShapeBoundary::Polygon { points }
}
CssShape::Inset(inset) => {
let x = reference_box.x + inset.inset_left;
let y = reference_box.y + inset.inset_top;
let width = reference_box.width - inset.inset_left - inset.inset_right;
let height = reference_box.height - inset.inset_top - inset.inset_bottom;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Inset - insets: ({}, {}, {}, {})",
inset.inset_top, inset.inset_right, inset.inset_bottom, inset.inset_left
)));
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Inset - resulting rect: x={}, y={}, \
w={}, h={}",
x, y, width, height
)));
}
ShapeBoundary::Rectangle(Rect {
x,
y,
width: width.max(0.0),
height: height.max(0.0),
})
}
CssShape::Path(path) => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"[ShapeBoundary::from_css_shape] Path - fallback to rectangle".to_string(),
));
}
ShapeBoundary::Rectangle(reference_box)
}
};
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[ShapeBoundary::from_css_shape] Result: {:?}",
result
)));
}
result
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InlineBreak {
pub break_type: BreakType,
pub clear: ClearType,
pub content_index: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BreakType {
Soft, Hard, Page, Column, }
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ClearType {
None,
Left,
Right,
Both,
}
#[derive(Debug, Clone)]
pub struct ShapeConstraints {
pub boundaries: Vec<ShapeBoundary>,
pub exclusions: Vec<ShapeBoundary>,
pub writing_mode: WritingMode,
pub text_align: TextAlign,
pub line_height: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
pub enum WritingMode {
#[default]
HorizontalTb, VerticalRl, VerticalLr, SidewaysRl, SidewaysLr, }
impl WritingMode {
pub fn is_advance_horizontal(&self) -> bool {
matches!(
self,
WritingMode::HorizontalTb | WritingMode::SidewaysRl | WritingMode::SidewaysLr
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
pub enum JustifyContent {
#[default]
None,
InterWord, InterCharacter, Distribute, Kashida, }
#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
pub enum TextAlign {
#[default]
Left,
Right,
Center,
Justify,
Start,
End, JustifyAll, }
#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
pub enum TextOrientation {
#[default]
Mixed, Upright, Sideways, }
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextDecoration {
pub underline: bool,
pub strikethrough: bool,
pub overline: bool,
}
impl Default for TextDecoration {
fn default() -> Self {
TextDecoration {
underline: false,
overline: false,
strikethrough: false,
}
}
}
impl TextDecoration {
pub fn from_css(css: azul_css::props::style::text::StyleTextDecoration) -> Self {
use azul_css::props::style::text::StyleTextDecoration;
match css {
StyleTextDecoration::None => Self::default(),
StyleTextDecoration::Underline => Self {
underline: true,
strikethrough: false,
overline: false,
},
StyleTextDecoration::Overline => Self {
underline: false,
strikethrough: false,
overline: true,
},
StyleTextDecoration::LineThrough => Self {
underline: false,
strikethrough: true,
overline: false,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum TextTransform {
#[default]
None,
Uppercase,
Lowercase,
Capitalize,
}
pub type FourCc = [u8; 4];
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Spacing {
Px(i32), Em(f32),
}
impl Eq for Spacing {}
impl Hash for Spacing {
fn hash<H: Hasher>(&self, state: &mut H) {
discriminant(self).hash(state);
match self {
Spacing::Px(val) => val.hash(state),
Spacing::Em(val) => val.to_bits().hash(state),
}
}
}
impl Default for Spacing {
fn default() -> Self {
Spacing::Px(0)
}
}
impl Default for FontHash {
fn default() -> Self {
Self::invalid()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StyleProperties {
pub font_stack: FontStack,
pub font_size_px: f32,
pub color: ColorU,
pub background_color: Option<ColorU>,
pub background_content: Vec<StyleBackgroundContent>,
pub border: Option<InlineBorderInfo>,
pub letter_spacing: Spacing,
pub word_spacing: Spacing,
pub line_height: f32,
pub text_decoration: TextDecoration,
pub font_features: Vec<String>,
pub font_variations: Vec<(FourCc, f32)>,
pub tab_size: f32,
pub text_transform: TextTransform,
pub writing_mode: WritingMode,
pub text_orientation: TextOrientation,
pub text_combine_upright: Option<TextCombineUpright>,
pub font_variant_caps: FontVariantCaps,
pub font_variant_numeric: FontVariantNumeric,
pub font_variant_ligatures: FontVariantLigatures,
pub font_variant_east_asian: FontVariantEastAsian,
}
impl Default for StyleProperties {
fn default() -> Self {
const FONT_SIZE: f32 = 16.0;
const TAB_SIZE: f32 = 8.0;
Self {
font_stack: FontStack::default(),
font_size_px: FONT_SIZE,
color: ColorU::default(),
background_color: None,
background_content: Vec::new(),
border: None,
letter_spacing: Spacing::default(), word_spacing: Spacing::default(), line_height: FONT_SIZE * 1.2,
text_decoration: TextDecoration::default(),
font_features: Vec::new(),
font_variations: Vec::new(),
tab_size: TAB_SIZE, text_transform: TextTransform::default(),
writing_mode: WritingMode::default(),
text_orientation: TextOrientation::default(),
text_combine_upright: None,
font_variant_caps: FontVariantCaps::default(),
font_variant_numeric: FontVariantNumeric::default(),
font_variant_ligatures: FontVariantLigatures::default(),
font_variant_east_asian: FontVariantEastAsian::default(),
}
}
}
impl Hash for StyleProperties {
fn hash<H: Hasher>(&self, state: &mut H) {
self.font_stack.hash(state);
self.color.hash(state);
self.background_color.hash(state);
self.text_decoration.hash(state);
self.font_features.hash(state);
self.writing_mode.hash(state);
self.text_orientation.hash(state);
self.text_combine_upright.hash(state);
self.letter_spacing.hash(state);
self.word_spacing.hash(state);
(self.font_size_px.round() as usize).hash(state);
(self.line_height.round() as usize).hash(state);
}
}
impl StyleProperties {
pub fn layout_hash(&self) -> u64 {
use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.font_stack.hash(&mut hasher);
(self.font_size_px.round() as usize).hash(&mut hasher);
self.font_features.hash(&mut hasher);
for (tag, value) in &self.font_variations {
tag.hash(&mut hasher);
(value.round() as i32).hash(&mut hasher);
}
self.letter_spacing.hash(&mut hasher);
self.word_spacing.hash(&mut hasher);
(self.line_height.round() as usize).hash(&mut hasher);
(self.tab_size.round() as usize).hash(&mut hasher);
self.writing_mode.hash(&mut hasher);
self.text_orientation.hash(&mut hasher);
self.text_combine_upright.hash(&mut hasher);
self.text_transform.hash(&mut hasher);
self.font_variant_caps.hash(&mut hasher);
self.font_variant_numeric.hash(&mut hasher);
self.font_variant_ligatures.hash(&mut hasher);
self.font_variant_east_asian.hash(&mut hasher);
hasher.finish()
}
pub fn layout_eq(&self, other: &Self) -> bool {
self.layout_hash() == other.layout_hash()
}
}
#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
pub enum TextCombineUpright {
None,
All, Digits(u8), }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GlyphSource {
Char,
Hyphen,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CharacterClass {
Space, Punctuation, Letter, Ideograph, Symbol, Combining, }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GlyphOrientation {
Horizontal, Vertical, Upright, Mixed, }
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BidiDirection {
Ltr,
Rtl,
}
impl BidiDirection {
pub fn is_rtl(&self) -> bool {
matches!(self, BidiDirection::Rtl)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum FontVariantCaps {
#[default]
Normal,
SmallCaps,
AllSmallCaps,
PetiteCaps,
AllPetiteCaps,
Unicase,
TitlingCaps,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum FontVariantNumeric {
#[default]
Normal,
LiningNums,
OldstyleNums,
ProportionalNums,
TabularNums,
DiagonalFractions,
StackedFractions,
Ordinal,
SlashedZero,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum FontVariantLigatures {
#[default]
Normal,
None,
Common,
NoCommon,
Discretionary,
NoDiscretionary,
Historical,
NoHistorical,
Contextual,
NoContextual,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum FontVariantEastAsian {
#[default]
Normal,
Jis78,
Jis83,
Jis90,
Jis04,
Simplified,
Traditional,
FullWidth,
ProportionalWidth,
Ruby,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BidiLevel(u8);
impl BidiLevel {
pub fn new(level: u8) -> Self {
Self(level)
}
pub fn is_rtl(&self) -> bool {
self.0 % 2 == 1
}
pub fn level(&self) -> u8 {
self.0
}
}
#[derive(Debug, Clone)]
pub struct StyleOverride {
pub target: ContentIndex,
pub style: PartialStyleProperties,
}
#[derive(Debug, Clone, Default)]
pub struct PartialStyleProperties {
pub font_stack: Option<FontStack>,
pub font_size_px: Option<f32>,
pub color: Option<ColorU>,
pub letter_spacing: Option<Spacing>,
pub word_spacing: Option<Spacing>,
pub line_height: Option<f32>,
pub text_decoration: Option<TextDecoration>,
pub font_features: Option<Vec<String>>,
pub font_variations: Option<Vec<(FourCc, f32)>>,
pub tab_size: Option<f32>,
pub text_transform: Option<TextTransform>,
pub writing_mode: Option<WritingMode>,
pub text_orientation: Option<TextOrientation>,
pub text_combine_upright: Option<Option<TextCombineUpright>>,
pub font_variant_caps: Option<FontVariantCaps>,
pub font_variant_numeric: Option<FontVariantNumeric>,
pub font_variant_ligatures: Option<FontVariantLigatures>,
pub font_variant_east_asian: Option<FontVariantEastAsian>,
}
impl Hash for PartialStyleProperties {
fn hash<H: Hasher>(&self, state: &mut H) {
self.font_stack.hash(state);
self.font_size_px.map(|f| f.to_bits()).hash(state);
self.color.hash(state);
self.letter_spacing.hash(state);
self.word_spacing.hash(state);
self.line_height.map(|f| f.to_bits()).hash(state);
self.text_decoration.hash(state);
self.font_features.hash(state);
self.font_variations.as_ref().map(|v| {
for (tag, val) in v {
tag.hash(state);
val.to_bits().hash(state);
}
});
self.tab_size.map(|f| f.to_bits()).hash(state);
self.text_transform.hash(state);
self.writing_mode.hash(state);
self.text_orientation.hash(state);
self.text_combine_upright.hash(state);
self.font_variant_caps.hash(state);
self.font_variant_numeric.hash(state);
self.font_variant_ligatures.hash(state);
self.font_variant_east_asian.hash(state);
}
}
impl PartialEq for PartialStyleProperties {
fn eq(&self, other: &Self) -> bool {
self.font_stack == other.font_stack &&
self.font_size_px.map(|f| f.to_bits()) == other.font_size_px.map(|f| f.to_bits()) &&
self.color == other.color &&
self.letter_spacing == other.letter_spacing &&
self.word_spacing == other.word_spacing &&
self.line_height.map(|f| f.to_bits()) == other.line_height.map(|f| f.to_bits()) &&
self.text_decoration == other.text_decoration &&
self.font_features == other.font_features &&
self.font_variations == other.font_variations && self.tab_size.map(|f| f.to_bits()) == other.tab_size.map(|f| f.to_bits()) &&
self.text_transform == other.text_transform &&
self.writing_mode == other.writing_mode &&
self.text_orientation == other.text_orientation &&
self.text_combine_upright == other.text_combine_upright &&
self.font_variant_caps == other.font_variant_caps &&
self.font_variant_numeric == other.font_variant_numeric &&
self.font_variant_ligatures == other.font_variant_ligatures &&
self.font_variant_east_asian == other.font_variant_east_asian
}
}
impl Eq for PartialStyleProperties {}
impl StyleProperties {
fn apply_override(&self, partial: &PartialStyleProperties) -> Self {
let mut new_style = self.clone();
if let Some(val) = &partial.font_stack {
new_style.font_stack = val.clone();
}
if let Some(val) = partial.font_size_px {
new_style.font_size_px = val;
}
if let Some(val) = &partial.color {
new_style.color = val.clone();
}
if let Some(val) = partial.letter_spacing {
new_style.letter_spacing = val;
}
if let Some(val) = partial.word_spacing {
new_style.word_spacing = val;
}
if let Some(val) = partial.line_height {
new_style.line_height = val;
}
if let Some(val) = &partial.text_decoration {
new_style.text_decoration = val.clone();
}
if let Some(val) = &partial.font_features {
new_style.font_features = val.clone();
}
if let Some(val) = &partial.font_variations {
new_style.font_variations = val.clone();
}
if let Some(val) = partial.tab_size {
new_style.tab_size = val;
}
if let Some(val) = partial.text_transform {
new_style.text_transform = val;
}
if let Some(val) = partial.writing_mode {
new_style.writing_mode = val;
}
if let Some(val) = partial.text_orientation {
new_style.text_orientation = val;
}
if let Some(val) = &partial.text_combine_upright {
new_style.text_combine_upright = val.clone();
}
if let Some(val) = partial.font_variant_caps {
new_style.font_variant_caps = val;
}
if let Some(val) = partial.font_variant_numeric {
new_style.font_variant_numeric = val;
}
if let Some(val) = partial.font_variant_ligatures {
new_style.font_variant_ligatures = val;
}
if let Some(val) = partial.font_variant_east_asian {
new_style.font_variant_east_asian = val;
}
new_style
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GlyphKind {
Character,
Hyphen,
NotDef,
Kashida {
width: f32,
},
}
#[derive(Debug, Clone)]
pub enum LogicalItem {
Text {
source: ContentIndex,
text: String,
style: Arc<StyleProperties>,
marker_position_outside: Option<bool>,
source_node_id: Option<NodeId>,
},
CombinedText {
source: ContentIndex,
text: String,
style: Arc<StyleProperties>,
},
Ruby {
source: ContentIndex,
base_text: String,
ruby_text: String,
style: Arc<StyleProperties>,
},
Object {
source: ContentIndex,
content: InlineContent,
},
Tab {
source: ContentIndex,
style: Arc<StyleProperties>,
},
Break {
source: ContentIndex,
break_info: InlineBreak,
},
}
impl Hash for LogicalItem {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
discriminant(self).hash(state);
match self {
LogicalItem::Text {
source,
text,
style,
marker_position_outside,
source_node_id,
} => {
source.hash(state);
text.hash(state);
style.as_ref().hash(state); marker_position_outside.hash(state);
source_node_id.hash(state);
}
LogicalItem::CombinedText {
source,
text,
style,
} => {
source.hash(state);
text.hash(state);
style.as_ref().hash(state);
}
LogicalItem::Ruby {
source,
base_text,
ruby_text,
style,
} => {
source.hash(state);
base_text.hash(state);
ruby_text.hash(state);
style.as_ref().hash(state);
}
LogicalItem::Object { source, content } => {
source.hash(state);
content.hash(state);
}
LogicalItem::Tab { source, style } => {
source.hash(state);
style.as_ref().hash(state);
}
LogicalItem::Break { source, break_info } => {
source.hash(state);
break_info.hash(state);
}
}
}
}
#[derive(Debug, Clone)]
pub struct VisualItem {
pub logical_source: LogicalItem,
pub bidi_level: BidiLevel,
pub script: Script,
pub text: String,
}
#[derive(Debug, Clone)]
pub enum ShapedItem {
Cluster(ShapedCluster),
CombinedBlock {
source: ContentIndex,
glyphs: Vec<ShapedGlyph>,
bounds: Rect,
baseline_offset: f32,
},
Object {
source: ContentIndex,
bounds: Rect,
baseline_offset: f32,
content: InlineContent,
},
Tab {
source: ContentIndex,
bounds: Rect,
},
Break {
source: ContentIndex,
break_info: InlineBreak,
},
}
impl ShapedItem {
pub fn as_cluster(&self) -> Option<&ShapedCluster> {
match self {
ShapedItem::Cluster(c) => Some(c),
_ => None,
}
}
pub fn bounds(&self) -> Rect {
match self {
ShapedItem::Cluster(cluster) => {
let width = cluster.advance;
let (ascent, descent) = get_item_vertical_metrics(self);
let height = ascent + descent;
Rect {
x: 0.0,
y: 0.0,
width,
height,
}
}
ShapedItem::CombinedBlock { bounds, .. } => *bounds,
ShapedItem::Object { bounds, .. } => *bounds,
ShapedItem::Tab { bounds, .. } => *bounds,
ShapedItem::Break { .. } => Rect::default(), }
}
}
#[derive(Debug, Clone)]
pub struct ShapedCluster {
pub text: String,
pub source_cluster_id: GraphemeClusterId,
pub source_content_index: ContentIndex,
pub source_node_id: Option<NodeId>,
pub glyphs: Vec<ShapedGlyph>,
pub advance: f32,
pub direction: BidiDirection,
pub style: Arc<StyleProperties>,
pub marker_position_outside: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct ShapedGlyph {
pub kind: GlyphKind,
pub glyph_id: u16,
pub cluster_offset: u32,
pub advance: f32,
pub kerning: f32,
pub offset: Point,
pub vertical_advance: f32,
pub vertical_offset: Point,
pub script: Script,
pub style: Arc<StyleProperties>,
pub font_hash: u64,
pub font_metrics: LayoutFontMetrics,
}
impl ShapedGlyph {
pub fn into_glyph_instance<T: ParsedFontTrait>(
&self,
writing_mode: WritingMode,
loaded_fonts: &LoadedFonts<T>,
) -> GlyphInstance {
let size = loaded_fonts
.get_by_hash(self.font_hash)
.and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
.unwrap_or_default();
let position = if writing_mode.is_advance_horizontal() {
LogicalPosition {
x: self.offset.x,
y: self.offset.y,
}
} else {
LogicalPosition {
x: self.vertical_offset.x,
y: self.vertical_offset.y,
}
};
GlyphInstance {
index: self.glyph_id as u32,
point: position,
size,
}
}
pub fn into_glyph_instance_at<T: ParsedFontTrait>(
&self,
writing_mode: WritingMode,
absolute_position: LogicalPosition,
loaded_fonts: &LoadedFonts<T>,
) -> GlyphInstance {
let size = loaded_fonts
.get_by_hash(self.font_hash)
.and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
.unwrap_or_default();
GlyphInstance {
index: self.glyph_id as u32,
point: absolute_position,
size,
}
}
pub fn into_glyph_instance_at_simple(
&self,
_writing_mode: WritingMode,
absolute_position: LogicalPosition,
) -> GlyphInstance {
GlyphInstance {
index: self.glyph_id as u32,
point: absolute_position,
size: LogicalSize::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct PositionedItem {
pub item: ShapedItem,
pub position: Point,
pub line_index: usize,
}
#[derive(Debug, Clone)]
pub struct UnifiedLayout {
pub items: Vec<PositionedItem>,
pub overflow: OverflowInfo,
}
impl UnifiedLayout {
pub fn bounds(&self) -> Rect {
if self.items.is_empty() {
return Rect::default();
}
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for item in &self.items {
let item_x = item.position.x;
let item_y = item.position.y;
let item_bounds = item.item.bounds();
let item_width = item_bounds.width;
let item_height = item_bounds.height;
min_x = min_x.min(item_x);
min_y = min_y.min(item_y);
max_x = max_x.max(item_x + item_width);
max_y = max_y.max(item_y + item_height);
}
Rect {
x: min_x,
y: min_y,
width: max_x - min_x,
height: max_y - min_y,
}
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn last_baseline(&self) -> Option<f32> {
self.items
.iter()
.rev()
.find_map(|item| get_baseline_for_item(&item.item))
}
pub fn hittest_cursor(&self, point: LogicalPosition) -> Option<TextCursor> {
if self.items.is_empty() {
return None;
}
let mut closest_item_idx = 0;
let mut closest_distance = f32::MAX;
for (idx, item) in self.items.iter().enumerate() {
if !matches!(item.item, ShapedItem::Cluster(_)) {
continue;
}
let item_bounds = item.item.bounds();
let item_center_y = item.position.y + item_bounds.height / 2.0;
let vertical_distance = (point.y - item_center_y).abs();
let horizontal_distance = if point.x < item.position.x {
item.position.x - point.x
} else if point.x > item.position.x + item_bounds.width {
point.x - (item.position.x + item_bounds.width)
} else {
0.0 };
let distance = vertical_distance * 2.0 + horizontal_distance;
if distance < closest_distance {
closest_distance = distance;
closest_item_idx = idx;
}
}
let closest_item = &self.items[closest_item_idx];
let cluster = match &closest_item.item {
ShapedItem::Cluster(c) => c,
ShapedItem::Object { source, .. } | ShapedItem::CombinedBlock { source, .. } => {
return Some(TextCursor {
cluster_id: GraphemeClusterId {
source_run: source.run_index,
start_byte_in_run: source.item_index,
},
affinity: if point.x
< closest_item.position.x + (closest_item.item.bounds().width / 2.0)
{
CursorAffinity::Leading
} else {
CursorAffinity::Trailing
},
});
}
_ => return None,
};
let cluster_mid_x = closest_item.position.x + cluster.advance / 2.0;
let affinity = if point.x < cluster_mid_x {
CursorAffinity::Leading
} else {
CursorAffinity::Trailing
};
Some(TextCursor {
cluster_id: cluster.source_cluster_id,
affinity,
})
}
pub fn get_selection_rects(&self, range: &SelectionRange) -> Vec<LogicalRect> {
let mut cluster_map: HashMap<GraphemeClusterId, &PositionedItem> = HashMap::new();
for item in &self.items {
if let Some(cluster) = item.item.as_cluster() {
cluster_map.insert(cluster.source_cluster_id, item);
}
}
let (start_cursor, end_cursor) = if range.start.cluster_id > range.end.cluster_id
|| (range.start.cluster_id == range.end.cluster_id
&& range.start.affinity > range.end.affinity)
{
(range.end, range.start)
} else {
(range.start, range.end)
};
let Some(start_item) = cluster_map.get(&start_cursor.cluster_id) else {
return Vec::new();
};
let Some(end_item) = cluster_map.get(&end_cursor.cluster_id) else {
return Vec::new();
};
let mut rects = Vec::new();
let get_cursor_x = |item: &PositionedItem, affinity: CursorAffinity| -> f32 {
match affinity {
CursorAffinity::Leading => item.position.x,
CursorAffinity::Trailing => item.position.x + get_item_measure(&item.item, false),
}
};
let get_line_bounds = |line_index: usize| -> Option<LogicalRect> {
let items_on_line = self.items.iter().filter(|i| i.line_index == line_index);
let mut min_x: Option<f32> = None;
let mut max_x: Option<f32> = None;
let mut min_y: Option<f32> = None;
let mut max_y: Option<f32> = None;
for item in items_on_line {
let item_bounds = item.item.bounds();
if item_bounds.width <= 0.0 && item_bounds.height <= 0.0 {
continue;
}
let item_x_end = item.position.x + item_bounds.width;
let item_y_end = item.position.y + item_bounds.height;
min_x = Some(min_x.map_or(item.position.x, |mx| mx.min(item.position.x)));
max_x = Some(max_x.map_or(item_x_end, |mx| mx.max(item_x_end)));
min_y = Some(min_y.map_or(item.position.y, |my| my.min(item.position.y)));
max_y = Some(max_y.map_or(item_y_end, |my| my.max(item_y_end)));
}
if let (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) =
(min_x, max_x, min_y, max_y)
{
Some(LogicalRect {
origin: LogicalPosition { x: min_x, y: min_y },
size: LogicalSize {
width: max_x - min_x,
height: max_y - min_y,
},
})
} else {
None
}
};
if start_item.line_index == end_item.line_index {
if let Some(line_bounds) = get_line_bounds(start_item.line_index) {
let start_x = get_cursor_x(start_item, start_cursor.affinity);
let end_x = get_cursor_x(end_item, end_cursor.affinity);
rects.push(LogicalRect {
origin: LogicalPosition {
x: start_x.min(end_x),
y: line_bounds.origin.y,
},
size: LogicalSize {
width: (end_x - start_x).abs(),
height: line_bounds.size.height,
},
});
}
}
else {
if let Some(start_line_bounds) = get_line_bounds(start_item.line_index) {
let start_x = get_cursor_x(start_item, start_cursor.affinity);
let line_end_x = start_line_bounds.origin.x + start_line_bounds.size.width;
rects.push(LogicalRect {
origin: LogicalPosition {
x: start_x,
y: start_line_bounds.origin.y,
},
size: LogicalSize {
width: line_end_x - start_x,
height: start_line_bounds.size.height,
},
});
}
for line_idx in (start_item.line_index + 1)..end_item.line_index {
if let Some(line_bounds) = get_line_bounds(line_idx) {
rects.push(line_bounds);
}
}
if let Some(end_line_bounds) = get_line_bounds(end_item.line_index) {
let line_start_x = end_line_bounds.origin.x;
let end_x = get_cursor_x(end_item, end_cursor.affinity);
rects.push(LogicalRect {
origin: LogicalPosition {
x: line_start_x,
y: end_line_bounds.origin.y,
},
size: LogicalSize {
width: end_x - line_start_x,
height: end_line_bounds.size.height,
},
});
}
}
rects
}
pub fn get_cursor_rect(&self, cursor: &TextCursor) -> Option<LogicalRect> {
for item in &self.items {
if let ShapedItem::Cluster(cluster) = &item.item {
if cluster.source_cluster_id == cursor.cluster_id {
let line_height = item.item.bounds().height;
let cursor_x = match cursor.affinity {
CursorAffinity::Leading => item.position.x,
CursorAffinity::Trailing => item.position.x + cluster.advance,
};
return Some(LogicalRect {
origin: LogicalPosition {
x: cursor_x,
y: item.position.y,
},
size: LogicalSize {
width: 1.0,
height: line_height,
}, });
}
}
}
None
}
pub fn get_first_cluster_cursor(&self) -> Option<TextCursor> {
for item in &self.items {
if let ShapedItem::Cluster(cluster) = &item.item {
return Some(TextCursor {
cluster_id: cluster.source_cluster_id,
affinity: CursorAffinity::Leading,
});
}
}
None
}
pub fn get_last_cluster_cursor(&self) -> Option<TextCursor> {
for item in self.items.iter().rev() {
if let ShapedItem::Cluster(cluster) = &item.item {
return Some(TextCursor {
cluster_id: cluster.source_cluster_id,
affinity: CursorAffinity::Trailing,
});
}
}
None
}
pub fn move_cursor_left(
&self,
cursor: TextCursor,
debug: &mut Option<Vec<String>>,
) -> TextCursor {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_left: starting at byte {}, affinity {:?}",
cursor.cluster_id.start_byte_in_run, cursor.affinity
));
}
let current_item_pos = self.items.iter().position(|i| {
i.item
.as_cluster()
.map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
});
let Some(current_pos) = current_item_pos else {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_left: cursor not found, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
return cursor;
};
if cursor.affinity == CursorAffinity::Trailing {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_left: moving from trailing to leading edge of byte {}",
cursor.cluster_id.start_byte_in_run
));
}
return TextCursor {
cluster_id: cursor.cluster_id,
affinity: CursorAffinity::Leading,
};
}
let current_line = self.items[current_pos].line_index;
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_left: at leading edge, current line {}",
current_line
));
}
for i in (0..current_pos).rev() {
if let Some(cluster) = self.items[i].item.as_cluster() {
if self.items[i].line_index == current_line {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_left: found previous cluster on same line, byte \
{}",
cluster.source_cluster_id.start_byte_in_run
));
}
return TextCursor {
cluster_id: cluster.source_cluster_id,
affinity: CursorAffinity::Trailing,
};
}
}
}
if current_line > 0 {
let prev_line = current_line - 1;
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_left: trying previous line {}",
prev_line
));
}
for i in (0..current_pos).rev() {
if let Some(cluster) = self.items[i].item.as_cluster() {
if self.items[i].line_index == prev_line {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_left: found cluster on previous line, byte \
{}",
cluster.source_cluster_id.start_byte_in_run
));
}
return TextCursor {
cluster_id: cluster.source_cluster_id,
affinity: CursorAffinity::Trailing,
};
}
}
}
}
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_left: at start of text, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
cursor
}
pub fn move_cursor_right(
&self,
cursor: TextCursor,
debug: &mut Option<Vec<String>>,
) -> TextCursor {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_right: starting at byte {}, affinity {:?}",
cursor.cluster_id.start_byte_in_run, cursor.affinity
));
}
let current_item_pos = self.items.iter().position(|i| {
i.item
.as_cluster()
.map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
});
let Some(current_pos) = current_item_pos else {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_right: cursor not found, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
return cursor;
};
if cursor.affinity == CursorAffinity::Leading {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_right: moving from leading to trailing edge of byte {}",
cursor.cluster_id.start_byte_in_run
));
}
return TextCursor {
cluster_id: cursor.cluster_id,
affinity: CursorAffinity::Trailing,
};
}
let current_line = self.items[current_pos].line_index;
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_right: at trailing edge, current line {}",
current_line
));
}
for i in (current_pos + 1)..self.items.len() {
if let Some(cluster) = self.items[i].item.as_cluster() {
if self.items[i].line_index == current_line {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_right: found next cluster on same line, byte {}",
cluster.source_cluster_id.start_byte_in_run
));
}
return TextCursor {
cluster_id: cluster.source_cluster_id,
affinity: CursorAffinity::Leading,
};
}
}
}
let next_line = current_line + 1;
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_right: trying next line {}",
next_line
));
}
for i in (current_pos + 1)..self.items.len() {
if let Some(cluster) = self.items[i].item.as_cluster() {
if self.items[i].line_index == next_line {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_right: found cluster on next line, byte {}",
cluster.source_cluster_id.start_byte_in_run
));
}
return TextCursor {
cluster_id: cluster.source_cluster_id,
affinity: CursorAffinity::Leading,
};
}
}
}
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_right: at end of text, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
cursor
}
pub fn move_cursor_up(
&self,
cursor: TextCursor,
goal_x: &mut Option<f32>,
debug: &mut Option<Vec<String>>,
) -> TextCursor {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_up: from byte {} (affinity {:?})",
cursor.cluster_id.start_byte_in_run, cursor.affinity
));
}
let Some(current_item) = self.items.iter().find(|i| {
i.item
.as_cluster()
.map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
}) else {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_up: cursor not found in items, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
return cursor;
};
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_up: current line {}, position ({}, {})",
current_item.line_index, current_item.position.x, current_item.position.y
));
}
let target_line_idx = current_item.line_index.saturating_sub(1);
if current_item.line_index == target_line_idx {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_up: already at top line {}, staying put",
current_item.line_index
));
}
return cursor;
}
let current_x = goal_x.unwrap_or_else(|| {
let x = match cursor.affinity {
CursorAffinity::Leading => current_item.position.x,
CursorAffinity::Trailing => {
current_item.position.x + get_item_measure(¤t_item.item, false)
}
};
*goal_x = Some(x);
x
});
let target_y = self
.items
.iter()
.find(|i| i.line_index == target_line_idx)
.map(|i| i.position.y + (i.item.bounds().height / 2.0))
.unwrap_or(current_item.position.y);
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_up: target line {}, hittesting at ({}, {})",
target_line_idx, current_x, target_y
));
}
let result = self
.hittest_cursor(LogicalPosition {
x: current_x,
y: target_y,
})
.unwrap_or(cursor);
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_up: result byte {} (affinity {:?})",
result.cluster_id.start_byte_in_run, result.affinity
));
}
result
}
pub fn move_cursor_down(
&self,
cursor: TextCursor,
goal_x: &mut Option<f32>,
debug: &mut Option<Vec<String>>,
) -> TextCursor {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_down: from byte {} (affinity {:?})",
cursor.cluster_id.start_byte_in_run, cursor.affinity
));
}
let Some(current_item) = self.items.iter().find(|i| {
i.item
.as_cluster()
.map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
}) else {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_down: cursor not found in items, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
return cursor;
};
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_down: current line {}, position ({}, {})",
current_item.line_index, current_item.position.x, current_item.position.y
));
}
let max_line = self.items.iter().map(|i| i.line_index).max().unwrap_or(0);
let target_line_idx = (current_item.line_index + 1).min(max_line);
if current_item.line_index == target_line_idx {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_down: already at bottom line {}, staying put",
current_item.line_index
));
}
return cursor;
}
let current_x = goal_x.unwrap_or_else(|| {
let x = match cursor.affinity {
CursorAffinity::Leading => current_item.position.x,
CursorAffinity::Trailing => {
current_item.position.x + get_item_measure(¤t_item.item, false)
}
};
*goal_x = Some(x);
x
});
let target_y = self
.items
.iter()
.find(|i| i.line_index == target_line_idx)
.map(|i| i.position.y + (i.item.bounds().height / 2.0))
.unwrap_or(current_item.position.y);
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_down: hit testing at ({}, {})",
current_x, target_y
));
}
let result = self
.hittest_cursor(LogicalPosition {
x: current_x,
y: target_y,
})
.unwrap_or(cursor);
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_down: result byte {}, affinity {:?}",
result.cluster_id.start_byte_in_run, result.affinity
));
}
result
}
pub fn move_cursor_to_line_start(
&self,
cursor: TextCursor,
debug: &mut Option<Vec<String>>,
) -> TextCursor {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_start: starting at byte {}, affinity {:?}",
cursor.cluster_id.start_byte_in_run, cursor.affinity
));
}
let Some(current_item) = self.items.iter().find(|i| {
i.item
.as_cluster()
.map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
}) else {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_start: cursor not found, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
return cursor;
};
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_start: current line {}, position ({}, {})",
current_item.line_index, current_item.position.x, current_item.position.y
));
}
let first_item_on_line = self
.items
.iter()
.filter(|i| i.line_index == current_item.line_index)
.min_by(|a, b| {
a.position
.x
.partial_cmp(&b.position.x)
.unwrap_or(Ordering::Equal)
});
if let Some(item) = first_item_on_line {
if let ShapedItem::Cluster(c) = &item.item {
let result = TextCursor {
cluster_id: c.source_cluster_id,
affinity: CursorAffinity::Leading,
};
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_start: result byte {}, affinity {:?}",
result.cluster_id.start_byte_in_run, result.affinity
));
}
return result;
}
}
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_start: no first item found, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
cursor
}
pub fn move_cursor_to_line_end(
&self,
cursor: TextCursor,
debug: &mut Option<Vec<String>>,
) -> TextCursor {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_end: starting at byte {}, affinity {:?}",
cursor.cluster_id.start_byte_in_run, cursor.affinity
));
}
let Some(current_item) = self.items.iter().find(|i| {
i.item
.as_cluster()
.map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
}) else {
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_end: cursor not found, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
return cursor;
};
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_end: current line {}, position ({}, {})",
current_item.line_index, current_item.position.x, current_item.position.y
));
}
let last_item_on_line = self
.items
.iter()
.filter(|i| i.line_index == current_item.line_index)
.max_by(|a, b| {
a.position
.x
.partial_cmp(&b.position.x)
.unwrap_or(Ordering::Equal)
});
if let Some(item) = last_item_on_line {
if let ShapedItem::Cluster(c) = &item.item {
let result = TextCursor {
cluster_id: c.source_cluster_id,
affinity: CursorAffinity::Trailing,
};
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_end: result byte {}, affinity {:?}",
result.cluster_id.start_byte_in_run, result.affinity
));
}
return result;
}
}
if let Some(d) = debug {
d.push(format!(
"[Cursor] move_cursor_to_line_end: no last item found, staying at byte {}",
cursor.cluster_id.start_byte_in_run
));
}
cursor
}
}
fn get_baseline_for_item(item: &ShapedItem) -> Option<f32> {
match item {
ShapedItem::CombinedBlock {
baseline_offset, ..
} => Some(*baseline_offset),
ShapedItem::Object {
baseline_offset, ..
} => Some(*baseline_offset),
ShapedItem::Cluster(ref cluster) => {
if let Some(last_glyph) = cluster.glyphs.last() {
Some(
last_glyph
.font_metrics
.baseline_scaled(last_glyph.style.font_size_px),
)
} else {
None
}
}
ShapedItem::Break { source, break_info } => {
None
}
ShapedItem::Tab { source, bounds } => {
None
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OverflowInfo {
pub overflow_items: Vec<ShapedItem>,
pub unclipped_bounds: Rect,
}
impl OverflowInfo {
pub fn has_overflow(&self) -> bool {
!self.overflow_items.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct UnifiedLine {
pub items: Vec<ShapedItem>,
pub cross_axis_position: f32,
pub constraints: LineConstraints,
pub is_last: bool,
}
pub type CacheId = u64;
#[derive(Debug, Clone)]
pub struct LayoutFragment {
pub id: String,
pub constraints: UnifiedConstraints,
}
#[derive(Debug, Clone)]
pub struct FlowLayout {
pub fragment_layouts: HashMap<String, Arc<UnifiedLayout>>,
pub remaining_items: Vec<ShapedItem>,
}
pub struct LayoutCache {
logical_items: HashMap<CacheId, Arc<Vec<LogicalItem>>>,
visual_items: HashMap<CacheId, Arc<Vec<VisualItem>>>,
shaped_items: HashMap<CacheId, Arc<Vec<ShapedItem>>>,
layouts: HashMap<CacheId, Arc<UnifiedLayout>>,
}
impl LayoutCache {
pub fn new() -> Self {
Self {
logical_items: HashMap::new(),
visual_items: HashMap::new(),
shaped_items: HashMap::new(),
layouts: HashMap::new(),
}
}
pub fn get_layout(&self, cache_id: &CacheId) -> Option<&Arc<UnifiedLayout>> {
self.layouts.get(cache_id)
}
pub fn get_all_layout_ids(&self) -> Vec<CacheId> {
self.layouts.keys().copied().collect()
}
pub fn use_old_layout(
old_constraints: &UnifiedConstraints,
new_constraints: &UnifiedConstraints,
old_content: &[InlineContent],
new_content: &[InlineContent],
) -> bool {
if old_constraints != new_constraints {
return false;
}
if old_content.len() != new_content.len() {
return false;
}
for (old, new) in old_content.iter().zip(new_content.iter()) {
if !Self::inline_content_layout_eq(old, new) {
return false;
}
}
true
}
fn inline_content_layout_eq(old: &InlineContent, new: &InlineContent) -> bool {
use InlineContent::*;
match (old, new) {
(Text(old_run), Text(new_run)) => {
old_run.text == new_run.text
&& old_run.style.layout_eq(&new_run.style)
}
(Image(old_img), Image(new_img)) => {
old_img.intrinsic_size == new_img.intrinsic_size
&& old_img.display_size == new_img.display_size
&& old_img.baseline_offset == new_img.baseline_offset
&& old_img.alignment == new_img.alignment
}
(Space(old_sp), Space(new_sp)) => old_sp == new_sp,
(LineBreak(old_br), LineBreak(new_br)) => old_br == new_br,
(Tab { style: old_style }, Tab { style: new_style }) => old_style.layout_eq(new_style),
(Marker { run: old_run, position_outside: old_pos },
Marker { run: new_run, position_outside: new_pos }) => {
old_pos == new_pos
&& old_run.text == new_run.text
&& old_run.style.layout_eq(&new_run.style)
}
(Shape(old_shape), Shape(new_shape)) => {
old_shape.shape_def == new_shape.shape_def
&& old_shape.baseline_offset == new_shape.baseline_offset
}
(Ruby { base: old_base, text: old_text, style: old_style },
Ruby { base: new_base, text: new_text, style: new_style }) => {
old_style.layout_eq(new_style)
&& old_base.len() == new_base.len()
&& old_text.len() == new_text.len()
&& old_base.iter().zip(new_base.iter())
.all(|(o, n)| Self::inline_content_layout_eq(o, n))
&& old_text.iter().zip(new_text.iter())
.all(|(o, n)| Self::inline_content_layout_eq(o, n))
}
_ => false,
}
}
}
impl Default for LayoutCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct LogicalItemsKey<'a> {
pub inline_content_hash: u64, pub default_font_size: u32, pub _marker: std::marker::PhantomData<&'a ()>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct VisualItemsKey {
pub logical_items_id: CacheId,
pub base_direction: BidiDirection,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ShapedItemsKey {
pub visual_items_id: CacheId,
pub style_hash: u64, }
impl ShapedItemsKey {
pub fn new(visual_items_id: CacheId, visual_items: &[VisualItem]) -> Self {
let style_hash = {
let mut hasher = DefaultHasher::new();
for item in visual_items.iter() {
match &item.logical_source {
LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
style.as_ref().hash(&mut hasher);
}
_ => {}
}
}
hasher.finish()
};
Self {
visual_items_id,
style_hash,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct LayoutKey {
pub shaped_items_id: CacheId,
pub constraints: UnifiedConstraints,
}
fn calculate_id<T: Hash>(item: &T) -> CacheId {
let mut hasher = DefaultHasher::new();
item.hash(&mut hasher);
hasher.finish()
}
impl LayoutCache {
pub fn layout_flow<T: ParsedFontTrait>(
&mut self,
content: &[InlineContent],
style_overrides: &[StyleOverride],
flow_chain: &[LayoutFragment],
font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
fc_cache: &FcFontCache,
loaded_fonts: &LoadedFonts<T>,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<FlowLayout, LayoutError> {
let logical_items_id = calculate_id(&content);
let logical_items = self
.logical_items
.entry(logical_items_id)
.or_insert_with(|| {
Arc::new(create_logical_items(
content,
style_overrides,
debug_messages,
))
})
.clone();
let default_constraints = UnifiedConstraints::default();
let first_constraints = flow_chain
.first()
.map(|f| &f.constraints)
.unwrap_or(&default_constraints);
let base_direction = first_constraints.direction.unwrap_or(BidiDirection::Ltr);
let visual_key = VisualItemsKey {
logical_items_id,
base_direction,
};
let visual_items_id = calculate_id(&visual_key);
let visual_items = self
.visual_items
.entry(visual_items_id)
.or_insert_with(|| {
Arc::new(
reorder_logical_items(&logical_items, base_direction, debug_messages).unwrap(),
)
})
.clone();
let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
let shaped_items_id = calculate_id(&shaped_key);
let shaped_items = match self.shaped_items.get(&shaped_items_id) {
Some(cached) => {
cached.clone()
}
None => {
let items = Arc::new(shape_visual_items(
&visual_items,
font_chain_cache,
fc_cache,
loaded_fonts,
debug_messages,
)?);
self.shaped_items.insert(shaped_items_id, items.clone());
items
}
};
let oriented_items = apply_text_orientation(shaped_items, first_constraints)?;
let mut fragment_layouts = HashMap::new();
let mut cursor = BreakCursor::new(&oriented_items);
for fragment in flow_chain {
let fragment_layout = perform_fragment_layout(
&mut cursor,
&logical_items,
&fragment.constraints,
debug_messages,
loaded_fonts,
)?;
fragment_layouts.insert(fragment.id.clone(), Arc::new(fragment_layout));
if cursor.is_done() {
break; }
}
Ok(FlowLayout {
fragment_layouts,
remaining_items: cursor.drain_remaining(),
})
}
}
pub fn create_logical_items(
content: &[InlineContent],
style_overrides: &[StyleOverride],
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Vec<LogicalItem> {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"\n--- Entering create_logical_items (Refactored) ---".to_string(),
));
msgs.push(LayoutDebugMessage::info(format!(
"Input content length: {}",
content.len()
)));
msgs.push(LayoutDebugMessage::info(format!(
"Input overrides length: {}",
style_overrides.len()
)));
}
let mut items = Vec::new();
let mut style_cache: HashMap<u64, Arc<StyleProperties>> = HashMap::new();
let mut run_overrides: HashMap<u32, HashMap<u32, &PartialStyleProperties>> = HashMap::new();
for override_item in style_overrides {
run_overrides
.entry(override_item.target.run_index)
.or_default()
.insert(override_item.target.item_index, &override_item.style);
}
for (run_idx, inline_item) in content.iter().enumerate() {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Processing content run #{}",
run_idx
)));
}
let marker_position_outside = match inline_item {
InlineContent::Marker {
position_outside, ..
} => Some(*position_outside),
_ => None,
};
match inline_item {
InlineContent::Text(run) | InlineContent::Marker { run, .. } => {
let text = &run.text;
if text.is_empty() {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
" Run is empty, skipping.".to_string(),
));
}
continue;
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(" Run text: '{}'", text)));
}
let current_run_overrides = run_overrides.get(&(run_idx as u32));
let mut boundaries = BTreeSet::new();
boundaries.insert(0);
boundaries.insert(text.len());
let mut scan_cursor = 0;
while scan_cursor < text.len() {
let style_at_cursor = if let Some(partial) =
current_run_overrides.and_then(|o| o.get(&(scan_cursor as u32)))
{
run.style.apply_override(partial)
} else {
(*run.style).clone()
};
let current_char = text[scan_cursor..].chars().next().unwrap();
if let Some(TextCombineUpright::Digits(max_digits)) =
style_at_cursor.text_combine_upright
{
if max_digits > 0 && current_char.is_ascii_digit() {
let digit_chunk: String = text[scan_cursor..]
.chars()
.take(max_digits as usize)
.take_while(|c| c.is_ascii_digit())
.collect();
let end_of_chunk = scan_cursor + digit_chunk.len();
boundaries.insert(scan_cursor);
boundaries.insert(end_of_chunk);
scan_cursor = end_of_chunk; continue;
}
}
if current_run_overrides
.and_then(|o| o.get(&(scan_cursor as u32)))
.is_some()
{
let grapheme_len = text[scan_cursor..]
.graphemes(true)
.next()
.unwrap_or("")
.len();
boundaries.insert(scan_cursor);
boundaries.insert(scan_cursor + grapheme_len);
scan_cursor += grapheme_len;
continue;
}
scan_cursor += current_char.len_utf8();
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" Boundaries: {:?}",
boundaries
)));
}
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
let (start, end) = (*start, *end);
if start >= end {
continue;
}
let text_slice = &text[start..end];
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" Processing chunk from {} to {}: '{}'",
start, end, text_slice
)));
}
let style_to_use = if let Some(partial_style) =
current_run_overrides.and_then(|o| o.get(&(start as u32)))
{
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" -> Applying override at byte {}",
start
)));
}
let mut hasher = DefaultHasher::new();
Arc::as_ptr(&run.style).hash(&mut hasher);
partial_style.hash(&mut hasher);
style_cache
.entry(hasher.finish())
.or_insert_with(|| Arc::new(run.style.apply_override(partial_style)))
.clone()
} else {
run.style.clone()
};
let is_combinable_chunk = if let Some(TextCombineUpright::Digits(max_digits)) =
&style_to_use.text_combine_upright
{
*max_digits > 0
&& !text_slice.is_empty()
&& text_slice.chars().all(|c| c.is_ascii_digit())
&& text_slice.chars().count() <= *max_digits as usize
} else {
false
};
if is_combinable_chunk {
items.push(LogicalItem::CombinedText {
source: ContentIndex {
run_index: run_idx as u32,
item_index: start as u32,
},
text: text_slice.to_string(),
style: style_to_use,
});
} else {
items.push(LogicalItem::Text {
source: ContentIndex {
run_index: run_idx as u32,
item_index: start as u32,
},
text: text_slice.to_string(),
style: style_to_use,
marker_position_outside,
source_node_id: run.source_node_id,
});
}
}
}
InlineContent::LineBreak(break_info) => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" LineBreak: {:?}",
break_info
)));
}
items.push(LogicalItem::Break {
source: ContentIndex {
run_index: run_idx as u32,
item_index: 0,
},
break_info: break_info.clone(),
});
}
InlineContent::Tab { style } => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(" Tab character".to_string()));
}
items.push(LogicalItem::Tab {
source: ContentIndex {
run_index: run_idx as u32,
item_index: 0,
},
style: style.clone(),
});
}
_ => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
" Run is not text, creating generic LogicalItem.".to_string(),
));
}
items.push(LogicalItem::Object {
source: ContentIndex {
run_index: run_idx as u32,
item_index: 0,
},
content: inline_item.clone(),
});
}
}
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"--- Exiting create_logical_items, created {} items ---",
items.len()
)));
}
items
}
pub fn get_base_direction_from_logical(logical_items: &[LogicalItem]) -> BidiDirection {
let first_strong = logical_items.iter().find_map(|item| {
if let LogicalItem::Text { text, .. } = item {
Some(unicode_bidi::get_base_direction(text.as_str()))
} else {
None
}
});
match first_strong {
Some(unicode_bidi::Direction::Rtl) => BidiDirection::Rtl,
_ => BidiDirection::Ltr,
}
}
pub fn reorder_logical_items(
logical_items: &[LogicalItem],
base_direction: BidiDirection,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<Vec<VisualItem>, LayoutError> {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"\n--- Entering reorder_logical_items ---".to_string(),
));
msgs.push(LayoutDebugMessage::info(format!(
"Input logical items count: {}",
logical_items.len()
)));
msgs.push(LayoutDebugMessage::info(format!(
"Base direction: {:?}",
base_direction
)));
}
let mut bidi_str = String::new();
let mut item_map = Vec::new();
for (idx, item) in logical_items.iter().enumerate() {
let text = match item {
LogicalItem::Text { text, .. } => text.as_str(),
LogicalItem::CombinedText { text, .. } => text.as_str(),
_ => "\u{FFFC}",
};
let start_byte = bidi_str.len();
bidi_str.push_str(text);
for _ in start_byte..bidi_str.len() {
item_map.push(idx);
}
}
if bidi_str.is_empty() {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"Bidi string is empty, returning.".to_string(),
));
}
return Ok(Vec::new());
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Constructed bidi string: '{}'",
bidi_str
)));
}
let bidi_level = if base_direction == BidiDirection::Rtl {
Some(Level::rtl())
} else {
Some(Level::ltr())
};
let bidi_info = BidiInfo::new(&bidi_str, bidi_level);
let para = &bidi_info.paragraphs[0];
let (levels, visual_runs) = bidi_info.visual_runs(para, para.range.clone());
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"Bidi visual runs generated:".to_string(),
));
for (i, run_range) in visual_runs.iter().enumerate() {
let level = levels[run_range.start].number();
let slice = &bidi_str[run_range.start..run_range.end];
msgs.push(LayoutDebugMessage::info(format!(
" Run {}: range={:?}, level={}, text='{}'",
i, run_range, level, slice
)));
}
}
let mut visual_items = Vec::new();
for run_range in visual_runs {
let bidi_level = BidiLevel::new(levels[run_range.start].number());
let mut sub_run_start = run_range.start;
for i in (run_range.start + 1)..run_range.end {
if item_map[i] != item_map[sub_run_start] {
let logical_idx = item_map[sub_run_start];
let logical_item = &logical_items[logical_idx];
let text_slice = &bidi_str[sub_run_start..i];
visual_items.push(VisualItem {
logical_source: logical_item.clone(),
bidi_level,
script: crate::text3::script::detect_script(text_slice)
.unwrap_or(Script::Latin),
text: text_slice.to_string(),
});
sub_run_start = i;
}
}
let logical_idx = item_map[sub_run_start];
let logical_item = &logical_items[logical_idx];
let text_slice = &bidi_str[sub_run_start..run_range.end];
visual_items.push(VisualItem {
logical_source: logical_item.clone(),
bidi_level,
script: crate::text3::script::detect_script(text_slice).unwrap_or(Script::Latin),
text: text_slice.to_string(),
});
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"Final visual items produced:".to_string(),
));
for (i, item) in visual_items.iter().enumerate() {
msgs.push(LayoutDebugMessage::info(format!(
" Item {}: level={}, text='{}'",
i,
item.bidi_level.level(),
item.text
)));
}
msgs.push(LayoutDebugMessage::info(
"--- Exiting reorder_logical_items ---".to_string(),
));
}
Ok(visual_items)
}
pub fn shape_visual_items<T: ParsedFontTrait>(
visual_items: &[VisualItem],
font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
fc_cache: &FcFontCache,
loaded_fonts: &LoadedFonts<T>,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<Vec<ShapedItem>, LayoutError> {
let mut shaped = Vec::new();
let mut idx = 0;
let mut _coalesced_runs = 0usize;
let mut _total_runs = 0usize;
let mut _shape_calls = 0usize;
while idx < visual_items.len() {
let item = &visual_items[idx];
match &item.logical_source {
LogicalItem::Text {
style,
source,
marker_position_outside,
source_node_id,
..
} => {
let layout_hash = style.layout_hash();
let bidi_level = item.bidi_level;
let script = item.script;
let mut coalesce_end = idx + 1;
while coalesce_end < visual_items.len() {
let next = &visual_items[coalesce_end];
if let LogicalItem::Text { style: next_style, .. } = &next.logical_source {
if next_style.layout_hash() == layout_hash
&& next.bidi_level == bidi_level
&& next.script == script
{
coalesce_end += 1;
} else {
break;
}
} else {
break;
}
}
let coalesce_count = coalesce_end - idx;
if coalesce_count > 1 {
_coalesced_runs += coalesce_count;
_shape_calls += 1;
let total_text_len: usize = visual_items[idx..coalesce_end]
.iter()
.map(|v| v.text.len())
.sum();
let mut merged_text = String::with_capacity(total_text_len);
let mut byte_ranges: Vec<(
usize, usize,
Arc<StyleProperties>,
ContentIndex,
Option<NodeId>,
Option<bool>,
)> = Vec::with_capacity(coalesce_count);
for j in idx..coalesce_end {
let start = merged_text.len();
merged_text.push_str(&visual_items[j].text);
let end = merged_text.len();
if let LogicalItem::Text {
style: s, source: src, source_node_id: nid,
marker_position_outside: mpo, ..
} = &visual_items[j].logical_source {
byte_ranges.push((start, end, s.clone(), *src, *nid, *mpo));
}
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[TextLayout] Coalescing {} text runs ({} bytes) into single shaping call",
coalesce_count, merged_text.len()
)));
}
let direction = if bidi_level.is_rtl() {
BidiDirection::Rtl
} else {
BidiDirection::Ltr
};
let language = script_to_language(script, &merged_text);
let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
FontStack::Ref(font_ref) => {
shape_text_correctly(
&merged_text, script, language, direction,
font_ref, style, *source, *source_node_id,
)
}
FontStack::Stack(selectors) => {
let cache_key = FontChainKey::from_selectors(selectors);
let font_chain = match font_chain_cache.get(&cache_key) {
Some(chain) => chain,
None => { idx = coalesce_end; continue; }
};
let first_char = merged_text.chars().next().unwrap_or('A');
let font_id = match font_chain.resolve_char(fc_cache, first_char) {
Some((id, _)) => id,
None => { idx = coalesce_end; continue; }
};
match loaded_fonts.get(&font_id) {
Some(font) => shape_text_correctly(
&merged_text, script, language, direction,
font, style, *source, *source_node_id,
),
None => { idx = coalesce_end; continue; }
}
}
};
let shaped_clusters = shaped_clusters_result?;
for cluster in shaped_clusters {
let byte_pos = cluster.source_cluster_id.start_byte_in_run as usize;
let orig = byte_ranges.iter().find(|(start, end, ..)| {
byte_pos >= *start && byte_pos < *end
});
let mut cluster = cluster;
if let Some((range_start, _, orig_style, orig_source, orig_nid, orig_mpo)) = orig {
cluster.style = orig_style.clone();
cluster.source_content_index = *orig_source;
cluster.source_node_id = *orig_nid;
cluster.source_cluster_id.source_run = orig_source.run_index;
cluster.source_cluster_id.start_byte_in_run = (byte_pos - range_start) as u32;
for glyph in &mut cluster.glyphs {
glyph.style = orig_style.clone();
}
if let Some(is_outside) = orig_mpo {
cluster.marker_position_outside = Some(*is_outside);
}
}
shaped.push(ShapedItem::Cluster(cluster));
}
idx = coalesce_end;
continue;
}
_total_runs += 1;
_shape_calls += 1;
let direction = if item.bidi_level.is_rtl() {
BidiDirection::Rtl
} else {
BidiDirection::Ltr
};
let language = script_to_language(item.script, &item.text);
let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
FontStack::Ref(font_ref) => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[TextLayout] Using direct FontRef for text: '{}'",
item.text.chars().take(30).collect::<String>()
)));
}
shape_text_correctly(
&item.text,
item.script,
language,
direction,
font_ref,
style,
*source,
*source_node_id,
)
}
FontStack::Stack(selectors) => {
let cache_key = FontChainKey::from_selectors(selectors);
let font_chain = match font_chain_cache.get(&cache_key) {
Some(chain) => chain,
None => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::warning(format!(
"[TextLayout] Font chain not pre-resolved for {:?} - text will \
not be rendered",
cache_key.font_families
)));
}
idx += 1;
continue;
}
};
let first_char = item.text.chars().next().unwrap_or('A');
let font_id = match font_chain.resolve_char(fc_cache, first_char) {
Some((id, _css_source)) => id,
None => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::warning(format!(
"[TextLayout] No font in chain can render character '{}' \
(U+{:04X})",
first_char, first_char as u32
)));
}
idx += 1;
continue;
}
};
match loaded_fonts.get(&font_id) {
Some(font) => {
shape_text_correctly(
&item.text,
item.script,
language,
direction,
font,
style,
*source,
*source_node_id,
)
}
None => {
if let Some(msgs) = debug_messages {
let truncated_text = item.text.chars().take(50).collect::<String>();
let display_text = if item.text.chars().count() > 50 {
format!("{}...", truncated_text)
} else {
truncated_text
};
msgs.push(LayoutDebugMessage::warning(format!(
"[TextLayout] Font {:?} not pre-loaded for text: '{}'",
font_id, display_text
)));
}
idx += 1;
continue;
}
}
}
};
let mut shaped_clusters = shaped_clusters_result?;
if let Some(is_outside) = marker_position_outside {
for cluster in &mut shaped_clusters {
cluster.marker_position_outside = Some(*is_outside);
}
}
shaped.extend(shaped_clusters.into_iter().map(ShapedItem::Cluster));
}
LogicalItem::Tab { source, style } => {
let space_advance = style.font_size_px * 0.33;
let tab_width = style.tab_size * space_advance;
shaped.push(ShapedItem::Tab {
source: *source,
bounds: Rect {
x: 0.0,
y: 0.0,
width: tab_width,
height: 0.0,
},
});
}
LogicalItem::Ruby {
source,
base_text,
ruby_text,
style,
} => {
let placeholder_width = base_text.chars().count() as f32 * style.font_size_px * 0.6;
shaped.push(ShapedItem::Object {
source: *source,
bounds: Rect {
x: 0.0,
y: 0.0,
width: placeholder_width,
height: style.line_height * 1.5,
},
baseline_offset: 0.0,
content: InlineContent::Text(StyledRun {
text: base_text.clone(),
style: style.clone(),
logical_start_byte: 0,
source_node_id: None,
}),
});
}
LogicalItem::CombinedText {
style,
source,
text,
} => {
let language = script_to_language(item.script, &item.text);
let glyphs: Vec<Glyph> = match &style.font_stack {
FontStack::Ref(font_ref) => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[TextLayout] Using direct FontRef for CombinedText: '{}'",
text.chars().take(30).collect::<String>()
)));
}
font_ref.shape_text(
text,
item.script,
language,
BidiDirection::Ltr,
style.as_ref(),
)?
}
FontStack::Stack(selectors) => {
let cache_key = FontChainKey::from_selectors(selectors);
let font_chain = match font_chain_cache.get(&cache_key) {
Some(chain) => chain,
None => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::warning(format!(
"[TextLayout] Font chain not pre-resolved for CombinedText {:?}",
cache_key.font_families
)));
}
idx += 1;
continue;
}
};
let first_char = text.chars().next().unwrap_or('A');
let font_id = match font_chain.resolve_char(fc_cache, first_char) {
Some((id, _)) => id,
None => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::warning(format!(
"[TextLayout] No font for CombinedText char '{}'",
first_char
)));
}
idx += 1;
continue;
}
};
match loaded_fonts.get(&font_id) {
Some(font) => {
font.shape_text(
text,
item.script,
language,
BidiDirection::Ltr,
style.as_ref(),
)?
}
None => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::warning(format!(
"[TextLayout] Font {:?} not pre-loaded for CombinedText",
font_id
)));
}
idx += 1;
continue;
}
}
}
};
let shaped_glyphs = glyphs
.into_iter()
.map(|g| ShapedGlyph {
kind: GlyphKind::Character,
glyph_id: g.glyph_id,
script: g.script,
font_hash: g.font_hash,
font_metrics: g.font_metrics,
style: g.style,
cluster_offset: 0,
advance: g.advance,
kerning: g.kerning,
offset: g.offset,
vertical_advance: g.vertical_advance,
vertical_offset: g.vertical_bearing,
})
.collect::<Vec<_>>();
let total_width: f32 = shaped_glyphs.iter().map(|g| g.advance + g.kerning).sum();
let bounds = Rect {
x: 0.0,
y: 0.0,
width: total_width,
height: style.line_height,
};
shaped.push(ShapedItem::CombinedBlock {
source: *source,
glyphs: shaped_glyphs,
bounds,
baseline_offset: 0.0,
});
}
LogicalItem::Object {
content, source, ..
} => {
let (bounds, baseline) = measure_inline_object(content)?;
shaped.push(ShapedItem::Object {
source: *source,
bounds,
baseline_offset: baseline,
content: content.clone(),
});
}
LogicalItem::Break { source, break_info } => {
shaped.push(ShapedItem::Break {
source: *source,
break_info: break_info.clone(),
});
}
}
idx += 1;
}
Ok(shaped)
}
fn is_hanging_punctuation(item: &ShapedItem) -> bool {
if let ShapedItem::Cluster(c) = item {
if c.glyphs.len() == 1 {
match c.text.as_str() {
"." | "," | ":" | ";" => true,
_ => false,
}
} else {
false
}
} else {
false
}
}
fn shape_text_correctly<T: ParsedFontTrait>(
text: &str,
script: Script,
language: crate::text3::script::Language,
direction: BidiDirection,
font: &T, style: &Arc<StyleProperties>,
source_index: ContentIndex,
source_node_id: Option<NodeId>,
) -> Result<Vec<ShapedCluster>, LayoutError> {
let glyphs = font.shape_text(text, script, language, direction, style.as_ref())?;
if glyphs.is_empty() {
return Ok(Vec::new());
}
let mut clusters = Vec::new();
let mut current_cluster_glyphs = Vec::new();
let mut cluster_id = glyphs[0].cluster;
let mut cluster_start_byte_in_text = glyphs[0].logical_byte_index;
for glyph in glyphs {
if glyph.cluster != cluster_id {
let advance = current_cluster_glyphs
.iter()
.map(|g: &Glyph| g.advance)
.sum();
let (start, end) = if cluster_start_byte_in_text <= glyph.logical_byte_index {
(cluster_start_byte_in_text, glyph.logical_byte_index)
} else {
(glyph.logical_byte_index, cluster_start_byte_in_text)
};
let cluster_text = text.get(start..end).unwrap_or("");
clusters.push(ShapedCluster {
text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
source_run: source_index.run_index,
start_byte_in_run: cluster_id,
},
source_content_index: source_index,
source_node_id,
glyphs: current_cluster_glyphs
.iter()
.map(|g| {
let source_char = text
.get(g.logical_byte_index..)
.and_then(|s| s.chars().next())
.unwrap_or('\u{FFFD}');
let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
(g.logical_byte_index - cluster_start_byte_in_text) as u32
} else {
0
};
ShapedGlyph {
kind: if g.glyph_id == 0 {
GlyphKind::NotDef
} else {
GlyphKind::Character
},
glyph_id: g.glyph_id,
script: g.script,
font_hash: g.font_hash,
font_metrics: g.font_metrics.clone(),
style: g.style.clone(),
cluster_offset,
advance: g.advance,
kerning: g.kerning,
vertical_advance: g.vertical_advance,
vertical_offset: g.vertical_bearing,
offset: g.offset,
}
})
.collect(),
advance,
direction,
style: style.clone(),
marker_position_outside: None,
});
current_cluster_glyphs.clear();
cluster_id = glyph.cluster;
cluster_start_byte_in_text = glyph.logical_byte_index;
}
current_cluster_glyphs.push(glyph);
}
if !current_cluster_glyphs.is_empty() {
let advance = current_cluster_glyphs
.iter()
.map(|g: &Glyph| g.advance)
.sum();
let cluster_text = text.get(cluster_start_byte_in_text..).unwrap_or("");
clusters.push(ShapedCluster {
text: cluster_text.to_string(), source_cluster_id: GraphemeClusterId {
source_run: source_index.run_index,
start_byte_in_run: cluster_id,
},
source_content_index: source_index,
source_node_id,
glyphs: current_cluster_glyphs
.iter()
.map(|g| {
let source_char = text
.get(g.logical_byte_index..)
.and_then(|s| s.chars().next())
.unwrap_or('\u{FFFD}');
let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
(g.logical_byte_index - cluster_start_byte_in_text) as u32
} else {
0
};
ShapedGlyph {
kind: if g.glyph_id == 0 {
GlyphKind::NotDef
} else {
GlyphKind::Character
},
glyph_id: g.glyph_id,
font_hash: g.font_hash,
font_metrics: g.font_metrics.clone(),
style: g.style.clone(),
script: g.script,
vertical_advance: g.vertical_advance,
vertical_offset: g.vertical_bearing,
cluster_offset,
advance: g.advance,
kerning: g.kerning,
offset: g.offset,
}
})
.collect(),
advance,
direction,
style: style.clone(),
marker_position_outside: None,
});
}
Ok(clusters)
}
fn measure_inline_object(item: &InlineContent) -> Result<(Rect, f32), LayoutError> {
match item {
InlineContent::Image(img) => {
let size = img.display_size.unwrap_or(img.intrinsic_size);
Ok((
Rect {
x: 0.0,
y: 0.0,
width: size.width,
height: size.height,
},
img.baseline_offset,
))
}
InlineContent::Shape(shape) => Ok({
let size = shape.shape_def.get_size();
(
Rect {
x: 0.0,
y: 0.0,
width: size.width,
height: size.height,
},
shape.baseline_offset,
)
}),
InlineContent::Space(space) => Ok((
Rect {
x: 0.0,
y: 0.0,
width: space.width,
height: 0.0,
},
0.0,
)),
InlineContent::Marker { .. } => {
Err(LayoutError::InvalidText(
"Marker is text content, not a measurable object".into(),
))
}
_ => Err(LayoutError::InvalidText("Not a measurable object".into())),
}
}
fn apply_text_orientation(
items: Arc<Vec<ShapedItem>>,
constraints: &UnifiedConstraints,
) -> Result<Arc<Vec<ShapedItem>>, LayoutError> {
if !constraints.is_vertical() {
return Ok(items);
}
let mut oriented_items = Vec::with_capacity(items.len());
let writing_mode = constraints.writing_mode.unwrap_or_default();
for item in items.iter() {
match item {
ShapedItem::Cluster(cluster) => {
let mut new_cluster = cluster.clone();
let mut total_vertical_advance = 0.0;
for glyph in &mut new_cluster.glyphs {
if glyph.vertical_advance > 0.0 {
total_vertical_advance += glyph.vertical_advance;
} else {
let fallback_advance = cluster.style.line_height;
glyph.vertical_advance = fallback_advance;
glyph.vertical_offset = Point {
x: -glyph.advance / 2.0,
y: 0.0,
};
total_vertical_advance += fallback_advance;
}
}
new_cluster.advance = total_vertical_advance;
oriented_items.push(ShapedItem::Cluster(new_cluster));
}
ShapedItem::Object {
source,
bounds,
baseline_offset,
content,
} => {
let mut new_bounds = *bounds;
std::mem::swap(&mut new_bounds.width, &mut new_bounds.height);
oriented_items.push(ShapedItem::Object {
source: *source,
bounds: new_bounds,
baseline_offset: *baseline_offset,
content: content.clone(),
});
}
_ => oriented_items.push(item.clone()),
}
}
Ok(Arc::new(oriented_items))
}
fn get_item_vertical_align(item: &ShapedItem) -> Option<VerticalAlign> {
match item {
ShapedItem::Object { content, .. } => match content {
InlineContent::Image(img) => Some(img.alignment),
InlineContent::Shape(shape) => Some(shape.alignment),
_ => None,
},
_ => None,
}
}
pub fn get_item_vertical_metrics(item: &ShapedItem) -> (f32, f32) {
match item {
ShapedItem::Cluster(c) => {
if c.glyphs.is_empty() {
return (c.style.line_height, 0.0);
}
c.glyphs
.iter()
.fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
let metrics = &glyph.font_metrics;
if metrics.units_per_em == 0 {
return (max_asc, max_desc);
}
let scale = glyph.style.font_size_px / metrics.units_per_em as f32;
let item_asc = metrics.ascent * scale;
let item_desc = (-metrics.descent * scale).max(0.0);
(max_asc.max(item_asc), max_desc.max(item_desc))
})
}
ShapedItem::Object {
bounds,
baseline_offset,
..
} => {
let ascent = bounds.height - *baseline_offset;
let descent = *baseline_offset;
(ascent.max(0.0), descent.max(0.0))
}
ShapedItem::CombinedBlock {
bounds,
baseline_offset,
..
} => {
let ascent = bounds.height - *baseline_offset;
let descent = *baseline_offset;
(ascent.max(0.0), descent.max(0.0))
}
_ => (0.0, 0.0), }
}
fn calculate_line_metrics(items: &[ShapedItem]) -> (f32, f32) {
items
.iter()
.fold((0.0f32, 0.0f32), |(max_asc, max_desc), item| {
let (item_asc, item_desc) = get_item_vertical_metrics(item);
(max_asc.max(item_asc), max_desc.max(item_desc))
})
}
pub fn perform_fragment_layout<T: ParsedFontTrait>(
cursor: &mut BreakCursor,
logical_items: &[LogicalItem],
fragment_constraints: &UnifiedConstraints,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
fonts: &LoadedFonts<T>,
) -> Result<UnifiedLayout, LayoutError> {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"\n--- Entering perform_fragment_layout ---".to_string(),
));
msgs.push(LayoutDebugMessage::info(format!(
"Constraints: available_width={:?}, available_height={:?}, columns={}, text_wrap={:?}",
fragment_constraints.available_width,
fragment_constraints.available_height,
fragment_constraints.columns,
fragment_constraints.text_wrap
)));
}
if fragment_constraints.text_wrap == TextWrap::Balance {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"Using Knuth-Plass algorithm for text-wrap: balance".to_string(),
));
}
let shaped_items: Vec<ShapedItem> = cursor.drain_remaining();
let hyphenator = if fragment_constraints.hyphenation {
fragment_constraints
.hyphenation_language
.and_then(|lang| get_hyphenator(lang).ok())
} else {
None
};
return crate::text3::knuth_plass::kp_layout(
&shaped_items,
logical_items,
fragment_constraints,
hyphenator.as_ref(),
fonts,
);
}
let hyphenator = if fragment_constraints.hyphenation {
fragment_constraints
.hyphenation_language
.and_then(|lang| get_hyphenator(lang).ok())
} else {
None
};
let mut positioned_items = Vec::new();
let mut layout_bounds = Rect::default();
let num_columns = fragment_constraints.columns.max(1);
let total_column_gap = fragment_constraints.column_gap * (num_columns - 1) as f32;
let is_min_content = matches!(fragment_constraints.available_width, AvailableSpace::MinContent);
let is_max_content = matches!(fragment_constraints.available_width, AvailableSpace::MaxContent);
let column_width = match fragment_constraints.available_width {
AvailableSpace::Definite(width) => (width - total_column_gap) / num_columns as f32,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
f32::MAX / 2.0
}
};
let mut current_column = 0;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Column width calculated: {}",
column_width
)));
}
let base_direction = fragment_constraints.direction.unwrap_or(BidiDirection::Ltr);
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[PFLayout] Base direction: {:?} (from CSS), Text align: {:?}",
base_direction, fragment_constraints.text_align
)));
}
'column_loop: while current_column < num_columns {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"\n-- Starting Column {} --",
current_column
)));
}
let column_start_x =
(column_width + fragment_constraints.column_gap) * current_column as f32;
let mut line_top_y = 0.0;
let mut line_index = 0;
let mut empty_segment_count = 0; const MAX_EMPTY_SEGMENTS: usize = 1000;
while !cursor.is_done() {
if let Some(max_height) = fragment_constraints.available_height {
if line_top_y >= max_height {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" Column full (pen {} >= height {}), breaking to next column.",
line_top_y, max_height
)));
}
break;
}
}
if let Some(clamp) = fragment_constraints.line_clamp {
if line_index >= clamp.get() {
break;
}
}
let mut column_constraints = fragment_constraints.clone();
if is_min_content {
column_constraints.available_width = AvailableSpace::MinContent;
} else if is_max_content {
column_constraints.available_width = AvailableSpace::MaxContent;
} else {
column_constraints.available_width = AvailableSpace::Definite(column_width);
}
let line_constraints = get_line_constraints(
line_top_y,
fragment_constraints.line_height,
&column_constraints,
debug_messages,
);
if line_constraints.segments.is_empty() {
empty_segment_count += 1;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" No available segments at y={}, skipping to next line. (empty count: \
{}/{})",
line_top_y, empty_segment_count, MAX_EMPTY_SEGMENTS
)));
}
if empty_segment_count >= MAX_EMPTY_SEGMENTS {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::warning(format!(
" [WARN] Reached maximum empty segment count ({}). Breaking to \
prevent infinite loop.",
MAX_EMPTY_SEGMENTS
)));
msgs.push(LayoutDebugMessage::warning(
" This likely means the shape constraints are too restrictive or \
positioned incorrectly."
.to_string(),
));
msgs.push(LayoutDebugMessage::warning(format!(
" Current y={}, shape boundaries might be outside this range.",
line_top_y
)));
}
break;
}
if !fragment_constraints.shape_boundaries.is_empty() && empty_segment_count > 50 {
let max_shape_y: f32 = fragment_constraints
.shape_boundaries
.iter()
.map(|shape| {
match shape {
ShapeBoundary::Circle { center, radius } => center.y + radius,
ShapeBoundary::Ellipse { center, radii } => center.y + radii.height,
ShapeBoundary::Polygon { points } => {
points.iter().map(|p| p.y).fold(0.0, f32::max)
}
ShapeBoundary::Rectangle(rect) => rect.y + rect.height,
ShapeBoundary::Path { .. } => f32::MAX, }
})
.fold(0.0, f32::max);
if line_top_y > max_shape_y + 100.0 {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" [INFO] Current y={} is far beyond maximum shape extent y={}. \
Breaking layout.",
line_top_y, max_shape_y
)));
msgs.push(LayoutDebugMessage::info(
" Shape boundaries exist but no segments available - text cannot \
fit in shape."
.to_string(),
));
}
break;
}
}
line_top_y += fragment_constraints.line_height;
continue;
}
empty_segment_count = 0;
let (mut line_items, was_hyphenated) =
break_one_line(cursor, &line_constraints, false, hyphenator.as_ref(), fonts);
if line_items.is_empty() {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
" Break returned no items. Ending column.".to_string(),
));
}
break;
}
let line_text_before_rev: String = line_items
.iter()
.filter_map(|i| i.as_cluster())
.map(|c| c.text.as_str())
.collect();
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[PFLayout] Line items from breaker (visual order): [{}]",
line_text_before_rev
)));
}
let (mut line_pos_items, line_height) = position_one_line(
line_items,
&line_constraints,
line_top_y,
line_index,
fragment_constraints.text_align,
base_direction,
cursor.is_done() && !was_hyphenated,
fragment_constraints,
debug_messages,
fonts,
);
for item in &mut line_pos_items {
item.position.x += column_start_x;
}
line_top_y += line_height.max(fragment_constraints.line_height);
line_index += 1;
positioned_items.extend(line_pos_items);
}
current_column += 1;
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"--- Exiting perform_fragment_layout, positioned {} items ---",
positioned_items.len()
)));
}
let layout = UnifiedLayout {
items: positioned_items,
overflow: OverflowInfo::default(),
};
let calculated_bounds = layout.bounds();
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"--- Calculated bounds: width={}, height={} ---",
calculated_bounds.width, calculated_bounds.height
)));
}
Ok(layout)
}
pub fn break_one_line<T: ParsedFontTrait>(
cursor: &mut BreakCursor,
line_constraints: &LineConstraints,
is_vertical: bool,
hyphenator: Option<&Standard>,
fonts: &LoadedFonts<T>,
) -> (Vec<ShapedItem>, bool) {
let mut line_items = Vec::new();
let mut current_width = 0.0;
if cursor.is_done() {
return (Vec::new(), false);
}
while !cursor.is_done() {
let next_unit = cursor.peek_next_unit();
if next_unit.is_empty() {
break;
}
if next_unit.len() == 1 && is_word_separator(&next_unit[0]) {
cursor.consume(1);
} else {
break;
}
}
loop {
let next_unit = cursor.peek_next_unit();
if next_unit.is_empty() {
break; }
if let Some(ShapedItem::Break { .. }) = next_unit.first() {
line_items.push(next_unit[0].clone());
cursor.consume(1);
return (line_items, false);
}
let unit_width: f32 = next_unit
.iter()
.map(|item| get_item_measure(item, is_vertical))
.sum();
let available_width = line_constraints.total_available - current_width;
if unit_width <= available_width {
line_items.extend_from_slice(&next_unit);
current_width += unit_width;
cursor.consume(next_unit.len());
} else {
if let Some(hyphenator) = hyphenator {
if !is_break_opportunity(next_unit.last().unwrap()) {
if let Some(hyphenation_result) = try_hyphenate_word_cluster(
&next_unit,
available_width,
is_vertical,
hyphenator,
fonts,
) {
line_items.extend(hyphenation_result.line_part);
cursor.consume(next_unit.len());
cursor.partial_remainder = hyphenation_result.remainder_part;
return (line_items, true);
}
}
}
if line_items.is_empty() {
line_items.push(next_unit[0].clone());
cursor.consume(1);
}
break;
}
}
(line_items, false)
}
#[derive(Clone)]
pub struct HyphenationBreak {
pub char_len_on_line: usize,
pub width_on_line: f32,
pub line_part: Vec<ShapedItem>,
pub hyphen_item: ShapedItem,
pub remainder_part: Vec<ShapedItem>,
}
pub fn find_all_hyphenation_breaks<T: ParsedFontTrait>(
word_clusters: &[ShapedCluster],
hyphenator: &Standard,
is_vertical: bool, fonts: &LoadedFonts<T>,
) -> Option<Vec<HyphenationBreak>> {
if word_clusters.is_empty() {
return None;
}
let mut word_string = String::new();
let mut char_map = Vec::new();
let mut current_width = 0.0;
for (cluster_idx, cluster) in word_clusters.iter().enumerate() {
for (char_byte_offset, _ch) in cluster.text.char_indices() {
let glyph_idx = cluster
.glyphs
.iter()
.rposition(|g| g.cluster_offset as usize <= char_byte_offset)
.unwrap_or(0);
let glyph = &cluster.glyphs[glyph_idx];
let num_chars_in_glyph = cluster.text[glyph.cluster_offset as usize..]
.chars()
.count();
let advance_per_char = if is_vertical {
glyph.vertical_advance
} else {
glyph.advance
} / (num_chars_in_glyph as f32).max(1.0);
current_width += advance_per_char;
char_map.push((cluster_idx, glyph_idx, current_width));
}
word_string.push_str(&cluster.text);
}
let opportunities = hyphenator.hyphenate(&word_string);
if opportunities.breaks.is_empty() {
return None;
}
let last_cluster = word_clusters.last().unwrap();
let last_glyph = last_cluster.glyphs.last().unwrap();
let style = last_cluster.style.clone();
let font = fonts.get_by_hash(last_glyph.font_hash)?;
let (hyphen_glyph_id, hyphen_advance) =
font.get_hyphen_glyph_and_advance(style.font_size_px)?;
let mut possible_breaks = Vec::new();
for &break_char_idx in &opportunities.breaks {
if break_char_idx == 0 || break_char_idx > char_map.len() {
continue;
}
let (_, _, width_at_break) = char_map[break_char_idx - 1];
let line_part: Vec<ShapedItem> = word_clusters[..break_char_idx]
.iter()
.map(|c| ShapedItem::Cluster(c.clone()))
.collect();
let remainder_part: Vec<ShapedItem> = word_clusters[break_char_idx..]
.iter()
.map(|c| ShapedItem::Cluster(c.clone()))
.collect();
let hyphen_item = ShapedItem::Cluster(ShapedCluster {
text: "-".to_string(),
source_cluster_id: GraphemeClusterId {
source_run: u32::MAX,
start_byte_in_run: u32::MAX,
},
source_content_index: ContentIndex {
run_index: u32::MAX,
item_index: u32::MAX,
},
source_node_id: None, glyphs: vec![ShapedGlyph {
kind: GlyphKind::Hyphen,
glyph_id: hyphen_glyph_id,
font_hash: last_glyph.font_hash,
font_metrics: last_glyph.font_metrics.clone(),
cluster_offset: 0,
script: Script::Latin,
advance: hyphen_advance,
kerning: 0.0,
offset: Point::default(),
style: style.clone(),
vertical_advance: hyphen_advance,
vertical_offset: Point::default(),
}],
advance: hyphen_advance,
direction: BidiDirection::Ltr,
style: style.clone(),
marker_position_outside: None,
});
possible_breaks.push(HyphenationBreak {
char_len_on_line: break_char_idx,
width_on_line: width_at_break + hyphen_advance,
line_part,
hyphen_item,
remainder_part,
});
}
Some(possible_breaks)
}
fn try_hyphenate_word_cluster<T: ParsedFontTrait>(
word_items: &[ShapedItem],
remaining_width: f32,
is_vertical: bool,
hyphenator: &Standard,
fonts: &LoadedFonts<T>,
) -> Option<HyphenationResult> {
let word_clusters: Vec<ShapedCluster> = word_items
.iter()
.filter_map(|item| item.as_cluster().cloned())
.collect();
if word_clusters.is_empty() {
return None;
}
let all_breaks = find_all_hyphenation_breaks(&word_clusters, hyphenator, is_vertical, fonts)?;
if let Some(best_break) = all_breaks
.into_iter()
.rfind(|b| b.width_on_line <= remaining_width)
{
let mut line_part = best_break.line_part;
line_part.push(best_break.hyphen_item);
return Some(HyphenationResult {
line_part,
remainder_part: best_break.remainder_part,
});
}
None
}
pub fn position_one_line<T: ParsedFontTrait>(
line_items: Vec<ShapedItem>,
line_constraints: &LineConstraints,
line_top_y: f32,
line_index: usize,
text_align: TextAlign,
base_direction: BidiDirection,
is_last_line: bool,
constraints: &UnifiedConstraints,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
fonts: &LoadedFonts<T>,
) -> (Vec<PositionedItem>, f32) {
let line_text: String = line_items
.iter()
.filter_map(|i| i.as_cluster())
.map(|c| c.text.as_str())
.collect();
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"\n--- Entering position_one_line for line: [{}] ---",
line_text
)));
}
let physical_align = match (text_align, base_direction) {
(TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
(TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
(TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
(TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
(other, _) => other,
};
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[Pos1Line] Physical align: {:?}",
physical_align
)));
}
if line_items.is_empty() {
return (Vec::new(), 0.0);
}
let mut positioned = Vec::new();
let is_vertical = constraints.is_vertical();
let (line_ascent, line_descent) = calculate_line_metrics(&line_items);
let line_box_height = line_ascent + line_descent;
let line_baseline_y = line_top_y + line_ascent;
let mut item_cursor = 0;
let is_first_line_of_para = line_index == 0;
for (segment_idx, segment) in line_constraints.segments.iter().enumerate() {
if item_cursor >= line_items.len() {
break;
}
let mut segment_items = Vec::new();
let mut current_segment_width = 0.0;
while item_cursor < line_items.len() {
let item = &line_items[item_cursor];
let item_measure = get_item_measure(item, is_vertical);
if current_segment_width + item_measure > segment.width && !segment_items.is_empty() {
break;
}
segment_items.push(item.clone());
current_segment_width += item_measure;
item_cursor += 1;
}
if segment_items.is_empty() {
continue;
}
let (extra_word_spacing, extra_char_spacing) = if constraints.text_justify
!= JustifyContent::None
&& (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
&& constraints.text_justify != JustifyContent::Kashida
{
let segment_line_constraints = LineConstraints {
segments: vec![segment.clone()],
total_available: segment.width,
};
calculate_justification_spacing(
&segment_items,
&segment_line_constraints,
constraints.text_justify,
is_vertical,
)
} else {
(0.0, 0.0)
};
let justified_segment_items = if constraints.text_justify == JustifyContent::Kashida
&& (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
{
let segment_line_constraints = LineConstraints {
segments: vec![segment.clone()],
total_available: segment.width,
};
justify_kashida_and_rebuild(
segment_items,
&segment_line_constraints,
is_vertical,
debug_messages,
fonts,
)
} else {
segment_items
};
let final_segment_width: f32 = justified_segment_items
.iter()
.map(|item| get_item_measure(item, is_vertical))
.sum();
let remaining_space = segment.width - final_segment_width;
let is_indefinite_width = segment.width.is_infinite() || segment.width > 1e30;
let alignment_offset = if is_indefinite_width {
0.0 } else {
match physical_align {
TextAlign::Center => remaining_space / 2.0,
TextAlign::Right => remaining_space,
_ => 0.0, }
};
let mut main_axis_pen = segment.start_x + alignment_offset;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[Pos1Line] Segment width: {}, Item width: {}, Remaining space: {}, Initial pen: \
{}",
segment.width, final_segment_width, remaining_space, main_axis_pen
)));
}
if is_first_line_of_para && segment_idx == 0 {
main_axis_pen += constraints.text_indent;
}
let total_marker_width: f32 = justified_segment_items
.iter()
.filter_map(|item| {
if let ShapedItem::Cluster(c) = item {
if c.marker_position_outside == Some(true) {
return Some(get_item_measure(item, is_vertical));
}
}
None
})
.sum();
let marker_spacing = 4.0; let mut marker_pen = if total_marker_width > 0.0 {
-(total_marker_width + marker_spacing)
} else {
0.0
};
for item in justified_segment_items {
let (item_ascent, item_descent) = get_item_vertical_metrics(&item);
let effective_align = get_item_vertical_align(&item)
.unwrap_or(constraints.vertical_align);
let item_baseline_pos = match effective_align {
VerticalAlign::Top => line_top_y + item_ascent,
VerticalAlign::Middle => {
line_top_y + (line_box_height / 2.0) - ((item_ascent + item_descent) / 2.0)
+ item_ascent
}
VerticalAlign::Bottom => line_top_y + line_box_height - item_descent,
_ => line_baseline_y, };
let item_measure = get_item_measure(&item, is_vertical);
let position = if is_vertical {
Point {
x: item_baseline_pos - item_ascent,
y: main_axis_pen,
}
} else {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[Pos1Line] is_vertical=false, main_axis_pen={}, item_baseline_pos={}, \
item_ascent={}",
main_axis_pen, item_baseline_pos, item_ascent
)));
}
let x_position = if let ShapedItem::Cluster(cluster) = &item {
if cluster.marker_position_outside == Some(true) {
let marker_width = item_measure;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[Pos1Line] Outside marker detected! width={}, positioning at \
marker_pen={}",
marker_width, marker_pen
)));
}
let pos = marker_pen;
marker_pen += marker_width; pos
} else {
main_axis_pen
}
} else {
main_axis_pen
};
Point {
y: item_baseline_pos - item_ascent,
x: x_position,
}
};
let item_text = item
.as_cluster()
.map(|c| c.text.as_str())
.unwrap_or("[OBJ]");
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[Pos1Line] Positioning item '{}' at pen_x={}",
item_text, main_axis_pen
)));
}
positioned.push(PositionedItem {
item: item.clone(),
position,
line_index,
});
let is_outside_marker = if let ShapedItem::Cluster(c) = &item {
c.marker_position_outside == Some(true)
} else {
false
};
if !is_outside_marker {
main_axis_pen += item_measure;
}
if !is_outside_marker && extra_char_spacing > 0.0 && can_justify_after(&item) {
main_axis_pen += extra_char_spacing;
}
if let ShapedItem::Cluster(c) = &item {
if !is_outside_marker {
let letter_spacing_px = match c.style.letter_spacing {
Spacing::Px(px) => px as f32,
Spacing::Em(em) => em * c.style.font_size_px,
};
main_axis_pen += letter_spacing_px;
if is_word_separator(&item) {
let word_spacing_px = match c.style.word_spacing {
Spacing::Px(px) => px as f32,
Spacing::Em(em) => em * c.style.font_size_px,
};
main_axis_pen += word_spacing_px;
main_axis_pen += extra_word_spacing;
}
}
}
}
}
(positioned, line_box_height)
}
fn calculate_alignment_offset(
items: &[ShapedItem],
line_constraints: &LineConstraints,
align: TextAlign,
is_vertical: bool,
constraints: &UnifiedConstraints,
) -> f32 {
if let Some(segment) = line_constraints.segments.first() {
let total_width: f32 = items
.iter()
.map(|item| get_item_measure(item, is_vertical))
.sum();
let available_width = if constraints.segment_alignment == SegmentAlignment::Total {
line_constraints.total_available
} else {
segment.width
};
if total_width >= available_width {
return 0.0; }
let remaining_space = available_width - total_width;
match align {
TextAlign::Center => remaining_space / 2.0,
TextAlign::Right => remaining_space,
_ => 0.0, }
} else {
0.0
}
}
fn calculate_justification_spacing(
items: &[ShapedItem],
line_constraints: &LineConstraints,
text_justify: JustifyContent,
is_vertical: bool,
) -> (f32, f32) {
let total_width: f32 = items
.iter()
.map(|item| get_item_measure(item, is_vertical))
.sum();
let available_width = line_constraints.total_available;
if total_width >= available_width || available_width <= 0.0 {
return (0.0, 0.0);
}
let extra_space = available_width - total_width;
match text_justify {
JustifyContent::InterWord => {
let space_count = items.iter().filter(|item| is_word_separator(item)).count();
if space_count > 0 {
(extra_space / space_count as f32, 0.0)
} else {
(0.0, 0.0) }
}
JustifyContent::InterCharacter | JustifyContent::Distribute => {
let gap_count = items
.iter()
.enumerate()
.filter(|(i, item)| *i < items.len() - 1 && can_justify_after(item))
.count();
if gap_count > 0 {
(0.0, extra_space / gap_count as f32)
} else {
(0.0, 0.0) }
}
_ => (0.0, 0.0),
}
}
pub fn justify_kashida_and_rebuild<T: ParsedFontTrait>(
items: Vec<ShapedItem>,
line_constraints: &LineConstraints,
is_vertical: bool,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
fonts: &LoadedFonts<T>,
) -> Vec<ShapedItem> {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"\n--- Entering justify_kashida_and_rebuild ---".to_string(),
));
}
let total_width: f32 = items
.iter()
.map(|item| get_item_measure(item, is_vertical))
.sum();
let available_width = line_constraints.total_available;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Total item width: {}, Available width: {}",
total_width, available_width
)));
}
if total_width >= available_width || available_width <= 0.0 {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"No justification needed (line is full or invalid).".to_string(),
));
}
return items;
}
let extra_space = available_width - total_width;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Extra space to fill: {}",
extra_space
)));
}
let font_info = items.iter().find_map(|item| {
if let ShapedItem::Cluster(c) = item {
if let Some(glyph) = c.glyphs.first() {
if glyph.script == Script::Arabic {
if let Some(font) = fonts.get_by_hash(glyph.font_hash) {
return Some((
font.clone(),
glyph.font_hash,
glyph.font_metrics.clone(),
glyph.style.clone(),
));
}
}
}
}
None
});
let (font, font_hash, font_metrics, style) = match font_info {
Some(info) => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"Found Arabic font for kashida.".to_string(),
));
}
info
}
None => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"No Arabic font found on line. Cannot insert kashidas.".to_string(),
));
}
return items;
}
};
let (kashida_glyph_id, kashida_advance) =
match font.get_kashida_glyph_and_advance(style.font_size_px) {
Some((id, adv)) if adv > 0.0 => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Font provides kashida glyph with advance {}",
adv
)));
}
(id, adv)
}
_ => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"Font does not support kashida justification.".to_string(),
));
}
return items;
}
};
let opportunity_indices: Vec<usize> = items
.windows(2)
.enumerate()
.filter_map(|(i, window)| {
if let (ShapedItem::Cluster(cur), ShapedItem::Cluster(next)) = (&window[0], &window[1])
{
if is_arabic_cluster(cur)
&& is_arabic_cluster(next)
&& !is_word_separator(&window[1])
{
return Some(i + 1);
}
}
None
})
.collect();
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Found {} kashida insertion opportunities at indices: {:?}",
opportunity_indices.len(),
opportunity_indices
)));
}
if opportunity_indices.is_empty() {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(
"No opportunities found. Exiting.".to_string(),
));
}
return items;
}
let num_kashidas_to_insert = (extra_space / kashida_advance).floor() as usize;
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Calculated number of kashidas to insert: {}",
num_kashidas_to_insert
)));
}
if num_kashidas_to_insert == 0 {
return items;
}
let kashidas_per_point = num_kashidas_to_insert / opportunity_indices.len();
let mut remainder = num_kashidas_to_insert % opportunity_indices.len();
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Distributing kashidas: {} per point, with {} remainder.",
kashidas_per_point, remainder
)));
}
let kashida_item = {
let kashida_glyph = ShapedGlyph {
kind: GlyphKind::Kashida {
width: kashida_advance,
},
glyph_id: kashida_glyph_id,
font_hash,
font_metrics: font_metrics.clone(),
style: style.clone(),
script: Script::Arabic,
advance: kashida_advance,
kerning: 0.0,
cluster_offset: 0,
offset: Point::default(),
vertical_advance: 0.0,
vertical_offset: Point::default(),
};
ShapedItem::Cluster(ShapedCluster {
text: "\u{0640}".to_string(),
source_cluster_id: GraphemeClusterId {
source_run: u32::MAX,
start_byte_in_run: u32::MAX,
},
source_content_index: ContentIndex {
run_index: u32::MAX,
item_index: u32::MAX,
},
source_node_id: None, glyphs: vec![kashida_glyph],
advance: kashida_advance,
direction: BidiDirection::Ltr,
style,
marker_position_outside: None,
})
};
let mut new_items = Vec::with_capacity(items.len() + num_kashidas_to_insert);
let mut last_copy_idx = 0;
for &point in &opportunity_indices {
new_items.extend_from_slice(&items[last_copy_idx..point]);
let mut num_to_insert = kashidas_per_point;
if remainder > 0 {
num_to_insert += 1;
remainder -= 1;
}
for _ in 0..num_to_insert {
new_items.push(kashida_item.clone());
}
last_copy_idx = point;
}
new_items.extend_from_slice(&items[last_copy_idx..]);
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"--- Exiting justify_kashida_and_rebuild, new item count: {} ---",
new_items.len()
)));
}
new_items
}
fn is_arabic_cluster(cluster: &ShapedCluster) -> bool {
cluster.glyphs.iter().any(|g| g.script == Script::Arabic)
}
pub fn is_word_separator(item: &ShapedItem) -> bool {
if let ShapedItem::Cluster(c) = item {
c.text.chars().any(|g| g.is_whitespace())
} else {
false
}
}
fn can_justify_after(item: &ShapedItem) -> bool {
if let ShapedItem::Cluster(c) = item {
c.text.chars().last().map_or(false, |g| {
!g.is_whitespace() && classify_character(g as u32) != CharacterClass::Combining
})
} else {
!matches!(item, ShapedItem::Break { .. })
}
}
fn classify_character(codepoint: u32) -> CharacterClass {
match codepoint {
0x0020 | 0x00A0 | 0x3000 => CharacterClass::Space,
0x0021..=0x002F | 0x003A..=0x0040 | 0x005B..=0x0060 | 0x007B..=0x007E => {
CharacterClass::Punctuation
}
0x4E00..=0x9FFF | 0x3400..=0x4DBF => CharacterClass::Ideograph,
0x0300..=0x036F | 0x1AB0..=0x1AFF => CharacterClass::Combining,
0x1800..=0x18AF => CharacterClass::Letter,
_ => CharacterClass::Letter,
}
}
pub fn get_item_measure(item: &ShapedItem, is_vertical: bool) -> f32 {
match item {
ShapedItem::Cluster(c) => {
let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
c.advance + total_kerning
}
ShapedItem::Object { bounds, .. }
| ShapedItem::CombinedBlock { bounds, .. }
| ShapedItem::Tab { bounds, .. } => {
if is_vertical {
bounds.height
} else {
bounds.width
}
}
ShapedItem::Break { .. } => 0.0,
}
}
fn get_item_bounds(item: &PositionedItem) -> Rect {
let measure = get_item_measure(&item.item, false); let cross_measure = match &item.item {
ShapedItem::Object { bounds, .. } => bounds.height,
_ => 20.0, };
Rect {
x: item.position.x,
y: item.position.y,
width: measure,
height: cross_measure,
}
}
fn get_line_constraints(
line_y: f32,
line_height: f32,
constraints: &UnifiedConstraints,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> LineConstraints {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"\n--- Entering get_line_constraints for y={} ---",
line_y
)));
}
let mut available_segments = Vec::new();
if constraints.shape_boundaries.is_empty() {
let segment_width = match constraints.available_width {
AvailableSpace::Definite(w) => w, AvailableSpace::MaxContent => f32::MAX / 2.0, AvailableSpace::MinContent => f32::MAX / 2.0, };
available_segments.push(LineSegment {
start_x: 0.0,
width: segment_width,
priority: 0,
});
} else {
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Initial available segments: {:?}",
available_segments
)));
}
for (idx, exclusion) in constraints.shape_exclusions.iter().enumerate() {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Applying exclusion #{}: {:?}",
idx, exclusion
)));
}
let exclusion_spans =
get_shape_horizontal_spans(exclusion, line_y, line_height).unwrap_or_default();
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" Exclusion spans at y={}: {:?}",
line_y, exclusion_spans
)));
}
if exclusion_spans.is_empty() {
continue;
}
let mut next_segments = Vec::new();
for (excl_start, excl_end) in exclusion_spans {
for segment in &available_segments {
let seg_start = segment.start_x;
let seg_end = segment.start_x + segment.width;
if seg_end > excl_start && seg_start < excl_end {
if seg_start < excl_start {
next_segments.push(LineSegment {
start_x: seg_start,
width: excl_start - seg_start,
priority: segment.priority,
});
}
if seg_end > excl_end {
next_segments.push(LineSegment {
start_x: excl_end,
width: seg_end - excl_end,
priority: segment.priority,
});
}
} else {
next_segments.push(segment.clone()); }
}
available_segments = merge_segments(next_segments);
next_segments = Vec::new();
}
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
" Segments after exclusion #{}: {:?}",
idx, available_segments
)));
}
}
let total_width = available_segments.iter().map(|s| s.width).sum();
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"Final segments: {:?}, total available width: {}",
available_segments, total_width
)));
msgs.push(LayoutDebugMessage::info(
"--- Exiting get_line_constraints ---".to_string(),
));
}
LineConstraints {
segments: available_segments,
total_available: total_width,
}
}
fn get_shape_horizontal_spans(
shape: &ShapeBoundary,
y: f32,
line_height: f32,
) -> Result<Vec<(f32, f32)>, LayoutError> {
match shape {
ShapeBoundary::Rectangle(rect) => {
let line_start = y;
let line_end = y + line_height;
let rect_start = rect.y;
let rect_end = rect.y + rect.height;
if line_start < rect_end && line_end > rect_start {
Ok(vec![(rect.x, rect.x + rect.width)])
} else {
Ok(vec![])
}
}
ShapeBoundary::Circle { center, radius } => {
let line_center_y = y + line_height / 2.0;
let dy = (line_center_y - center.y).abs();
if dy <= *radius {
let dx = (radius.powi(2) - dy.powi(2)).sqrt();
Ok(vec![(center.x - dx, center.x + dx)])
} else {
Ok(vec![])
}
}
ShapeBoundary::Ellipse { center, radii } => {
let line_center_y = y + line_height / 2.0;
let dy = line_center_y - center.y;
if dy.abs() <= radii.height {
let y_term = dy / radii.height;
let x_term_squared = 1.0 - y_term.powi(2);
if x_term_squared >= 0.0 {
let dx = radii.width * x_term_squared.sqrt();
Ok(vec![(center.x - dx, center.x + dx)])
} else {
Ok(vec![])
}
} else {
Ok(vec![])
}
}
ShapeBoundary::Polygon { points } => {
let segments = polygon_line_intersection(points, y, line_height)?;
Ok(segments
.iter()
.map(|s| (s.start_x, s.start_x + s.width))
.collect())
}
ShapeBoundary::Path { .. } => Ok(vec![]), }
}
fn merge_segments(mut segments: Vec<LineSegment>) -> Vec<LineSegment> {
if segments.len() <= 1 {
return segments;
}
segments.sort_by(|a, b| a.start_x.partial_cmp(&b.start_x).unwrap());
let mut merged = vec![segments[0].clone()];
for next_seg in segments.iter().skip(1) {
let last = merged.last_mut().unwrap();
if next_seg.start_x <= last.start_x + last.width {
let new_width = (next_seg.start_x + next_seg.width) - last.start_x;
last.width = last.width.max(new_width);
} else {
merged.push(next_seg.clone());
}
}
merged
}
fn polygon_line_intersection(
points: &[Point],
y: f32,
line_height: f32,
) -> Result<Vec<LineSegment>, LayoutError> {
if points.len() < 3 {
return Ok(vec![]);
}
let line_center_y = y + line_height / 2.0;
let mut intersections = Vec::new();
for i in 0..points.len() {
let p1 = points[i];
let p2 = points[(i + 1) % points.len()];
if (p2.y - p1.y).abs() < f32::EPSILON {
continue;
}
let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
|| (p1.y > line_center_y && p2.y <= line_center_y);
if crosses {
let t = (line_center_y - p1.y) / (p2.y - p1.y);
let x = p1.x + t * (p2.x - p1.x);
intersections.push(x);
}
}
intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mut segments = Vec::new();
for chunk in intersections.chunks_exact(2) {
let start_x = chunk[0];
let end_x = chunk[1];
if end_x > start_x {
segments.push(LineSegment {
start_x,
width: end_x - start_x,
priority: 0,
});
}
}
Ok(segments)
}
#[cfg(feature = "text_layout_hyphenation")]
fn get_hyphenator(language: HyphenationLanguage) -> Result<Standard, LayoutError> {
Standard::from_embedded(language).map_err(|e| LayoutError::HyphenationError(e.to_string()))
}
#[cfg(not(feature = "text_layout_hyphenation"))]
fn get_hyphenator(_language: Language) -> Result<Standard, LayoutError> {
Err(LayoutError::HyphenationError("Hyphenation feature not enabled".to_string()))
}
fn is_break_opportunity(item: &ShapedItem) -> bool {
if is_word_separator(item) {
return true;
}
if let ShapedItem::Break { .. } = item {
return true;
}
if let ShapedItem::Cluster(c) = item {
if c.text.starts_with('\u{00AD}') {
return true;
}
}
false
}
pub struct BreakCursor<'a> {
pub items: &'a [ShapedItem],
pub next_item_index: usize,
pub partial_remainder: Vec<ShapedItem>,
}
impl<'a> BreakCursor<'a> {
pub fn new(items: &'a [ShapedItem]) -> Self {
Self {
items,
next_item_index: 0,
partial_remainder: Vec::new(),
}
}
pub fn is_at_start(&self) -> bool {
self.next_item_index == 0 && self.partial_remainder.is_empty()
}
pub fn drain_remaining(&mut self) -> Vec<ShapedItem> {
let mut remaining = std::mem::take(&mut self.partial_remainder);
if self.next_item_index < self.items.len() {
remaining.extend_from_slice(&self.items[self.next_item_index..]);
}
self.next_item_index = self.items.len();
remaining
}
pub fn is_done(&self) -> bool {
self.next_item_index >= self.items.len() && self.partial_remainder.is_empty()
}
pub fn consume(&mut self, count: usize) {
if count == 0 {
return;
}
let remainder_len = self.partial_remainder.len();
if count <= remainder_len {
self.partial_remainder.drain(..count);
} else {
let from_main_list = count - remainder_len;
self.partial_remainder.clear();
self.next_item_index += from_main_list;
}
}
pub fn peek_next_unit(&self) -> Vec<ShapedItem> {
let mut unit = Vec::new();
let mut source_items = self.partial_remainder.clone();
source_items.extend_from_slice(&self.items[self.next_item_index..]);
if source_items.is_empty() {
return unit;
}
if is_break_opportunity(&source_items[0]) {
unit.push(source_items[0].clone());
return unit;
}
for item in source_items {
if is_break_opportunity(&item) {
break;
}
unit.push(item.clone());
}
unit
}
}
struct HyphenationResult {
line_part: Vec<ShapedItem>,
remainder_part: Vec<ShapedItem>,
}
fn perform_bidi_analysis<'a, 'b: 'a>(
styled_runs: &'a [TextRunInfo],
full_text: &'b str,
force_lang: Option<Language>,
) -> Result<(Vec<VisualRun<'a>>, BidiDirection), LayoutError> {
if full_text.is_empty() {
return Ok((Vec::new(), BidiDirection::Ltr));
}
let bidi_info = BidiInfo::new(full_text, None);
let para = &bidi_info.paragraphs[0];
let base_direction = if para.level.is_rtl() {
BidiDirection::Rtl
} else {
BidiDirection::Ltr
};
let mut byte_to_run_index: Vec<usize> = vec![0; full_text.len()];
for (run_idx, run) in styled_runs.iter().enumerate() {
let start = run.logical_start;
let end = start + run.text.len();
for i in start..end {
byte_to_run_index[i] = run_idx;
}
}
let mut final_visual_runs = Vec::new();
let (levels, visual_run_ranges) = bidi_info.visual_runs(para, para.range.clone());
for range in visual_run_ranges {
let bidi_level = levels[range.start];
let mut sub_run_start = range.start;
for i in (range.start + 1)..range.end {
if byte_to_run_index[i] != byte_to_run_index[sub_run_start] {
let original_run_idx = byte_to_run_index[sub_run_start];
let script = crate::text3::script::detect_script(&full_text[sub_run_start..i])
.unwrap_or(Script::Latin);
final_visual_runs.push(VisualRun {
text_slice: &full_text[sub_run_start..i],
style: styled_runs[original_run_idx].style.clone(),
logical_start_byte: sub_run_start,
bidi_level: BidiLevel::new(bidi_level.number()),
language: force_lang.unwrap_or_else(|| {
crate::text3::script::script_to_language(
script,
&full_text[sub_run_start..i],
)
}),
script,
});
sub_run_start = i;
}
}
let original_run_idx = byte_to_run_index[sub_run_start];
let script = crate::text3::script::detect_script(&full_text[sub_run_start..range.end])
.unwrap_or(Script::Latin);
final_visual_runs.push(VisualRun {
text_slice: &full_text[sub_run_start..range.end],
style: styled_runs[original_run_idx].style.clone(),
logical_start_byte: sub_run_start,
bidi_level: BidiLevel::new(bidi_level.number()),
script,
language: force_lang.unwrap_or_else(|| {
crate::text3::script::script_to_language(
script,
&full_text[sub_run_start..range.end],
)
}),
});
}
Ok((final_visual_runs, base_direction))
}
fn get_justification_priority(class: CharacterClass) -> u8 {
match class {
CharacterClass::Space => 0,
CharacterClass::Punctuation => 64,
CharacterClass::Ideograph => 128,
CharacterClass::Letter => 192,
CharacterClass::Symbol => 224,
CharacterClass::Combining => 255,
}
}