use std::cell::RefCell;
use std::ops::Range;
use parley::Alignment;
use super::anchor::LayoutBounds;
use super::block::{compute_block_paints, BlockPaint};
use super::draw::marker_x_range;
use super::parser::parse;
use super::reduce::{reduce, BaselineRun, Block, BlockKind, InlineRun};
use super::shape::shape_run;
use super::style::{ResolvedStyle, RichTextStyleSheet};
use super::wrap::EdgeSpacing;
use crate::color::Color;
use crate::layout::{Measure, WidthHint};
use crate::style_vocab::{HAlign, Palette};
use crate::text::TextStyle;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RichBrush(pub Color);
impl Default for RichBrush {
fn default() -> Self {
RichBrush(Color::from_rgba8(0, 0, 0, 255))
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub enum RichTextWidth {
#[default]
Natural,
Fixed(f32),
}
pub(crate) struct MarkerLayout {
pub(crate) layout: parley::Layout<RichBrush>,
pub(crate) width_px: f32,
pub(crate) gap_px: f32,
}
pub(crate) struct BlockLayout {
pub(crate) layout: parley::Layout<RichBrush>,
pub(crate) baseline_shifts: Vec<BaselineRun>,
pub(crate) text_range: Range<usize>,
pub(crate) kind: BlockKind,
pub(crate) style: ResolvedStyle,
pub(crate) left_px: f32,
pub(crate) right_inset_px: f32,
pub(crate) shape_width_px: f32,
pub(crate) first_line_shift_px: f32,
pub(crate) continuation_shift_px: f32,
pub(crate) y_px: f32,
pub(crate) height_px: f32,
pub(crate) top_chain: Vec<EdgeSpacing>,
pub(crate) bottom_chain: Vec<EdgeSpacing>,
pub(crate) padding_top_px: f32,
pub(crate) padding_right_px: f32,
pub(crate) padding_bottom_px: f32,
pub(crate) padding_left_px: f32,
pub(crate) alignment_override: Option<Alignment>,
pub(crate) is_rtl: bool,
pub(crate) continuation_layout: Option<parley::Layout<RichBrush>>,
pub(crate) continuation_baseline_shifts: Vec<BaselineRun>,
pub(crate) continuation_inlines: Vec<InlineRun>,
pub(crate) first_line_height_px: f32,
pub(crate) source_text: String,
pub(crate) source_inlines: Vec<InlineRun>,
pub(crate) source_baselines: Vec<BaselineRun>,
pub(crate) marker: Option<MarkerLayout>,
}
impl BlockLayout {
pub(crate) fn outer_rect(&self) -> crate::geometry::Rect {
let x0 = self.left_px - self.padding_left_px;
let y0 = self.y_px - self.padding_top_px;
let x1 = self.left_px + self.shape_width_px + self.padding_right_px;
let y1 = self.y_px + self.height_px + self.padding_bottom_px;
crate::geometry::Rect::new(x0 as f64, y0 as f64, x1 as f64, y1 as f64)
}
}
pub(crate) struct Derived {
pub(crate) bounds: LayoutBounds,
pub(crate) paints: Vec<BlockPaint>,
pub(crate) ink_top: f32,
pub(crate) ink_bottom: f32,
}
pub struct RichTextRun {
pub(crate) blocks: RefCell<Vec<BlockLayout>>,
pub(crate) containers: Vec<Block>,
pub(crate) base_style: TextStyle,
pub(crate) palette: Palette,
pub(crate) base_brush: Color,
pub(crate) dpi: f64,
pub(crate) last_break: RefCell<Option<(f32, HAlign)>>,
pub(crate) derived: RefCell<Option<Derived>>,
pub(crate) natural_width_px: f32,
pub(crate) natural_height_px: f32,
pub(crate) current_height_px: RefCell<f32>,
pub(crate) min_width_px: f32,
}
impl RichTextRun {
pub fn new(
source: &str,
base_style: &TextStyle,
base_brush: Color,
sheet: &RichTextStyleSheet,
palette: &Palette,
dpi: f64,
) -> Self {
Self::new_with_width(
source,
base_style,
base_brush,
sheet,
palette,
dpi,
RichTextWidth::Natural,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_width(
source: &str,
base_style: &TextStyle,
base_brush: Color,
sheet: &RichTextStyleSheet,
palette: &Palette,
dpi: f64,
width: RichTextWidth,
) -> Self {
let events = parse(source);
let base = ResolvedStyle::from_base(base_style);
let runs = reduce(&events, sheet, &base);
let r = shape_run(runs, base_style, base_brush, palette, dpi);
if let RichTextWidth::Fixed(px) = width {
r.set_max_width(px, HAlign::Start);
}
r
}
pub fn natural_width(&self) -> f64 {
self.natural_width_px as f64
}
pub fn natural_height(&self) -> f64 {
self.natural_height_px as f64
}
pub fn current_height(&self) -> f64 {
*self.current_height_px.borrow() as f64
}
pub fn content_width(&self) -> f64 {
self.layout_bounds().width as f64
}
pub fn block_paints(&self) -> Vec<BlockPaint> {
self.with_derived(|d| d.paints.clone())
}
pub fn layout_bounds(&self) -> LayoutBounds {
self.with_derived(|d| d.bounds)
}
pub fn baseline_offset(&self) -> f64 {
self.with_derived(|d| d.bounds.first_baseline as f64)
}
pub fn ink_top_offset(&self) -> f64 {
self.with_derived(|d| d.ink_top as f64)
}
pub fn inked_height(&self) -> f64 {
self.with_derived(|d| (d.ink_bottom - d.ink_top).max(0.0) as f64)
}
pub fn cap_height(&self) -> f64 {
let blocks = self.blocks.borrow();
let Some(line) = blocks.first().and_then(|bl| bl.layout.lines().next()) else {
return 0.0;
};
let ascent_fallback = line.metrics().ascent as f64;
for item in line.items() {
if let parley::PositionedLayoutItem::GlyphRun(gr) = item {
let m = gr.run().metrics();
if let Some(h) = m.cap_height.or(m.x_height) {
return h as f64;
}
}
}
ascent_fallback * 0.7
}
fn compute_ink_band(&self, paints: &[BlockPaint]) -> (f32, f32) {
let blocks = self.blocks.borrow();
let mut top = f32::INFINITY;
let mut bottom = f32::NEG_INFINITY;
for bl in blocks.iter() {
if let Some(line) = bl.layout.lines().next() {
let m = line.metrics();
top = top.min(bl.y_px + m.baseline - m.ascent);
}
let last = match &bl.continuation_layout {
Some(cont) => cont.lines().last().map(|l| {
let m = l.metrics();
bl.y_px + bl.first_line_height_px + m.baseline + m.descent
}),
None => bl.layout.lines().last().map(|l| {
let m = l.metrics();
bl.y_px + m.baseline + m.descent
}),
};
if let Some(y) = last {
bottom = bottom.max(y);
}
}
for p in paints {
top = top.min(p.outer_rect.y0 as f32);
bottom = bottom.max(p.outer_rect.y1 as f32);
}
if !top.is_finite() {
top = 0.0;
}
if !bottom.is_finite() {
bottom = 0.0;
}
(top, bottom.max(top))
}
pub(crate) fn with_derived<T>(&self, f: impl FnOnce(&Derived) -> T) -> T {
{
let cached = self.derived.borrow();
if let Some(d) = cached.as_ref() {
return f(d);
}
}
let paints = compute_block_paints(self);
let (ink_top, ink_bottom) = self.compute_ink_band(&paints);
let derived = Derived {
bounds: self.compute_layout_bounds(),
paints,
ink_top,
ink_bottom,
};
let out = f(&derived);
*self.derived.borrow_mut() = Some(derived);
out
}
fn compute_layout_bounds(&self) -> LayoutBounds {
let blocks = self.blocks.borrow();
if blocks.is_empty() {
return LayoutBounds {
width: 0.0,
height: 0.0,
ink_left: 0.0,
ink_right: 0.0,
ink_top: 0.0,
ink_bottom: 0.0,
first_baseline: 0.0,
last_baseline: 0.0,
};
}
let mut ink_left = f32::INFINITY;
let mut ink_right = f32::NEG_INFINITY;
let mut ink_top = f32::INFINITY;
let mut ink_bottom = f32::NEG_INFINITY;
let mut first_baseline: Option<f32> = None;
let mut last_baseline: f32 = 0.0;
let mut total_width: f32 = 0.0;
let mut total_height: f32 = 0.0;
for bl in blocks.iter() {
total_width = total_width.max(bl.left_px + bl.shape_width_px + bl.right_inset_px);
total_height = total_height.max(bl.y_px + bl.height_px + bl.padding_bottom_px);
if let Some(marker) = &bl.marker {
let (m_x0, m_x1) = marker_x_range(bl, marker);
ink_left = ink_left.min(m_x0);
ink_right = ink_right.max(m_x1);
}
for (line_index, line) in bl.layout.lines().enumerate() {
let m = line.metrics();
let shift = if line_index == 0 {
bl.first_line_shift_px
} else {
bl.continuation_shift_px
};
let x_off = bl.left_px + shift;
let y_off = bl.y_px;
ink_left = ink_left.min(m.inline_min_coord + x_off);
ink_right = ink_right.max(m.inline_max_coord + x_off);
ink_top = ink_top.min(m.block_min_coord + y_off);
ink_bottom = ink_bottom.max(m.block_max_coord + y_off);
if first_baseline.is_none() {
first_baseline = Some(m.baseline + y_off);
}
last_baseline = m.baseline + y_off;
}
}
if !ink_left.is_finite() {
ink_left = 0.0;
}
if !ink_right.is_finite() {
ink_right = 0.0;
}
if !ink_top.is_finite() {
ink_top = 0.0;
}
if !ink_bottom.is_finite() {
ink_bottom = 0.0;
}
LayoutBounds {
width: total_width,
height: total_height,
ink_left,
ink_right,
ink_top,
ink_bottom,
first_baseline: first_baseline.unwrap_or(0.0),
last_baseline,
}
}
}
impl Measure for RichTextRun {
fn width_hint(&self, _dpi: f64) -> WidthHint {
WidthHint::Min(self.min_width_px as f64)
}
fn height_at(&self, width: f64, _dpi: f64) -> f64 {
let _ = self.set_max_width(width as f32, HAlign::Start);
self.inked_height()
}
}