Decouple caching from Paragraph API

This commit is contained in:
Héctor Ramón Jiménez 2024-07-17 18:47:58 +02:00
parent 616689ca54
commit ffb520fb37
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
7 changed files with 128 additions and 95 deletions

View file

@ -79,7 +79,7 @@ impl text::Paragraph for () {
fn resize(&mut self, _new_bounds: Size) {} fn resize(&mut self, _new_bounds: Size) {}
fn compare(&self, _text: Text<&str>) -> text::Difference { fn compare(&self, _text: Text<()>) -> text::Difference {
text::Difference::None text::Difference::None
} }

View file

@ -1,8 +1,7 @@
//! Draw and interact with text. //! Draw and interact with text.
mod paragraph;
pub mod editor; pub mod editor;
pub mod highlighter; pub mod highlighter;
pub mod paragraph;
pub use editor::Editor; pub use editor::Editor;
pub use highlighter::Highlighter; pub use highlighter::Highlighter;

View file

@ -1,3 +1,4 @@
//! Draw paragraphs.
use crate::alignment; use crate::alignment;
use crate::text::{Difference, Hit, Text}; use crate::text::{Difference, Hit, Text};
use crate::{Point, Size}; use crate::{Point, Size};
@ -15,7 +16,7 @@ pub trait Paragraph: Sized + Default {
/// Compares the [`Paragraph`] with some desired [`Text`] and returns the /// Compares the [`Paragraph`] with some desired [`Text`] and returns the
/// [`Difference`]. /// [`Difference`].
fn compare(&self, text: Text<&str, Self::Font>) -> Difference; fn compare(&self, text: Text<(), Self::Font>) -> Difference;
/// Returns the horizontal alignment of the [`Paragraph`]. /// Returns the horizontal alignment of the [`Paragraph`].
fn horizontal_alignment(&self) -> alignment::Horizontal; fn horizontal_alignment(&self) -> alignment::Horizontal;
@ -34,19 +35,6 @@ pub trait Paragraph: Sized + Default {
/// Returns the distance to the given grapheme index in the [`Paragraph`]. /// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
/// Updates the [`Paragraph`] to match the given [`Text`], if needed.
fn update(&mut self, text: Text<&str, Self::Font>) {
match self.compare(text) {
Difference::None => {}
Difference::Bounds => {
self.resize(text.bounds);
}
Difference::Shape => {
*self = Self::with_text(text);
}
}
}
/// Returns the minimum width that can fit the contents of the [`Paragraph`]. /// Returns the minimum width that can fit the contents of the [`Paragraph`].
fn min_width(&self) -> f32 { fn min_width(&self) -> f32 {
self.min_bounds().width self.min_bounds().width
@ -57,3 +45,77 @@ pub trait Paragraph: Sized + Default {
self.min_bounds().height self.min_bounds().height
} }
} }
/// A [`Paragraph`] of plain text.
#[derive(Debug, Clone, Default)]
pub struct Plain<P: Paragraph> {
raw: P,
content: String,
}
impl<P: Paragraph> Plain<P> {
/// Creates a new [`Plain`] paragraph.
pub fn new(text: Text<&str, P::Font>) -> Self {
let content = text.content.to_owned();
Self {
raw: P::with_text(text),
content,
}
}
/// Updates the plain [`Paragraph`] to match the given [`Text`], if needed.
pub fn update(&mut self, text: Text<&str, P::Font>) {
if self.content != text.content {
text.content.clone_into(&mut self.content);
self.raw = P::with_text(text);
return;
}
match self.raw.compare(Text {
content: (),
bounds: text.bounds,
size: text.size,
line_height: text.line_height,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
}) {
Difference::None => {}
Difference::Bounds => {
self.raw.resize(text.bounds);
}
Difference::Shape => {
self.raw = P::with_text(text);
}
}
}
/// Returns the horizontal alignment of the [`Paragraph`].
pub fn horizontal_alignment(&self) -> alignment::Horizontal {
self.raw.horizontal_alignment()
}
/// Returns the vertical alignment of the [`Paragraph`].
pub fn vertical_alignment(&self) -> alignment::Vertical {
self.raw.vertical_alignment()
}
/// Returns the minimum boundaries that can fit the contents of the
/// [`Paragraph`].
pub fn min_bounds(&self) -> Size {
self.raw.min_bounds()
}
/// Returns the minimum width that can fit the contents of the
/// [`Paragraph`].
pub fn min_width(&self) -> f32 {
self.raw.min_width()
}
/// Returns the cached [`Paragraph`].
pub fn raw(&self) -> &P {
&self.raw
}
}

View file

@ -3,7 +3,8 @@ use crate::alignment;
use crate::layout; use crate::layout;
use crate::mouse; use crate::mouse;
use crate::renderer; use crate::renderer;
use crate::text::{self, Paragraph}; use crate::text;
use crate::text::paragraph::{self, Paragraph};
use crate::widget::tree::{self, Tree}; use crate::widget::tree::{self, Tree};
use crate::{ use crate::{
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme, Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme,
@ -155,7 +156,7 @@ where
/// The internal state of a [`Text`] widget. /// The internal state of a [`Text`] widget.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct State<P: Paragraph>(P); pub struct State<P: Paragraph>(paragraph::Plain<P>);
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Text<'a, Theme, Renderer> for Text<'a, Theme, Renderer>
@ -168,7 +169,9 @@ where
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State(Renderer::Paragraph::default())) tree::State::new(State::<Renderer::Paragraph>(
paragraph::Plain::default(),
))
} }
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
@ -294,7 +297,7 @@ pub fn draw<Renderer>(
}; };
renderer.fill_paragraph( renderer.fill_paragraph(
paragraph, paragraph.raw(),
Point::new(x, y), Point::new(x, y),
appearance.color.unwrap_or(style.text_color), appearance.color.unwrap_or(style.text_color),
*viewport, *viewport,

View file

@ -1,8 +1,8 @@
//! Draw paragraphs. //! Draw paragraphs.
use crate::core; use crate::core;
use crate::core::alignment; use crate::core::alignment;
use crate::core::text::{Hit, LineHeight, Shaping, Text}; use crate::core::text::{Hit, Shaping, Text};
use crate::core::{Font, Pixels, Point, Size}; use crate::core::{Font, Point, Size};
use crate::text; use crate::text;
use std::fmt; use std::fmt;
@ -10,11 +10,11 @@ use std::sync::{self, Arc};
/// A bunch of text. /// A bunch of text.
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct Paragraph(Option<Arc<Internal>>); pub struct Paragraph(Arc<Internal>);
#[derive(Clone)]
struct Internal { struct Internal {
buffer: cosmic_text::Buffer, buffer: cosmic_text::Buffer,
content: String, // TODO: Reuse from `buffer` (?)
font: Font, font: Font,
shaping: Shaping, shaping: Shaping,
horizontal_alignment: alignment::Horizontal, horizontal_alignment: alignment::Horizontal,
@ -52,9 +52,7 @@ impl Paragraph {
} }
fn internal(&self) -> &Arc<Internal> { fn internal(&self) -> &Arc<Internal> {
self.0 &self.0
.as_ref()
.expect("paragraph should always be initialized")
} }
} }
@ -90,9 +88,8 @@ impl core::text::Paragraph for Paragraph {
let min_bounds = text::measure(&buffer); let min_bounds = text::measure(&buffer);
Self(Some(Arc::new(Internal { Self(Arc::new(Internal {
buffer, buffer,
content: text.content.to_owned(),
font: text.font, font: text.font,
horizontal_alignment: text.horizontal_alignment, horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment, vertical_alignment: text.vertical_alignment,
@ -100,59 +97,31 @@ impl core::text::Paragraph for Paragraph {
bounds: text.bounds, bounds: text.bounds,
min_bounds, min_bounds,
version: font_system.version(), version: font_system.version(),
}))) }))
} }
fn resize(&mut self, new_bounds: Size) { fn resize(&mut self, new_bounds: Size) {
let paragraph = self let paragraph = Arc::make_mut(&mut self.0);
.0
.take()
.expect("paragraph should always be initialized");
match Arc::try_unwrap(paragraph) { let mut font_system =
Ok(mut internal) => { text::font_system().write().expect("Write font system");
let mut font_system =
text::font_system().write().expect("Write font system");
internal.buffer.set_size( paragraph.buffer.set_size(
font_system.raw(), font_system.raw(),
Some(new_bounds.width), Some(new_bounds.width),
Some(new_bounds.height), Some(new_bounds.height),
); );
internal.bounds = new_bounds; paragraph.bounds = new_bounds;
internal.min_bounds = text::measure(&internal.buffer); paragraph.min_bounds = text::measure(&paragraph.buffer);
self.0 = Some(Arc::new(internal));
}
Err(internal) => {
let metrics = internal.buffer.metrics();
// If there is a strong reference somewhere, we recompute the
// buffer from scratch
*self = Self::with_text(Text {
content: &internal.content,
bounds: internal.bounds,
size: Pixels(metrics.font_size),
line_height: LineHeight::Absolute(Pixels(
metrics.line_height,
)),
font: internal.font,
horizontal_alignment: internal.horizontal_alignment,
vertical_alignment: internal.vertical_alignment,
shaping: internal.shaping,
});
}
}
} }
fn compare(&self, text: Text<&str>) -> core::text::Difference { fn compare(&self, text: Text<()>) -> core::text::Difference {
let font_system = text::font_system().read().expect("Read font system"); let font_system = text::font_system().read().expect("Read font system");
let paragraph = self.internal(); let paragraph = self.internal();
let metrics = paragraph.buffer.metrics(); let metrics = paragraph.buffer.metrics();
if paragraph.version != font_system.version if paragraph.version != font_system.version
|| paragraph.content != text.content
|| metrics.font_size != text.size.0 || metrics.font_size != text.size.0
|| metrics.line_height != text.line_height.to_absolute(text.size).0 || metrics.line_height != text.line_height.to_absolute(text.size).0
|| paragraph.font != text.font || paragraph.font != text.font
@ -231,7 +200,7 @@ impl core::text::Paragraph for Paragraph {
impl Default for Paragraph { impl Default for Paragraph {
fn default() -> Self { fn default() -> Self {
Self(Some(Arc::new(Internal::default()))) Self(Arc::new(Internal::default()))
} }
} }
@ -240,7 +209,6 @@ impl fmt::Debug for Paragraph {
let paragraph = self.internal(); let paragraph = self.internal();
f.debug_struct("Paragraph") f.debug_struct("Paragraph")
.field("content", &paragraph.content)
.field("font", &paragraph.font) .field("font", &paragraph.font)
.field("shaping", &paragraph.shaping) .field("shaping", &paragraph.shaping)
.field("horizontal_alignment", &paragraph.horizontal_alignment) .field("horizontal_alignment", &paragraph.horizontal_alignment)
@ -253,8 +221,7 @@ impl fmt::Debug for Paragraph {
impl PartialEq for Internal { impl PartialEq for Internal {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.content == other.content self.font == other.font
&& self.font == other.font
&& self.shaping == other.shaping && self.shaping == other.shaping
&& self.horizontal_alignment == other.horizontal_alignment && self.horizontal_alignment == other.horizontal_alignment
&& self.vertical_alignment == other.vertical_alignment && self.vertical_alignment == other.vertical_alignment
@ -271,7 +238,6 @@ impl Default for Internal {
font_size: 1.0, font_size: 1.0,
line_height: 1.0, line_height: 1.0,
}), }),
content: String::new(),
font: Font::default(), font: Font::default(),
shaping: Shaping::default(), shaping: Shaping::default(),
horizontal_alignment: alignment::Horizontal::Left, horizontal_alignment: alignment::Horizontal::Left,
@ -298,7 +264,7 @@ pub struct Weak {
impl Weak { impl Weak {
/// Tries to update the reference into a [`Paragraph`]. /// Tries to update the reference into a [`Paragraph`].
pub fn upgrade(&self) -> Option<Paragraph> { pub fn upgrade(&self) -> Option<Paragraph> {
self.raw.upgrade().map(Some).map(Paragraph) self.raw.upgrade().map(Paragraph)
} }
} }

View file

@ -6,7 +6,8 @@ use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::overlay; use crate::core::overlay;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text::{self, Paragraph as _, Text}; use crate::core::text::paragraph;
use crate::core::text::{self, Text};
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
@ -622,8 +623,8 @@ struct State<P: text::Paragraph> {
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
is_open: bool, is_open: bool,
hovered_option: Option<usize>, hovered_option: Option<usize>,
options: Vec<P>, options: Vec<paragraph::Plain<P>>,
placeholder: P, placeholder: paragraph::Plain<P>,
} }
impl<P: text::Paragraph> State<P> { impl<P: text::Paragraph> State<P> {
@ -635,7 +636,7 @@ impl<P: text::Paragraph> State<P> {
is_open: bool::default(), is_open: bool::default(),
hovered_option: Option::default(), hovered_option: Option::default(),
options: Vec::new(), options: Vec::new(),
placeholder: P::default(), placeholder: paragraph::Plain::default(),
} }
} }
} }

View file

@ -19,7 +19,8 @@ use crate::core::keyboard::key;
use crate::core::layout; use crate::core::layout;
use crate::core::mouse::{self, click}; use crate::core::mouse::{self, click};
use crate::core::renderer; use crate::core::renderer;
use crate::core::text::{self, Paragraph as _, Text}; use crate::core::text::paragraph;
use crate::core::text::{self, Text};
use crate::core::time::{Duration, Instant}; use crate::core::time::{Duration, Instant};
use crate::core::touch; use crate::core::touch;
use crate::core::widget; use crate::core::widget;
@ -360,7 +361,7 @@ where
let icon_layout = children_layout.next().unwrap(); let icon_layout = children_layout.next().unwrap();
renderer.fill_paragraph( renderer.fill_paragraph(
&state.icon, state.icon.raw(),
icon_layout.bounds().center(), icon_layout.bounds().center(),
style.icon, style.icon,
*viewport, *viewport,
@ -378,7 +379,7 @@ where
cursor::State::Index(position) => { cursor::State::Index(position) => {
let (text_value_width, offset) = let (text_value_width, offset) =
measure_cursor_and_scroll_offset( measure_cursor_and_scroll_offset(
&state.value, state.value.raw(),
text_bounds, text_bounds,
position, position,
); );
@ -415,14 +416,14 @@ where
let (left_position, left_offset) = let (left_position, left_offset) =
measure_cursor_and_scroll_offset( measure_cursor_and_scroll_offset(
&state.value, state.value.raw(),
text_bounds, text_bounds,
left, left,
); );
let (right_position, right_offset) = let (right_position, right_offset) =
measure_cursor_and_scroll_offset( measure_cursor_and_scroll_offset(
&state.value, state.value.raw(),
text_bounds, text_bounds,
right, right,
); );
@ -469,9 +470,9 @@ where
renderer.fill_paragraph( renderer.fill_paragraph(
if text.is_empty() { if text.is_empty() {
&state.placeholder state.placeholder.raw()
} else { } else {
&state.value state.value.raw()
}, },
Point::new(text_bounds.x, text_bounds.center_y()) Point::new(text_bounds.x, text_bounds.center_y())
- Vector::new(offset, 0.0), - Vector::new(offset, 0.0),
@ -1178,9 +1179,9 @@ pub fn select_all<T>(id: Id) -> Task<T> {
/// The state of a [`TextInput`]. /// The state of a [`TextInput`].
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct State<P: text::Paragraph> { pub struct State<P: text::Paragraph> {
value: P, value: paragraph::Plain<P>,
placeholder: P, placeholder: paragraph::Plain<P>,
icon: P, icon: paragraph::Plain<P>,
is_focused: Option<Focus>, is_focused: Option<Focus>,
is_dragging: bool, is_dragging: bool,
is_pasting: Option<Value>, is_pasting: Option<Value>,
@ -1212,9 +1213,9 @@ impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`], representing a focused [`TextInput`]. /// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused() -> Self { pub fn focused() -> Self {
Self { Self {
value: P::default(), value: paragraph::Plain::default(),
placeholder: P::default(), placeholder: paragraph::Plain::default(),
icon: P::default(), icon: paragraph::Plain::default(),
is_focused: None, is_focused: None,
is_dragging: false, is_dragging: false,
is_pasting: None, is_pasting: None,
@ -1319,7 +1320,7 @@ fn offset<P: text::Paragraph>(
}; };
let (_, offset) = measure_cursor_and_scroll_offset( let (_, offset) = measure_cursor_and_scroll_offset(
&state.value, state.value.raw(),
text_bounds, text_bounds,
focus_position, focus_position,
); );
@ -1357,6 +1358,7 @@ fn find_cursor_position<P: text::Paragraph>(
let char_offset = state let char_offset = state
.value .value
.raw()
.hit_test(Point::new(x + offset, text_bounds.height / 2.0)) .hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?; .map(text::Hit::cursor)?;
@ -1386,7 +1388,7 @@ fn replace_paragraph<Renderer>(
let mut children_layout = layout.children(); let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds(); let text_bounds = children_layout.next().unwrap().bounds();
state.value = Renderer::Paragraph::with_text(Text { state.value = paragraph::Plain::new(Text {
font, font,
line_height, line_height,
content: &value.to_string(), content: &value.to_string(),