use std::ops::Range;
use std::rc::Rc;
use super::{EnvUpdateCtx, Link, TextStorage};
use crate::kurbo::{Line, Point, Rect, Size};
use crate::piet::{
Color, PietText, PietTextLayout, Text as _, TextAlignment, TextAttribute, TextLayout as _,
TextLayoutBuilder as _,
};
use crate::{Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext, UpdateCtx};
#[derive(Clone)]
pub struct TextLayout<T> {
text: Option<T>,
font: KeyOrValue<FontDescriptor>,
text_size_override: Option<KeyOrValue<f64>>,
text_color: KeyOrValue<Color>,
layout: Option<PietTextLayout>,
wrap_width: f64,
alignment: TextAlignment,
links: Rc<[(Rect, usize)]>,
text_is_rtl: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutMetrics {
pub size: Size,
pub first_baseline: f64,
pub trailing_whitespace_width: f64,
}
impl<T> TextLayout<T> {
pub fn new() -> Self {
TextLayout {
text: None,
font: crate::theme::UI_FONT.into(),
text_color: crate::theme::TEXT_COLOR.into(),
text_size_override: None,
layout: None,
wrap_width: f64::INFINITY,
alignment: Default::default(),
links: Rc::new([]),
text_is_rtl: false,
}
}
pub fn set_text_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
let color = color.into();
if color != self.text_color {
self.text_color = color;
self.layout = None;
}
}
pub fn set_font(&mut self, font: impl Into<KeyOrValue<FontDescriptor>>) {
let font = font.into();
if font != self.font {
self.font = font;
self.layout = None;
self.text_size_override = None;
}
}
pub fn set_text_size(&mut self, size: impl Into<KeyOrValue<f64>>) {
let size = size.into();
if Some(&size) != self.text_size_override.as_ref() {
self.text_size_override = Some(size);
self.layout = None;
}
}
pub fn set_wrap_width(&mut self, width: f64) {
let width = width.max(0.0);
if (width - self.wrap_width).abs() > 1e-4 {
self.wrap_width = width;
self.layout = None;
}
}
pub fn set_text_alignment(&mut self, alignment: TextAlignment) {
if self.alignment != alignment {
self.alignment = alignment;
self.layout = None;
}
}
pub fn text_is_rtl(&self) -> bool {
self.text_is_rtl
}
}
impl<T: TextStorage> TextLayout<T> {
pub fn from_text(text: impl Into<T>) -> Self {
let mut this = TextLayout::new();
this.set_text(text.into());
this
}
pub fn needs_rebuild(&self) -> bool {
self.layout.is_none()
}
pub fn set_text(&mut self, text: T) {
if self.text.is_none() || !self.text.as_ref().unwrap().same(&text) {
self.text_is_rtl = crate::piet::util::first_strong_rtl(text.as_str());
self.text = Some(text);
self.layout = None;
}
}
pub fn text(&self) -> Option<&T> {
self.text.as_ref()
}
pub fn layout(&self) -> Option<&PietTextLayout> {
self.layout.as_ref()
}
pub fn size(&self) -> Size {
self.layout
.as_ref()
.map(|layout| layout.size())
.unwrap_or_default()
}
pub fn layout_metrics(&self) -> LayoutMetrics {
debug_assert!(
self.layout.is_some(),
"TextLayout::layout_metrics called without rebuilding layout object. Text was '{}'",
self.text().as_ref().map(|s| s.as_str()).unwrap_or_default()
);
if let Some(layout) = self.layout.as_ref() {
let first_baseline = layout.line_metric(0).unwrap().baseline;
let size = layout.size();
LayoutMetrics {
size,
first_baseline,
trailing_whitespace_width: layout.trailing_whitespace_width(),
}
} else {
LayoutMetrics::default()
}
}
pub fn text_position_for_point(&self, point: Point) -> usize {
self.layout
.as_ref()
.map(|layout| layout.hit_test_point(point).idx)
.unwrap_or_default()
}
pub fn point_for_text_position(&self, text_pos: usize) -> Point {
self.layout
.as_ref()
.map(|layout| layout.hit_test_text_position(text_pos).point)
.unwrap_or_default()
}
pub fn rects_for_range(&self, range: Range<usize>) -> Vec<Rect> {
self.layout
.as_ref()
.map(|layout| layout.rects_for_range(range))
.unwrap_or_default()
}
pub fn underline_for_range(&self, range: Range<usize>) -> Line {
self.layout
.as_ref()
.map(|layout| {
let p1 = layout.hit_test_text_position(range.start);
let p2 = layout.hit_test_text_position(range.end);
let line_metric = layout.line_metric(p1.line).unwrap();
let y_pos = line_metric.baseline + (line_metric.height / 5.0);
Line::new((p1.point.x, y_pos), (p2.point.x, y_pos))
})
.unwrap_or_else(|| Line::new(Point::ZERO, Point::ZERO))
}
pub fn cursor_line_for_text_position(&self, text_pos: usize) -> Line {
self.layout
.as_ref()
.map(|layout| {
let pos = layout.hit_test_text_position(text_pos);
let line_metrics = layout.line_metric(pos.line).unwrap();
let p1 = (pos.point.x, line_metrics.y_offset);
let p2 = (pos.point.x, (line_metrics.y_offset + line_metrics.height));
Line::new(p1, p2)
})
.unwrap_or_else(|| Line::new(Point::ZERO, Point::ZERO))
}
pub fn link_for_pos(&self, pos: Point) -> Option<&Link> {
let (_, i) = self
.links
.iter()
.rfind(|(hit_box, _)| hit_box.contains(pos))?;
let text = self.text()?;
text.links().get(*i)
}
pub fn needs_rebuild_after_update(&mut self, ctx: &mut UpdateCtx) -> bool {
if ctx.env_changed() && self.layout.is_some() {
let rebuild = ctx.env_key_changed(&self.font)
|| ctx.env_key_changed(&self.text_color)
|| self
.text_size_override
.as_ref()
.map(|k| ctx.env_key_changed(k))
.unwrap_or(false)
|| self
.text
.as_ref()
.map(|text| text.env_update(&EnvUpdateCtx::for_update(ctx)))
.unwrap_or(false);
if rebuild {
self.layout = None;
}
}
self.layout.is_none()
}
pub fn rebuild_if_needed(&mut self, factory: &mut PietText, env: &Env) {
if let Some(text) = &self.text {
if self.layout.is_none() {
let font = self.font.resolve(env);
let color = self.text_color.resolve(env);
let size_override = self.text_size_override.as_ref().map(|key| key.resolve(env));
let descriptor = if let Some(size) = size_override {
font.with_size(size)
} else {
font
};
let builder = factory
.new_text_layout(text.clone())
.max_width(self.wrap_width)
.alignment(self.alignment)
.font(descriptor.family.clone(), descriptor.size)
.default_attribute(descriptor.weight)
.default_attribute(descriptor.style)
.default_attribute(TextAttribute::TextColor(color));
let layout = text.add_attributes(builder, env).build().unwrap();
self.links = text
.links()
.iter()
.enumerate()
.flat_map(|(i, link)| {
layout
.rects_for_range(link.range())
.into_iter()
.map(move |rect| (rect, i))
})
.collect();
self.layout = Some(layout);
}
}
}
pub fn draw(&self, ctx: &mut PaintCtx, point: impl Into<Point>) {
debug_assert!(
self.layout.is_some(),
"TextLayout::draw called without rebuilding layout object. Text was '{}'",
self.text
.as_ref()
.map(|t| t.as_str())
.unwrap_or("layout is missing text")
);
if let Some(layout) = self.layout.as_ref() {
ctx.draw_text(layout, point);
}
}
}
impl<T> std::fmt::Debug for TextLayout<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("TextLayout")
.field("font", &self.font)
.field("text_size_override", &self.text_size_override)
.field("text_color", &self.text_color)
.field(
"layout",
if self.layout.is_some() {
&"Some"
} else {
&"None"
},
)
.finish()
}
}
impl<T: TextStorage> Default for TextLayout<T> {
fn default() -> Self {
Self::new()
}
}