use std::{
cell::{Cell, OnceCell, RefCell},
ops::Range,
rc::Rc,
sync::Arc,
};
use cranpose_core::{
PackedU64,
collections::pass_aged::{IDLE_PASSES, PassAgedCache},
};
use cranpose_text::{
CaretDirection, FontExtent, FontFamily, FontFamilyResolver, FontRequest, FontStyle, FontWeight,
LayoutInput, ParagraphIntrinsics, TextLayout, TextLayouter, paragraph_line_box,
};
use cranpose_ui_graphics::{MemoryHeadroom, Size, TextLayoutHandle};
use super::{
AnnotatedString, LineBox, RenderString, SpanStyle, TextAlign, TextDrawStyle, TextUnit,
layout_cache::{CacheAdmission, Cached, LayoutCache},
layout_input::{DEFAULT_FONT_SIZE_SP, ParagraphSpans, TextUnits},
layout_options::{TextLayoutOptions, TextOverflow},
style::TextStyle,
text_key::TextKey,
};
use crate::{density::Density, font_scale::FontScaleCurve, text_layout_result::TextLayoutResult};
const TEXT_INTRINSICS_CACHE_CAPACITY: usize = 1024;
const SCALE_DOWN_SEARCH_STEPS: usize = 14;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextMetrics {
pub width: f32,
pub height: f32,
pub line_height: f32,
pub line_count: usize,
}
#[derive(Clone, Debug)]
pub struct PreparedTextLayout {
pub text: Rc<AnnotatedString>,
pub visual_style: Arc<TextStyle>,
pub layout: TextLayout,
max_width: f32,
min_lines: u32,
pub render_text: OnceCell<(Arc<RenderString>, PackedU64)>,
pub(crate) visual_style_hash: OnceCell<PackedU64>,
layout_handle: OnceCell<TextLayoutHandle>,
}
impl PreparedTextLayout {
fn new(
text: Rc<AnnotatedString>,
style: Arc<TextStyle>,
layout: TextLayout,
(max_width, min_lines): (f32, usize),
) -> Self {
Self {
max_width,
min_lines: u32::try_from(min_lines).unwrap_or(u32::MAX),
text,
visual_style: style,
layout,
render_text: OnceCell::new(),
visual_style_hash: OnceCell::new(),
layout_handle: OnceCell::new(),
}
}
pub fn size(&self) -> Size {
Size::new(
self.layout.width().min(self.max_width),
self.layout.height(),
)
}
pub(crate) fn max_widths_kept(&self, min_width: f32) -> cranpose_ui_layout::BoundRange {
let width = self.layout.width();
if self.max_width < width {
return cranpose_ui_layout::BoundRange::exactly(self.max_width);
}
let (low, high) = self.layout.max_widths_kept(min_width);
cranpose_ui_layout::BoundRange {
low: low.max(width),
high,
}
}
pub fn metrics(&self) -> TextMetrics {
let layout = &self.layout;
let size = self.size();
TextMetrics {
width: size.width,
height: size.height,
line_height: layout.line_advance(),
line_count: layout
.line_count()
.max(usize::try_from(self.min_lines).unwrap_or(usize::MAX)),
}
}
pub fn alignment_lines(&self) -> cranpose_ui_layout::AlignmentLines {
let layout = &self.layout;
cranpose_ui_layout::AlignmentLines::new(
Some(layout.line_baseline(0)),
Some(layout.line_baseline(layout.line_count() - 1)),
)
}
pub fn did_overflow(&self) -> bool {
self.layout.did_overflow_height() || self.layout.did_overflow_width()
}
pub(crate) fn layout_handle(&self) -> TextLayoutHandle {
self.layout_handle
.get_or_init(|| TextLayoutHandle::from(Arc::new(self.layout.clone())))
.clone()
}
pub(crate) fn at_widths(
self: &Rc<Self>,
widths: (f32, f32),
align: TextAlign,
spare: Option<Rc<Self>>,
) -> Rc<Self> {
let width = self.layout.width_between(widths.0, widths.1);
if width.to_bits() == self.layout.width().to_bits() {
return Rc::clone(self);
}
self.aligned_at(widths, align, spare)
}
#[inline(never)]
fn aligned_at(
self: &Rc<Self>,
widths: (f32, f32),
align: TextAlign,
spare: Option<Rc<Self>>,
) -> Rc<Self> {
let mut aligned = Self {
min_lines: self.min_lines,
render_text: self.render_text.clone(),
visual_style_hash: self.visual_style_hash.clone(),
..Self::new(
Rc::clone(&self.text),
Arc::clone(&self.visual_style),
self.layout.clone(),
(self.max_width, 0),
)
};
aligned.realign(widths, align);
if let Some(mut spare) = spare
&& let Some(slot) = Rc::get_mut(&mut spare)
{
*slot = aligned;
return spare;
}
Rc::new(aligned)
}
pub(crate) fn realign(&mut self, (min_width, max_width): (f32, f32), align: TextAlign) {
self.layout.align(min_width, max_width, align);
self.max_width = max_width;
self.layout_handle = OnceCell::new();
}
pub(crate) fn try_realign(
&mut self,
(min_width, max_width): (f32, f32),
align: TextAlign,
) -> bool {
let held = self.layout.try_align(min_width, max_width, align);
if held {
self.max_width = max_width;
self.layout_handle = OnceCell::new();
}
held
}
pub fn render_text(&self) -> Arc<RenderString> {
if let Some((converted, _)) = self.render_text.get() {
return Arc::clone(converted);
}
self.convert_render_text().0
}
pub fn render_text_hash(&self) -> u64 {
if let Some((_, hash)) = self.render_text.get() {
return hash.get();
}
self.convert_render_text().1
}
fn convert_render_text(&self) -> (Arc<RenderString>, u64) {
let converted = Arc::new(self.text.render_string());
let hash = converted.render_hash();
let _ = self
.render_text
.set((Arc::clone(&converted), PackedU64::new(hash)));
(converted, hash)
}
pub fn visual_style_hash(&self) -> u64 {
if let Some(hash) = self.visual_style_hash.get() {
return hash.get();
}
let hash = self.visual_style.render_hash();
let _ = self.visual_style_hash.set(PackedU64::new(hash));
hash
}
pub fn result(&self) -> TextLayoutResult {
TextLayoutResult::new(self.layout.clone())
}
}
impl PartialEq for PreparedTextLayout {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
&& self.visual_style == other.visual_style
&& self.metrics() == other.metrics()
&& self.alignment_lines() == other.alignment_lines()
&& self.did_overflow() == other.did_overflow()
}
}
#[derive(Clone, Copy)]
enum LayoutSource<'a> {
Borrowed {
text: &'a AnnotatedString,
style: &'a TextStyle,
},
Shared {
text: &'a SharedText<'a>,
shaped: Option<&'a TextLayout>,
},
}
pub(crate) struct SharedText<'a> {
pub(crate) text: &'a Rc<AnnotatedString>,
pub(crate) style: &'a Arc<TextStyle>,
pub(crate) style_hash: u64,
}
impl LayoutSource<'_> {
fn text(&self) -> &AnnotatedString {
match self {
Self::Borrowed { text, .. } => text,
Self::Shared { text, .. } => text.text,
}
}
fn style(&self) -> &TextStyle {
match self {
Self::Borrowed { style, .. } => style,
Self::Shared { text, .. } => text.style,
}
}
fn shared(&self) -> (Rc<AnnotatedString>, Arc<TextStyle>) {
match self {
Self::Borrowed { text, style } => {
(Rc::new((*text).clone()), Arc::new((*style).clone()))
}
Self::Shared { text, .. } => (Rc::clone(text.text), Arc::clone(text.style)),
}
}
fn style_hash(&self) -> u64 {
match self {
Self::Borrowed { style, .. } => style.render_hash(),
Self::Shared { text, .. } => text.style_hash,
}
}
fn shaped(&self) -> Option<&TextLayout> {
match self {
Self::Borrowed { .. } => None,
Self::Shared { shaped, .. } => *shaped,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct TextFrame {
pub(crate) min_width: f32,
pub(crate) max_width: f32,
pub(crate) options: TextLayoutOptions,
}
impl TextFrame {
fn unbounded(options: TextLayoutOptions) -> Self {
Self {
min_width: 0.0,
max_width: f32::INFINITY,
options,
}
}
pub(crate) fn up_to(max_width: Option<f32>, options: TextLayoutOptions) -> Self {
Self {
max_width: max_width
.filter(|width| width.is_finite() && *width > 0.0)
.unwrap_or(f32::INFINITY),
..Self::unbounded(options)
}
}
}
pub(crate) struct TextService {
generation: Cell<u64>,
pass_owed: Cell<bool>,
fonts: RefCell<FontFamilyResolver>,
layouter: RefCell<TextLayouter>,
layouts: RefCell<LayoutCache>,
intrinsics: RefCell<PassAgedCache<TextKey, ParagraphIntrinsics>>,
}
impl TextService {
pub(crate) fn new() -> Self {
Self {
generation: Cell::new(1),
pass_owed: Cell::new(false),
fonts: RefCell::new(default_fonts()),
layouter: RefCell::new(TextLayouter::default()),
layouts: RefCell::new(LayoutCache::new()),
intrinsics: RefCell::new(PassAgedCache::with_capacity_at_least_one(
TEXT_INTRINSICS_CACHE_CAPACITY,
)),
}
}
pub(crate) fn set_fonts(&self, fonts: FontFamilyResolver) {
*self.fonts.borrow_mut() = fonts;
self.generation
.set(self.generation.get().wrapping_add(1).max(1));
self.layouts.borrow_mut().clear();
self.intrinsics.borrow_mut().clear();
}
pub(crate) fn generation(&self) -> u64 {
self.generation.get()
}
fn prepare(
&self,
source: LayoutSource<'_>,
frame: TextFrame,
(units, admission): (TextUnits, CacheAdmission),
) -> Rc<PreparedTextLayout> {
let (text, style) = (source.text(), source.style());
let key = TextKey::of(text, source.style_hash(), frame.options, units);
let cached = key.map(|key| {
let cached = self
.layouts
.borrow_mut()
.cached(key, frame, (style, admission));
(key, cached)
});
let missed = match cached {
Some((_, Cached::Held(prepared))) => return prepared,
Some((key, Cached::Missed(miss))) => Some((key, miss)),
None => None,
};
let (text, style) = source.shared();
let prepared = Rc::new(self.lay_out(text, style, frame, (units, source.shaped())));
if let Some((key, miss)) = missed {
self.layouts
.borrow_mut()
.keep(key, &prepared, (miss, admission));
}
prepared
}
fn lay_out(
&self,
text: Rc<AnnotatedString>,
style: Arc<TextStyle>,
frame: TextFrame,
(units, shaped): (TextUnits, Option<&TextLayout>),
) -> PreparedTextLayout {
let options = frame.options.normalized();
let min_lines = options.min_lines;
let Some(min_font_size_sp) = options.overflow.scale_down_min_font_size_sp() else {
let layout = self.layout(&text, &style, frame, (units, shaped));
return PreparedTextLayout::new(text, style, layout, (frame.max_width, min_lines));
};
self.lay_out_scaled_down(text, style, frame, (units, shaped), min_font_size_sp)
}
#[inline(never)]
fn lay_out_scaled_down(
&self,
text: Rc<AnnotatedString>,
style: Arc<TextStyle>,
frame: TextFrame,
(units, shaped): (TextUnits, Option<&TextLayout>),
min_font_size_sp: f32,
) -> PreparedTextLayout {
let options = frame.options.normalized();
let min_lines = options.min_lines;
let frame = TextFrame {
options: TextLayoutOptions {
overflow: TextOverflow::Clip,
..options
},
..frame
};
let fits =
|layout: &TextLayout| !layout.did_overflow_height() && !layout.did_overflow_width();
let full = self.layout(&text, &style, frame, (units, shaped));
let base = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
let min_factor = (min_font_size_sp.min(base) / base).clamp(0.0, 1.0);
if fits(&full) || !frame.max_width.is_finite() || min_factor >= 1.0 {
return PreparedTextLayout::new(text, style, full, (frame.max_width, min_lines));
}
let at = |factor: f32| {
let units = TextUnits {
factor: units.factor * factor,
..units
};
self.layout(&text, &style, frame, (units, Some(&full)))
};
let (mut best, mut factor) = (at(min_factor), min_factor);
if fits(&best) {
let (mut low, mut high) = (min_factor, 1.0);
for _ in 0..SCALE_DOWN_SEARCH_STEPS {
let mid = (low + high) * 0.5;
let candidate = at(mid);
if fits(&candidate) {
low = mid;
(best, factor) = (candidate, mid);
} else {
high = mid;
}
}
}
let (text, style) = scaled_drawing(&text, &style, factor);
PreparedTextLayout::new(text, style, best, (frame.max_width, min_lines))
}
fn layout(
&self,
text: &AnnotatedString,
style: &TextStyle,
frame: TextFrame,
(units, shaped): (TextUnits, Option<&TextLayout>),
) -> TextLayout {
let paragraph = ParagraphSpans::new(text, style, units);
let fonts = self.fonts.borrow();
let spans = paragraph.spans(&fonts);
let input = LayoutInput {
text: &text.text,
spans: &spans,
paragraph: &style.paragraph_style,
lengths: paragraph.lengths,
min_width: frame.min_width,
max_width: frame.max_width,
options: frame.options,
};
let mut layouter = self.layouter.borrow_mut();
match shaped {
Some(shaped) => layouter.relayout(&input, &fonts, shaped),
None => layouter.layout(&input, &fonts),
}
}
fn intrinsics(
&self,
text: &AnnotatedString,
style: &TextStyle,
units: TextUnits,
) -> ParagraphIntrinsics {
let key = TextKey::of(
text,
style.measurement_hash(),
TextLayoutOptions::default(),
units,
);
if let Some(key) = key
&& let Some(found) = self.intrinsics.borrow_mut().get(&key).copied()
{
return found;
}
let paragraph = ParagraphSpans::new(text, style, units);
let fonts = self.fonts.borrow();
let spans = paragraph.spans(&fonts);
let input = LayoutInput {
text: &text.text,
spans: &spans,
paragraph: &style.paragraph_style,
lengths: paragraph.lengths,
min_width: 0.0,
max_width: f32::INFINITY,
options: TextLayoutOptions::default(),
};
let found = self.layouter.borrow_mut().intrinsics(&input, &fonts);
if let Some(key) = key {
self.intrinsics.borrow_mut().push(key, found);
}
found
}
fn extent(&self, style: &TextStyle, units: TextUnits) -> Option<FontExtent> {
let span = &style.span_style;
let size = units
.density
.font_scale_curve()
.sp_to_dp(style.resolve_font_size(DEFAULT_FONT_SIZE_SP))
* units.factor;
let fonts = self.fonts.borrow();
let typeface = fonts.resolve(FontRequest {
family: span.font_family.as_ref().unwrap_or(&FontFamily::Default),
weight: span.font_weight.unwrap_or(FontWeight::NORMAL),
style: span.font_style.unwrap_or(FontStyle::Normal),
})?;
let scale = size / f32::from(typeface.units_per_em());
let metrics = typeface.line_metrics();
Some(FontExtent::new(
metrics.ascent * scale,
-metrics.descent * scale,
metrics.line_gap * scale,
))
}
fn line_box(&self, style: &TextStyle, units: TextUnits) -> Option<LineBox> {
let extent = self.extent(style, units)?;
let empty = AnnotatedString::from("");
let paragraph = ParagraphSpans::new(&empty, style, units);
let asked = paragraph.lengths.line_height.unwrap_or(f32::NAN);
Some(paragraph_line_box(
&style.paragraph_style,
extent,
asked,
units.density.density(),
))
}
pub(crate) fn begin_layout_pass(&self) {
self.pass_owed.set(false);
let headroom = crate::current_memory_headroom();
let idle_passes = match headroom {
MemoryHeadroom::Ample => IDLE_PASSES,
MemoryHeadroom::Tight => 1,
};
self.layouts.borrow_mut().begin_pass(idle_passes, headroom);
self.intrinsics.borrow_mut().begin_pass(idle_passes, drop);
self.layouter.borrow_mut().set_memory_headroom(headroom);
}
pub(crate) fn owe_pass(&self) {
self.pass_owed.set(true);
}
pub(crate) fn pay_owed_pass(&self) {
if self.pass_owed.get() {
self.begin_layout_pass();
}
}
}
fn default_fonts() -> FontFamilyResolver {
#[cfg(any(test, feature = "test-helpers"))]
{
super::test_fonts()
}
#[cfg(not(any(test, feature = "test-helpers")))]
{
FontFamilyResolver::new([])
}
}
fn scaled_drawing(
text: &Rc<AnnotatedString>,
style: &TextStyle,
factor: f32,
) -> (Rc<AnnotatedString>, Arc<TextStyle>) {
let mut scaled_style = style.clone();
scaled_style.span_style.font_size =
TextUnit::Sp(style.resolve_font_size(DEFAULT_FONT_SIZE_SP) * factor);
scale_span_drawing(&mut scaled_style.span_style, factor);
let text = if text.span_styles.is_empty() {
Rc::clone(text)
} else {
let mut scaled = AnnotatedString::clone(text);
for span in &mut scaled.span_styles {
if let TextUnit::Sp(size) = span.item.font_size {
span.item.font_size = TextUnit::Sp(size * factor);
}
scale_span_drawing(&mut span.item, factor);
}
Rc::new(scaled)
};
(text, Arc::new(scaled_style))
}
fn scale_span_drawing(span: &mut SpanStyle, factor: f32) {
if let TextUnit::Sp(spacing) = span.letter_spacing {
span.letter_spacing = TextUnit::Sp(spacing * factor);
}
if let Some(shadow) = &mut span.shadow {
shadow.offset.x *= factor;
shadow.offset.y *= factor;
shadow.blur_radius *= factor;
}
if let Some(TextDrawStyle::Stroke { width }) = &mut span.draw_style {
*width *= factor;
}
}
fn host_units() -> TextUnits {
TextUnits {
density: Density::from_host(),
factor: 1.0,
}
}
fn resolved_units() -> TextUnits {
TextUnits {
density: Density::with_curve(Density::from_host().density(), FontScaleCurve::linear(1.0)),
factor: 1.0,
}
}
fn prepare_borrowed(
text: &AnnotatedString,
style: &TextStyle,
frame: TextFrame,
units: TextUnits,
) -> Rc<PreparedTextLayout> {
crate::render_state::with_text_service(|service| {
service.prepare(
LayoutSource::Borrowed { text, style },
frame,
(units, CacheAdmission::First),
)
})
}
pub fn set_text_fonts(fonts: FontFamilyResolver) {
let Some(context) = crate::render_state::current_app_context() else {
panic!("set_text_fonts requires an active AppContext");
};
context.set_text_fonts(fonts);
}
pub fn measure_text(text: &AnnotatedString, style: &TextStyle) -> TextMetrics {
prepare_borrowed(
text,
style,
TextFrame::unbounded(TextLayoutOptions::default()),
host_units(),
)
.metrics()
}
pub(crate) fn prepare_resolved(text: &SharedText<'_>) -> Rc<PreparedTextLayout> {
crate::render_state::with_text_service(|service| {
service.prepare(
LayoutSource::Shared { text, shaped: None },
TextFrame::unbounded(TextLayoutOptions::default()),
(resolved_units(), CacheAdmission::Drawn),
)
})
}
pub fn measure_text_with_options(
text: &AnnotatedString,
style: &TextStyle,
options: TextLayoutOptions,
max_width: Option<f32>,
) -> TextMetrics {
prepare_borrowed(
text,
style,
TextFrame::up_to(max_width, options),
host_units(),
)
.metrics()
}
pub fn prepare_text_layout(
text: &AnnotatedString,
style: &TextStyle,
options: TextLayoutOptions,
max_width: Option<f32>,
) -> Rc<PreparedTextLayout> {
prepare_borrowed(
text,
style,
TextFrame::up_to(max_width, options),
host_units(),
)
}
pub(crate) fn prepare_shared(
text: &SharedText<'_>,
frame: TextFrame,
admission: CacheAdmission,
shaped: Option<&TextLayout>,
) -> Rc<PreparedTextLayout> {
crate::render_state::with_text_service(|service| {
service.prepare(
LayoutSource::Shared { text, shaped },
frame,
(host_units(), admission),
)
})
}
pub fn get_offset_for_position(text: &AnnotatedString, style: &TextStyle, x: f32, y: f32) -> usize {
prepare_borrowed(
text,
style,
TextFrame::unbounded(TextLayoutOptions::default()),
host_units(),
)
.layout
.offset_for_position(x, y)
}
pub fn offset_for_position_wrapped(
text: &str,
style: &TextStyle,
wrap_width: Option<f32>,
x: f32,
y: f32,
) -> usize {
prepare_borrowed(
&AnnotatedString::from(text),
style,
TextFrame::up_to(wrap_width, TextLayoutOptions::default()),
host_units(),
)
.layout
.offset_for_position(x, y)
}
pub fn get_cursor_x_for_offset(text: &AnnotatedString, style: &TextStyle, offset: usize) -> f32 {
prepare_borrowed(
text,
style,
TextFrame::unbounded(TextLayoutOptions::default()),
host_units(),
)
.layout
.horizontal_position(offset, CaretDirection::Primary)
}
pub fn layout_text(text: &AnnotatedString, style: &TextStyle) -> TextLayoutResult {
prepare_borrowed(
text,
style,
TextFrame::unbounded(TextLayoutOptions::default()),
host_units(),
)
.result()
}
pub fn wrapped_line_ranges(
text: &AnnotatedString,
style: &TextStyle,
options: TextLayoutOptions,
max_width: Option<f32>,
) -> Vec<Range<usize>> {
let prepared = prepare_borrowed(
text,
style,
TextFrame::up_to(max_width, options),
host_units(),
);
let layout = &prepared.layout;
(0..layout.line_count())
.map(|line| {
let (start, end) = (layout.line_start(line), layout.line_end(line));
let line_text = text.text.get(start..end).unwrap_or_default();
let without_break = line_text.trim_end_matches(['\n', '\r', '\u{2028}', '\u{2029}']);
start..start + without_break.len()
})
.collect()
}
pub(crate) fn min_intrinsic_text_width(text: &AnnotatedString, style: &TextStyle) -> f32 {
crate::render_state::with_text_service(|service| service.intrinsics(text, style, host_units()))
.min_intrinsic_width
}
pub fn text_line_box(style: &TextStyle) -> Option<LineBox> {
crate::render_state::with_text_service(|service| service.line_box(style, host_units()))
}
pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<LineBox> {
crate::render_state::current_app_context()?;
crate::render_state::with_text_service(|service| service.line_box(style, resolved_units()))
}
pub fn first_baseline(style: &TextStyle) -> Option<f32> {
text_line_box(style).map(LineBox::first_baseline)
}
pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
let units = host_units();
crate::render_state::with_text_service(|service| {
let extent = service.extent(style, units)?;
let line_box = service.line_box(style, units)?;
Some((
line_box.first_baseline() - extent.ascent,
extent.ascent + extent.descent,
))
})
.map_or((0.0, line_height), |(top, height)| {
(top.clamp(0.0, line_height), height.min(line_height))
})
}
#[cfg(test)]
#[path = "tests/measure_tests.rs"]
mod tests;