summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-08-30 04:31:21 +0200
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-08-30 04:31:21 +0200
commited3454301e663a7cb7d73cd56b57b188f4d14a2f (patch)
tree8118d1305c2eba3a1b45d04634cd0e8d050fc0fa /core
parentc9bd48704dd9679c033dd0b8588e2744a3df44a0 (diff)
downloadiced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.tar.gz
iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.tar.bz2
iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.zip
Implement explicit text caching in the widget state tree
Diffstat (limited to 'core')
-rw-r--r--core/src/element.rs6
-rw-r--r--core/src/layout.rs28
-rw-r--r--core/src/layout/flex.rs12
-rw-r--r--core/src/renderer.rs15
-rw-r--r--core/src/renderer/null.rs96
-rw-r--r--core/src/text.rs161
-rw-r--r--core/src/widget.rs3
-rw-r--r--core/src/widget/text.rs136
8 files changed, 305 insertions, 152 deletions
diff --git a/core/src/element.rs b/core/src/element.rs
index d2c6358b..0d23a9e7 100644
--- a/core/src/element.rs
+++ b/core/src/element.rs
@@ -306,10 +306,11 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.widget.layout(renderer, limits)
+ self.widget.layout(tree, renderer, limits)
}
fn operate(
@@ -491,10 +492,11 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.element.widget.layout(renderer, limits)
+ self.element.widget.layout(tree, renderer, limits)
}
fn operate(
diff --git a/core/src/layout.rs b/core/src/layout.rs
index 04954fb9..50ccf1f4 100644
--- a/core/src/layout.rs
+++ b/core/src/layout.rs
@@ -7,7 +7,7 @@ pub mod flex;
pub use limits::Limits;
pub use node::Node;
-use crate::{Point, Rectangle, Vector};
+use crate::{Point, Rectangle, Size, Vector};
/// The bounds of a [`Node`] and its children, using absolute coordinates.
#[derive(Debug, Clone, Copy)]
@@ -63,3 +63,29 @@ impl<'a> Layout<'a> {
})
}
}
+
+/// Produces a [`Node`] with two children nodes one right next to each other.
+pub fn next_to_each_other(
+ limits: &Limits,
+ spacing: f32,
+ left: impl FnOnce(&Limits) -> Node,
+ right: impl FnOnce(&Limits) -> Node,
+) -> Node {
+ let left_node = left(limits);
+ let left_size = left_node.size();
+
+ let right_limits = limits.shrink(Size::new(left_size.width + spacing, 0.0));
+
+ let mut right_node = right(&right_limits);
+ let right_size = right_node.size();
+
+ right_node.move_to(Point::new(left_size.width + spacing, 0.0));
+
+ Node::with_children(
+ Size::new(
+ left_size.width + spacing + right_size.width,
+ left_size.height.max(right_size.height),
+ ),
+ vec![left_node, right_node],
+ )
+}
diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs
index 8b967849..86b1a45b 100644
--- a/core/src/layout/flex.rs
+++ b/core/src/layout/flex.rs
@@ -19,6 +19,7 @@
use crate::Element;
use crate::layout::{Limits, Node};
+use crate::widget;
use crate::{Alignment, Padding, Point, Size};
/// The main axis of a flex layout.
@@ -66,6 +67,7 @@ pub fn resolve<Message, Renderer>(
spacing: f32,
align_items: Alignment,
items: &[Element<'_, Message, Renderer>],
+ trees: &[widget::Tree],
) -> Node
where
Renderer: crate::Renderer,
@@ -81,7 +83,7 @@ where
let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
nodes.resize(items.len(), Node::default());
- for (i, child) in items.iter().enumerate() {
+ for (i, (child, tree)) in items.iter().zip(trees).enumerate() {
let fill_factor = match axis {
Axis::Horizontal => child.as_widget().width(),
Axis::Vertical => child.as_widget().height(),
@@ -94,7 +96,8 @@ where
let child_limits =
Limits::new(Size::ZERO, Size::new(max_width, max_height));
- let layout = child.as_widget().layout(renderer, &child_limits);
+ let layout =
+ child.as_widget().layout(tree, renderer, &child_limits);
let size = layout.size();
available -= axis.main(size);
@@ -108,7 +111,7 @@ where
let remaining = available.max(0.0);
- for (i, child) in items.iter().enumerate() {
+ for (i, (child, tree)) in items.iter().zip(trees).enumerate() {
let fill_factor = match axis {
Axis::Horizontal => child.as_widget().width(),
Axis::Vertical => child.as_widget().height(),
@@ -133,7 +136,8 @@ where
Size::new(max_width, max_height),
);
- let layout = child.as_widget().layout(renderer, &child_limits);
+ let layout =
+ child.as_widget().layout(tree, renderer, &child_limits);
cross = cross.max(axis.cross(layout.size()));
nodes[i] = layout;
diff --git a/core/src/renderer.rs b/core/src/renderer.rs
index 7c73d2e4..1b327e56 100644
--- a/core/src/renderer.rs
+++ b/core/src/renderer.rs
@@ -5,26 +5,13 @@ mod null;
#[cfg(debug_assertions)]
pub use null::Null;
-use crate::layout;
-use crate::{Background, BorderRadius, Color, Element, Rectangle, Vector};
+use crate::{Background, BorderRadius, Color, Rectangle, Vector};
/// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer: Sized {
/// The supported theme of the [`Renderer`].
type Theme;
- /// Lays out the elements of a user interface.
- ///
- /// You should override this if you need to perform any operations before or
- /// after layouting. For instance, trimming the measurements cache.
- fn layout<Message>(
- &mut self,
- element: &Element<'_, Message, Self>,
- limits: &layout::Limits,
- ) -> layout::Node {
- element.as_widget().layout(self, limits)
- }
-
/// Draws the primitives recorded in the given closure in a new layer.
///
/// The layer will clip its contents to the provided `bounds`.
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs
index 5d49699e..55d58a59 100644
--- a/core/src/renderer/null.rs
+++ b/core/src/renderer/null.rs
@@ -1,6 +1,7 @@
+use crate::alignment;
use crate::renderer::{self, Renderer};
use crate::text::{self, Text};
-use crate::{Background, Font, Point, Rectangle, Size, Vector};
+use crate::{Background, Color, Font, Pixels, Point, Rectangle, Size, Vector};
use std::borrow::Cow;
@@ -41,6 +42,7 @@ impl Renderer for Null {
impl text::Renderer for Null {
type Font = Font;
+ type Paragraph = ();
const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0';
@@ -50,37 +52,83 @@ impl text::Renderer for Null {
Font::default()
}
- fn default_size(&self) -> f32 {
- 16.0
+ fn default_size(&self) -> Pixels {
+ Pixels(16.0)
}
fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
- fn measure(
- &self,
- _content: &str,
- _size: f32,
- _line_height: text::LineHeight,
- _font: Font,
- _bounds: Size,
- _shaping: text::Shaping,
- ) -> Size {
- Size::new(0.0, 20.0)
+ fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph {
}
- fn hit_test(
+ fn resize_paragraph(
&self,
- _contents: &str,
- _size: f32,
- _line_height: text::LineHeight,
- _font: Self::Font,
- _bounds: Size,
- _shaping: text::Shaping,
- _point: Point,
- _nearest_only: bool,
- ) -> Option<text::Hit> {
+ _paragraph: &mut Self::Paragraph,
+ _new_bounds: Size,
+ ) {
+ }
+
+ fn fill_paragraph(
+ &mut self,
+ _paragraph: &Self::Paragraph,
+ _position: Point,
+ _color: Color,
+ ) {
+ }
+
+ fn fill_text(
+ &mut self,
+ _paragraph: Text<'_, Self::Font>,
+ _position: Point,
+ _color: Color,
+ ) {
+ }
+}
+
+impl text::Paragraph for () {
+ type Font = Font;
+
+ fn content(&self) -> &str {
+ ""
+ }
+
+ fn text_size(&self) -> Pixels {
+ Pixels(16.0)
+ }
+
+ fn font(&self) -> Self::Font {
+ Font::default()
+ }
+
+ fn line_height(&self) -> text::LineHeight {
+ text::LineHeight::default()
+ }
+
+ fn shaping(&self) -> text::Shaping {
+ text::Shaping::default()
+ }
+
+ fn horizontal_alignment(&self) -> alignment::Horizontal {
+ alignment::Horizontal::Left
+ }
+
+ fn vertical_alignment(&self) -> alignment::Vertical {
+ alignment::Vertical::Top
+ }
+
+ fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> {
None
}
- fn fill_text(&mut self, _text: Text<'_, Self::Font>) {}
+ fn bounds(&self) -> Size {
+ Size::ZERO
+ }
+
+ fn min_bounds(&self) -> Size {
+ Size::ZERO
+ }
+
+ fn hit_test(&self, _point: Point) -> Option<text::Hit> {
+ None
+ }
}
diff --git a/core/src/text.rs b/core/src/text.rs
index fc8aa20e..c59c683a 100644
--- a/core/src/text.rs
+++ b/core/src/text.rs
@@ -1,6 +1,6 @@
//! Draw and interact with text.
use crate::alignment;
-use crate::{Color, Pixels, Point, Rectangle, Size};
+use crate::{Color, Pixels, Point, Size};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
@@ -12,17 +12,14 @@ pub struct Text<'a, Font> {
pub content: &'a str,
/// The bounds of the paragraph.
- pub bounds: Rectangle,
+ pub bounds: Size,
/// The size of the [`Text`] in logical pixels.
- pub size: f32,
+ pub size: Pixels,
/// The line height of the [`Text`].
pub line_height: LineHeight,
- /// The color of the [`Text`].
- pub color: Color,
-
/// The font of the [`Text`].
pub font: Font,
@@ -132,7 +129,10 @@ impl Hit {
/// A renderer capable of measuring and drawing [`Text`].
pub trait Renderer: crate::Renderer {
/// The font type used.
- type Font: Copy;
+ type Font: Copy + PartialEq;
+
+ /// The [`Paragraph`] of this [`Renderer`].
+ type Paragraph: Paragraph<Font = Self::Font> + 'static;
/// The icon font of the backend.
const ICON_FONT: Self::Font;
@@ -151,62 +151,107 @@ pub trait Renderer: crate::Renderer {
fn default_font(&self) -> Self::Font;
/// Returns the default size of [`Text`].
- fn default_size(&self) -> f32;
+ fn default_size(&self) -> Pixels;
- /// Measures the text in the given bounds and returns the minimum boundaries
- /// that can fit the contents.
- fn measure(
+ /// Loads a [`Self::Font`] from its bytes.
+ fn load_font(&mut self, font: Cow<'static, [u8]>);
+
+ /// Creates a new [`Paragraph`] laid out with the given [`Text`].
+ fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph;
+
+ /// Lays out the given [`Paragraph`] with some new boundaries.
+ fn resize_paragraph(
&self,
- content: &str,
- size: f32,
- line_height: LineHeight,
- font: Self::Font,
- bounds: Size,
- shaping: Shaping,
- ) -> Size;
-
- /// Measures the width of the text as if it were laid out in a single line.
- fn measure_width(
+ paragraph: &mut Self::Paragraph,
+ new_bounds: Size,
+ );
+
+ /// Updates a [`Paragraph`] to match the given [`Text`], if needed.
+ fn update_paragraph(
&self,
- content: &str,
- size: f32,
- font: Self::Font,
- shaping: Shaping,
- ) -> f32 {
- let bounds = self.measure(
- content,
- size,
- LineHeight::Absolute(Pixels(size)),
- font,
- Size::INFINITY,
- shaping,
- );
-
- bounds.width
+ paragraph: &mut Self::Paragraph,
+ text: Text<'_, Self::Font>,
+ ) {
+ if paragraph.content() != text.content
+ || paragraph.text_size() != text.size
+ || paragraph.line_height().to_absolute(text.size)
+ != text.line_height.to_absolute(text.size)
+ || paragraph.font() != text.font
+ || paragraph.shaping() != text.shaping
+ || paragraph.horizontal_alignment() != text.horizontal_alignment
+ || paragraph.vertical_alignment() != text.vertical_alignment
+ {
+ *paragraph = self.create_paragraph(text);
+ } else if paragraph.bounds() != text.bounds {
+ self.resize_paragraph(paragraph, text.bounds);
+ }
}
- /// Tests whether the provided point is within the boundaries of text
- /// laid out with the given parameters, returning information about
- /// the nearest character.
- ///
- /// If `nearest_only` is true, the hit test does not consider whether the
- /// the point is interior to any glyph bounds, returning only the character
- /// with the nearest centeroid.
- fn hit_test(
- &self,
- contents: &str,
- size: f32,
- line_height: LineHeight,
- font: Self::Font,
- bounds: Size,
- shaping: Shaping,
- point: Point,
- nearest_only: bool,
- ) -> Option<Hit>;
+ /// Draws the given [`Paragraph`] at the given position and with the given
+ /// [`Color`].
+ fn fill_paragraph(
+ &mut self,
+ text: &Self::Paragraph,
+ position: Point,
+ color: Color,
+ );
+
+ /// Draws the given [`Text`] at the given position and with the given
+ /// [`Color`].
+ fn fill_text(
+ &mut self,
+ text: Text<'_, Self::Font>,
+ position: Point,
+ color: Color,
+ );
+}
+/// A text paragraph.
+pub trait Paragraph: Default {
+ /// The font of this [`Paragraph`].
+ type Font;
- /// Loads a [`Self::Font`] from its bytes.
- fn load_font(&mut self, font: Cow<'static, [u8]>);
+ /// Returns the content of the [`Paragraph`].
+ fn content(&self) -> &str;
- /// Draws the given [`Text`].
- fn fill_text(&mut self, text: Text<'_, Self::Font>);
+ /// Returns the text size of the [`Paragraph`].
+ fn text_size(&self) -> Pixels;
+
+ /// Returns the [`LineHeight`] of the [`Paragraph`].
+ fn line_height(&self) -> LineHeight;
+
+ /// Returns the [`Font`] of the [`Paragraph`].
+ fn font(&self) -> Self::Font;
+
+ /// Returns the [`Shaping`] strategy of the [`Paragraph`].
+ fn shaping(&self) -> Shaping;
+
+ /// Returns the horizontal alignment of the [`Paragraph`].
+ fn horizontal_alignment(&self) -> alignment::Horizontal;
+
+ /// Returns the vertical alignment of the [`Paragraph`].
+ fn vertical_alignment(&self) -> alignment::Vertical;
+
+ /// Returns the boundaries of the [`Paragraph`].
+ fn bounds(&self) -> Size;
+
+ /// Returns the minimum boundaries that can fit the contents of the
+ /// [`Paragraph`].
+ fn min_bounds(&self) -> Size;
+
+ /// Tests whether the provided point is within the boundaries of the
+ /// [`Paragraph`], returning information about the nearest character.
+ fn hit_test(&self, point: Point) -> Option<Hit>;
+
+ /// Returns the distance to the given grapheme index in the [`Paragraph`].
+ fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
+
+ /// Returns the minimum width that can fit the contents of the [`Paragraph`].
+ fn min_width(&self) -> f32 {
+ self.min_bounds().width
+ }
+
+ /// Returns the minimum height that can fit the contents of the [`Paragraph`].
+ fn min_height(&self) -> f32 {
+ self.min_bounds().height
+ }
}
diff --git a/core/src/widget.rs b/core/src/widget.rs
index d6a99208..70328ff7 100644
--- a/core/src/widget.rs
+++ b/core/src/widget.rs
@@ -55,6 +55,7 @@ where
/// user interface.
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node;
@@ -62,7 +63,7 @@ where
/// Draws the [`Widget`] using the associated `Renderer`.
fn draw(
&self,
- state: &Tree,
+ tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs
index 79df2b02..0405537b 100644
--- a/core/src/widget/text.rs
+++ b/core/src/widget/text.rs
@@ -3,11 +3,12 @@ use crate::alignment;
use crate::layout;
use crate::mouse;
use crate::renderer;
-use crate::text;
-use crate::widget::Tree;
-use crate::{Color, Element, Layout, Length, Pixels, Rectangle, Widget};
+use crate::text::{self, Paragraph as _};
+use crate::widget::tree::{self, Tree};
+use crate::{Color, Element, Layout, Length, Pixels, Point, Rectangle, Widget};
use std::borrow::Cow;
+use std::cell::RefCell;
pub use text::{LineHeight, Shaping};
@@ -19,7 +20,7 @@ where
Renderer::Theme: StyleSheet,
{
content: Cow<'a, str>,
- size: Option<f32>,
+ size: Option<Pixels>,
line_height: LineHeight,
width: Length,
height: Length,
@@ -53,7 +54,7 @@ where
/// Sets the size of the [`Text`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
- self.size = Some(size.into().0);
+ self.size = Some(size.into());
self
}
@@ -117,11 +118,23 @@ where
}
}
+/// The internal state of a [`Text`] widget.
+#[derive(Debug, Default)]
+pub struct State<T>(RefCell<T>);
+
impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State<Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State(RefCell::new(Renderer::Paragraph::default())))
+ }
+
fn width(&self) -> Length {
self.width
}
@@ -132,30 +145,29 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
-
- let size = self.size.unwrap_or_else(|| renderer.default_size());
-
- let bounds = renderer.measure(
+ layout(
+ tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
+ renderer,
+ limits,
+ self.width,
+ self.height,
&self.content,
- size,
self.line_height,
- self.font.unwrap_or_else(|| renderer.default_font()),
- limits.max(),
+ self.size,
+ self.font,
+ self.horizontal_alignment,
+ self.vertical_alignment,
self.shaping,
- );
-
- let size = limits.resolve(bounds);
-
- layout::Node::new(size)
+ )
}
fn draw(
&self,
- _state: &Tree,
+ tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
@@ -163,22 +175,63 @@ where
_cursor_position: mouse::Cursor,
_viewport: &Rectangle,
) {
+ let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
+
draw(
renderer,
style,
layout,
- &self.content,
- self.size,
- self.line_height,
- self.font,
+ state,
theme.appearance(self.style.clone()),
- self.horizontal_alignment,
- self.vertical_alignment,
- self.shaping,
);
}
}
+/// Produces the [`layout::Node`] of a [`Text`] widget.
+pub fn layout<Renderer>(
+ state: &State<Renderer::Paragraph>,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ width: Length,
+ height: Length,
+ content: &str,
+ line_height: LineHeight,
+ size: Option<Pixels>,
+ font: Option<Renderer::Font>,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+ shaping: Shaping,
+) -> layout::Node
+where
+ Renderer: text::Renderer,
+{
+ let limits = limits.width(width).height(height);
+ let bounds = limits.max();
+
+ let size = size.unwrap_or_else(|| renderer.default_size());
+ let font = font.unwrap_or_else(|| renderer.default_font());
+
+ let mut paragraph = state.0.borrow_mut();
+
+ renderer.update_paragraph(
+ &mut paragraph,
+ text::Text {
+ content,
+ bounds,
+ size,
+ line_height,
+ font,
+ shaping,
+ horizontal_alignment,
+ vertical_alignment,
+ },
+ );
+
+ let size = limits.resolve(paragraph.min_bounds());
+
+ layout::Node::new(size)
+}
+
/// Draws text using the same logic as the [`Text`] widget.
///
/// Specifically:
@@ -193,44 +246,31 @@ pub fn draw<Renderer>(
renderer: &mut Renderer,
style: &renderer::Style,
layout: Layout<'_>,
- content: &str,
- size: Option<f32>,
- line_height: LineHeight,
- font: Option<Renderer::Font>,
+ state: &State<Renderer::Paragraph>,
appearance: Appearance,
- horizontal_alignment: alignment::Horizontal,
- vertical_alignment: alignment::Vertical,
- shaping: Shaping,
) where
Renderer: text::Renderer,
{
+ let paragraph = state.0.borrow();
let bounds = layout.bounds();
- let x = match horizontal_alignment {
+ let x = match paragraph.horizontal_alignment() {
alignment::Horizontal::Left => bounds.x,
alignment::Horizontal::Center => bounds.center_x(),
alignment::Horizontal::Right => bounds.x + bounds.width,
};
- let y = match vertical_alignment {
+ let y = match paragraph.vertical_alignment() {
alignment::Vertical::Top => bounds.y,
alignment::Vertical::Center => bounds.center_y(),
alignment::Vertical::Bottom => bounds.y + bounds.height,
};
- let size = size.unwrap_or_else(|| renderer.default_size());
-
- renderer.fill_text(crate::Text {
- content,
- size,
- line_height,
- bounds: Rectangle { x, y, ..bounds },
- color: appearance.color.unwrap_or(style.text_color),
- font: font.unwrap_or_else(|| renderer.default_font()),
- horizontal_alignment,
- vertical_alignment,
- shaping,
- });
+ renderer.fill_paragraph(
+ &paragraph,
+ Point::new(x, y),
+ appearance.color.unwrap_or(style.text_color),
+ );
}
impl<'a, Message, Renderer> From<Text<'a, Renderer>>