// Copyright 2020 The xi-editor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A type for laying out, drawing, and interacting with text.
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};
/// A component for displaying text on screen.
///
/// This is a type intended to be used by other widgets that display text.
/// It allows for the text itself as well as font and other styling information
/// to be set and modified. It wraps an inner layout object, and handles
/// invalidating and rebuilding it as required.
///
/// This object is not valid until the [`rebuild_if_needed`] method has been
/// called. You should generally do this in your widget's [`layout`] method.
/// Additionally, you should call [`needs_rebuild_after_update`]
/// as part of your widget's [`update`] method; if this returns `true`, you will need
/// to call [`rebuild_if_needed`] again, generally by scheduling another [`layout`]
/// pass.
///
/// [`layout`]: crate::Widget::layout
/// [`update`]: crate::Widget::update
/// [`needs_rebuild_after_update`]: #method.needs_rebuild_after_update
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
#[derive(Clone)]
pub struct TextLayout<T> {
text: Option<T>,
font: KeyOrValue<FontDescriptor>,
// when set, this will be used to override the size in he font descriptor.
// This provides an easy way to change only the font size, while still
// using a `FontDescriptor` in the `Env`.
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,
}
/// Metrics describing the layout text.
#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutMetrics {
/// The nominal size of the layout.
pub size: Size,
/// The distance from the nominal top of the layout to the first baseline.
pub first_baseline: f64,
/// The width of the layout, inclusive of trailing whitespace.
pub trailing_whitespace_width: f64,
//TODO: add inking_rect
}
impl<T> TextLayout<T> {
/// Create a new `TextLayout` object.
///
/// You must set the text ([`set_text`]) before using this object.
///
/// [`set_text`]: TextLayout::set_text
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,
}
}
/// Set the default text color for this layout.
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;
}
}
/// Set the default font.
///
/// The argument is a [`FontDescriptor`] or a [`Key<FontDescriptor>`] that
/// can be resolved from the [`Env`].
///
/// [`Key<FontDescriptor>`]: crate::Key
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;
}
}
/// Set the font size.
///
/// This overrides the size in the [`FontDescriptor`] provided to [`set_font`].
///
/// [`set_font`]: TextLayout::set_font
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;
}
}
/// Set the width at which to wrap words.
///
/// You may pass `f64::INFINITY` to disable word wrapping
/// (the default behaviour).
pub fn set_wrap_width(&mut self, width: f64) {
let width = width.max(0.0);
// 1e-4 is an arbitrary small-enough value that we don't care to rewrap
if (width - self.wrap_width).abs() > 1e-4 {
self.wrap_width = width;
self.layout = None;
}
}
/// Set the [`TextAlignment`] for this layout.
pub fn set_text_alignment(&mut self, alignment: TextAlignment) {
if self.alignment != alignment {
self.alignment = alignment;
self.layout = None;
}
}
/// Returns `true` if this layout's text appears to be right-to-left.
///
/// See [`piet::util::first_strong_rtl`] for more information.
///
/// [`piet::util::first_strong_rtl`]: crate::piet::util::first_strong_rtl
pub fn text_is_rtl(&self) -> bool {
self.text_is_rtl
}
}
impl<T: TextStorage> TextLayout<T> {
/// Create a new `TextLayout` with the provided text.
///
/// This is useful when the text is not tied to application data.
pub fn from_text(text: impl Into<T>) -> Self {
let mut this = TextLayout::new();
this.set_text(text.into());
this
}
/// Returns `true` if this layout needs to be rebuilt.
///
/// This happens (for instance) after style attributes are modified.
///
/// This does not account for things like the text changing, handling that
/// is the responsibility of the user.
pub fn needs_rebuild(&self) -> bool {
self.layout.is_none()
}
/// Set the text to display.
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;
}
}
/// Returns the [`TextStorage`] backing this layout, if it exists.
pub fn text(&self) -> Option<&T> {
self.text.as_ref()
}
/// Returns the inner Piet [`TextLayout`] type.
///
/// [`TextLayout`]: crate::piet::TextLayout
pub fn layout(&self) -> Option<&PietTextLayout> {
self.layout.as_ref()
}
/// The size of the laid-out text.
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
///
/// [`rebuild_if_needed`]: TextLayout::rebuild_if_needed
pub fn size(&self) -> Size {
self.layout
.as_ref()
.map(|layout| layout.size())
.unwrap_or_default()
}
/// Return the text's [`LayoutMetrics`].
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
///
/// [`rebuild_if_needed`]: TextLayout::rebuild_if_needed
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()
}
}
/// For a given `Point` (relative to this object's origin), returns index
/// into the underlying text of the nearest grapheme boundary.
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()
}
/// Given the utf-8 position of a character boundary in the underlying text,
/// return the `Point` (relative to this object's origin) representing the
/// boundary of the containing grapheme.
///
/// # Panics
///
/// Panics if `text_pos` is not a character boundary.
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()
}
/// Given a utf-8 range in the underlying text, return a `Vec` of `Rect`s
/// representing the nominal bounding boxes of the text in that range.
///
/// # Panics
///
/// Panics if the range start or end is not a character boundary.
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()
}
/// Return a line suitable for underlining a range of text.
///
/// This is really only intended to be used to indicate the composition
/// range while IME is active.
///
/// range is expected to be on a single visual line.
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();
// heuristic; 1/5 of height is a rough guess at the descender pos?
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))
}
/// Given the utf-8 position of a character boundary in the underlying text,
/// return a `Line` suitable for drawing a vertical cursor at that boundary.
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))
}
/// Returns the [`Link`] at the provided point (relative to the layout's origin) if one exists.
///
/// This can be used both for hit-testing (deciding whether to change the mouse cursor,
/// or performing some other action when hovering) as well as for retrieving a [`Link`]
/// on click.
///
/// [`Link`]: super::attribute::Link
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)
}
/// Called during the containing widget's [`update`] method; this text object
/// will check to see if any used environment items have changed,
/// and invalidate itself as needed.
///
/// Returns `true` if the text item needs to be rebuilt.
///
/// [`update`]: crate::Widget::update
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()
}
/// Rebuild the inner layout as needed.
///
/// This `TextLayout` object manages a lower-level layout object that may
/// need to be rebuilt in response to changes to the text or attributes
/// like the font.
///
/// This method should be called whenever any of these things may have changed.
/// A simple way to ensure this is correct is to always call this method
/// as part of your widget's [`layout`] method.
///
/// [`layout`]: crate::Widget::layout
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);
}
}
}
/// Draw the layout at the provided `Point`.
///
/// The origin of the layout is the top-left corner.
///
/// You must call [`rebuild_if_needed`] at some point before you first
/// call this method.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
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()
}
}