summaryrefslogtreecommitdiffstats
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
parentc9bd48704dd9679c033dd0b8588e2744a3df44a0 (diff)
downloadiced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.tar.gz
iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.tar.bz2
iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.zip
Implement explicit text caching in the widget state tree
-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
-rw-r--r--examples/color_palette/src/main.rs6
-rw-r--r--examples/combo_box/src/main.rs1
-rw-r--r--examples/custom_quad/src/main.rs1
-rw-r--r--examples/custom_widget/src/main.rs1
-rw-r--r--examples/game_of_life/src/main.rs2
-rw-r--r--examples/geometry/src/main.rs1
-rw-r--r--examples/integration/src/main.rs13
-rw-r--r--examples/loading_spinners/src/circular.rs1
-rw-r--r--examples/loading_spinners/src/linear.rs1
-rw-r--r--examples/modal/src/main.rs11
-rw-r--r--examples/toast/src/main.rs6
-rw-r--r--examples/tour/src/main.rs4
-rw-r--r--graphics/Cargo.toml11
-rw-r--r--graphics/fonts/Iced-Icons.ttf (renamed from tiny_skia/fonts/Iced-Icons.ttf)bin5108 -> 5108 bytes
-rw-r--r--graphics/src/backend.rs69
-rw-r--r--graphics/src/damage.rs26
-rw-r--r--graphics/src/geometry/text.rs6
-rw-r--r--graphics/src/lib.rs3
-rw-r--r--graphics/src/primitive.rs14
-rw-r--r--graphics/src/renderer.rs125
-rw-r--r--graphics/src/text.rs113
-rw-r--r--graphics/src/text/cache.rs120
-rw-r--r--graphics/src/text/paragraph.rs246
-rw-r--r--renderer/src/compositor.rs19
-rw-r--r--renderer/src/lib.rs73
-rw-r--r--renderer/src/settings.rs6
-rw-r--r--runtime/src/user_interface.rs17
-rw-r--r--src/settings.rs6
-rw-r--r--tiny_skia/src/backend.rs105
-rw-r--r--tiny_skia/src/settings.rs6
-rw-r--r--tiny_skia/src/text.rs305
-rw-r--r--tiny_skia/src/window/compositor.rs15
-rw-r--r--wgpu/Cargo.toml9
-rw-r--r--wgpu/fonts/Iced-Icons.ttfbin5108 -> 0 bytes
-rw-r--r--wgpu/src/backend.rs75
-rw-r--r--wgpu/src/layer.rs29
-rw-r--r--wgpu/src/layer/text.rs19
-rw-r--r--wgpu/src/lib.rs2
-rw-r--r--wgpu/src/settings.rs6
-rw-r--r--wgpu/src/text.rs509
-rw-r--r--wgpu/src/window/compositor.rs9
-rw-r--r--widget/src/button.rs23
-rw-r--r--widget/src/canvas.rs1
-rw-r--r--widget/src/checkbox.rs103
-rw-r--r--widget/src/column.rs2
-rw-r--r--widget/src/combo_box.rs167
-rw-r--r--widget/src/container.rs38
-rw-r--r--widget/src/image.rs1
-rw-r--r--widget/src/image/viewer.rs1
-rw-r--r--widget/src/lazy.rs3
-rw-r--r--widget/src/lazy/component.rs3
-rw-r--r--widget/src/lazy/responsive.rs17
-rw-r--r--widget/src/mouse_area.rs3
-rw-r--r--widget/src/overlay/menu.rs54
-rw-r--r--widget/src/pane_grid.rs13
-rw-r--r--widget/src/pane_grid/content.rs13
-rw-r--r--widget/src/pane_grid/title_bar.rs18
-rw-r--r--widget/src/pick_list.rs198
-rw-r--r--widget/src/progress_bar.rs1
-rw-r--r--widget/src/qr_code.rs1
-rw-r--r--widget/src/radio.rs72
-rw-r--r--widget/src/row.rs2
-rw-r--r--widget/src/rule.rs1
-rw-r--r--widget/src/scrollable.rs7
-rw-r--r--widget/src/slider.rs1
-rw-r--r--widget/src/space.rs1
-rw-r--r--widget/src/svg.rs1
-rw-r--r--widget/src/text_input.rs322
-rw-r--r--widget/src/toggler.rs88
-rw-r--r--widget/src/tooltip.rs17
-rw-r--r--widget/src/vertical_slider.rs1
79 files changed, 1913 insertions, 1708 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>>
diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs
index 736a9d53..7dc981d9 100644
--- a/examples/color_palette/src/main.rs
+++ b/examples/color_palette/src/main.rs
@@ -3,8 +3,8 @@ use iced::mouse;
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};
use iced::widget::{column, row, text, Slider};
use iced::{
- Color, Element, Length, Point, Rectangle, Renderer, Sandbox, Settings,
- Size, Vector,
+ Color, Element, Length, Pixels, Point, Rectangle, Renderer, Sandbox,
+ Settings, Size, Vector,
};
use palette::{
self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue,
@@ -168,7 +168,7 @@ impl Theme {
let mut text = canvas::Text {
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Top,
- size: 15.0,
+ size: Pixels(15.0),
..canvas::Text::default()
};
diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs
index 2e6f95d5..4f347667 100644
--- a/examples/combo_box/src/main.rs
+++ b/examples/combo_box/src/main.rs
@@ -40,7 +40,6 @@ impl Sandbox for Example {
Message::Selected(language) => {
self.selected_language = Some(language);
self.text = language.hello().to_string();
- self.languages.unfocus();
}
Message::OptionHovered(language) => {
self.text = language.hello().to_string();
diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs
index 4b300116..91401f73 100644
--- a/examples/custom_quad/src/main.rs
+++ b/examples/custom_quad/src/main.rs
@@ -36,6 +36,7 @@ mod quad {
fn layout(
&self,
+ _tree: &widget::Tree,
_renderer: &Renderer,
_limits: &layout::Limits,
) -> layout::Node {
diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs
index 713bc62d..e0a23541 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -43,6 +43,7 @@ mod circle {
fn layout(
&self,
+ _tree: &widget::Tree,
_renderer: &Renderer,
_limits: &layout::Limits,
) -> layout::Node {
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index e951d734..fa711744 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -591,7 +591,7 @@ mod grid {
let text = Text {
color: Color::WHITE,
- size: 14.0,
+ size: 14.0.into(),
position: Point::new(frame.width(), frame.height()),
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Bottom,
diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs
index 3bc7f46b..a0d505b8 100644
--- a/examples/geometry/src/main.rs
+++ b/examples/geometry/src/main.rs
@@ -26,6 +26,7 @@ mod rainbow {
fn layout(
&self,
+ _tree: &widget::Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index 342d4c69..e011a411 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -8,7 +8,7 @@ use iced_wgpu::graphics::Viewport;
use iced_wgpu::{wgpu, Backend, Renderer, Settings};
use iced_winit::core::mouse;
use iced_winit::core::renderer;
-use iced_winit::core::{Color, Size};
+use iced_winit::core::{Color, Font, Pixels, Size};
use iced_winit::runtime::program;
use iced_winit::runtime::Debug;
use iced_winit::style::Theme;
@@ -143,12 +143,11 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize iced
let mut debug = Debug::new();
- let mut renderer = Renderer::new(Backend::new(
- &device,
- &queue,
- Settings::default(),
- format,
- ));
+ let mut renderer = Renderer::new(
+ Backend::new(&device, &queue, Settings::default(), format),
+ Font::default(),
+ Pixels(16.0),
+ );
let mut state = program::State::new(
controls,
diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs
index 3898d76e..6bcfd507 100644
--- a/examples/loading_spinners/src/circular.rs
+++ b/examples/loading_spinners/src/circular.rs
@@ -254,6 +254,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &iced::Renderer<Theme>,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs
index 20fbe9f3..3addd7bb 100644
--- a/examples/loading_spinners/src/linear.rs
+++ b/examples/loading_spinners/src/linear.rs
@@ -175,6 +175,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs
index 8a48f830..5d47f02c 100644
--- a/examples/modal/src/main.rs
+++ b/examples/modal/src/main.rs
@@ -285,10 +285,13 @@ mod modal {
fn layout(
&self,
+ tree: &widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.base.as_widget().layout(renderer, limits)
+ self.base
+ .as_widget()
+ .layout(&tree.children[0], renderer, limits)
}
fn on_event(
@@ -408,7 +411,11 @@ mod modal {
.width(Length::Fill)
.height(Length::Fill);
- let mut child = self.content.as_widget().layout(renderer, &limits);
+ let mut child = self
+ .content
+ .as_widget()
+ .layout(self.tree, renderer, &limits);
+
child.align(Alignment::Center, Alignment::Center, limits.max());
let mut node = layout::Node::with_children(self.size, vec![child]);
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index 42f6c348..01eea3cc 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -326,10 +326,13 @@ mod toast {
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.content.as_widget().layout(renderer, limits)
+ self.content
+ .as_widget()
+ .layout(&tree.children[0], renderer, limits)
}
fn tag(&self) -> widget::tree::Tag {
@@ -517,6 +520,7 @@ mod toast {
10.0,
Alignment::End,
self.toasts,
+ self.state,
)
.translate(Vector::new(position.x, position.y))
}
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index 13bcd5ff..10de2ae1 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -5,7 +5,7 @@ use iced::widget::{
scrollable, slider, text, text_input, toggler, vertical_space,
};
use iced::widget::{Button, Column, Container, Slider};
-use iced::{Color, Element, Font, Length, Renderer, Sandbox, Settings};
+use iced::{Color, Element, Font, Length, Pixels, Renderer, Sandbox, Settings};
pub fn main() -> iced::Result {
env_logger::init();
@@ -571,7 +571,7 @@ impl<'a> Step {
text_input = text_input.icon(text_input::Icon {
font: Font::default(),
code_point: '🚀',
- size: Some(28.0),
+ size: Some(Pixels(28.0)),
spacing: 10.0,
side: text_input::Side::Right,
});
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index ca7bf61a..442eb007 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -23,6 +23,9 @@ log = "0.4"
raw-window-handle = "0.5"
thiserror = "1.0"
bitflags = "1.2"
+cosmic-text = "0.9"
+rustc-hash = "1.1"
+unicode-segmentation = "1.6"
[dependencies.bytemuck]
version = "1.4"
@@ -32,6 +35,14 @@ features = ["derive"]
version = "0.10"
path = "../core"
+[dependencies.twox-hash]
+version = "1.6"
+default-features = false
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
+version = "1.6.1"
+features = ["std"]
+
[dependencies.image]
version = "0.24"
optional = true
diff --git a/tiny_skia/fonts/Iced-Icons.ttf b/graphics/fonts/Iced-Icons.ttf
index e3273141..e3273141 100644
--- a/tiny_skia/fonts/Iced-Icons.ttf
+++ b/graphics/fonts/Iced-Icons.ttf
Binary files differ
diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs
index 59e95bf8..6774b9ca 100644
--- a/graphics/src/backend.rs
+++ b/graphics/src/backend.rs
@@ -1,8 +1,8 @@
//! Write a graphics backend.
-use iced_core::image;
-use iced_core::svg;
-use iced_core::text;
-use iced_core::{Font, Point, Size};
+use crate::core::image;
+use crate::core::svg;
+use crate::core::Size;
+use crate::text;
use std::borrow::Cow;
@@ -12,70 +12,15 @@ use std::borrow::Cow;
pub trait Backend {
/// The custom kind of primitives this [`Backend`] supports.
type Primitive;
-
- /// Trims the measurements cache.
- ///
- /// This method is currently necessary to properly trim the text cache in
- /// `iced_wgpu` and `iced_glow` because of limitations in the text rendering
- /// pipeline. It will be removed in the future.
- fn trim_measurements(&mut self) {}
}
/// A graphics backend that supports text rendering.
pub trait Text {
- /// The icon font of the backend.
- const ICON_FONT: Font;
-
- /// The `char` representing a ✔ icon in the [`ICON_FONT`].
- ///
- /// [`ICON_FONT`]: Self::ICON_FONT
- const CHECKMARK_ICON: char;
-
- /// The `char` representing a ▼ icon in the built-in [`ICON_FONT`].
- ///
- /// [`ICON_FONT`]: Self::ICON_FONT
- const ARROW_DOWN_ICON: char;
-
- /// Returns the default [`Font`].
- fn default_font(&self) -> Font;
-
- /// Returns the default size of text.
- fn default_size(&self) -> f32;
-
- /// Measures the text contents with the given size and font,
- /// returning the size of a laid out paragraph that fits in the provided
- /// bounds.
- fn measure(
- &self,
- contents: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- ) -> Size;
-
- /// 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: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- point: Point,
- nearest_only: bool,
- ) -> Option<text::Hit>;
-
/// Loads a [`Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
+
+ /// Returns the [`cosmic_text::FontSystem`] of the [`Backend`].
+ fn font_system(&self) -> &text::FontSystem;
}
/// A graphics backend that supports image rendering.
diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs
index 2f29956e..3276c2d4 100644
--- a/graphics/src/damage.rs
+++ b/graphics/src/damage.rs
@@ -40,6 +40,32 @@ impl<T: Damage> Damage for Primitive<T> {
bounds.expand(1.5)
}
+ Self::Paragraph {
+ paragraph,
+ position,
+ ..
+ } => {
+ let mut bounds =
+ Rectangle::new(*position, paragraph.min_bounds);
+
+ bounds.x = match paragraph.horizontal_alignment {
+ alignment::Horizontal::Left => bounds.x,
+ alignment::Horizontal::Center => {
+ bounds.x - bounds.width / 2.0
+ }
+ alignment::Horizontal::Right => bounds.x - bounds.width,
+ };
+
+ bounds.y = match paragraph.vertical_alignment {
+ alignment::Vertical::Top => bounds.y,
+ alignment::Vertical::Center => {
+ bounds.y - bounds.height / 2.0
+ }
+ alignment::Vertical::Bottom => bounds.y - bounds.height,
+ };
+
+ bounds.expand(1.5)
+ }
Self::Quad { bounds, .. }
| Self::Image { bounds, .. }
| Self::Svg { bounds, .. } => bounds.expand(1.0),
diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs
index c584f3cd..0bf7ec97 100644
--- a/graphics/src/geometry/text.rs
+++ b/graphics/src/geometry/text.rs
@@ -1,6 +1,6 @@
use crate::core::alignment;
use crate::core::text::{LineHeight, Shaping};
-use crate::core::{Color, Font, Point};
+use crate::core::{Color, Font, Pixels, Point};
/// A bunch of text that can be drawn to a canvas
#[derive(Debug, Clone)]
@@ -19,7 +19,7 @@ pub struct Text {
/// The color of the text
pub color: Color,
/// The size of the text
- pub size: f32,
+ pub size: Pixels,
/// The line height of the text.
pub line_height: LineHeight,
/// The font of the text
@@ -38,7 +38,7 @@ impl Default for Text {
content: String::new(),
position: Point::ORIGIN,
color: Color::BLACK,
- size: 16.0,
+ size: Pixels(16.0),
line_height: LineHeight::Relative(1.2),
font: Font::default(),
horizontal_alignment: alignment::Horizontal::Left,
diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs
index af374a2f..902eb5b0 100644
--- a/graphics/src/lib.rs
+++ b/graphics/src/lib.rs
@@ -9,7 +9,7 @@
)]
#![deny(
missing_debug_implementations,
- missing_docs,
+ //missing_docs,
unsafe_code,
unused_results,
clippy::extra_unused_lifetimes,
@@ -34,6 +34,7 @@ pub mod damage;
pub mod gradient;
pub mod mesh;
pub mod renderer;
+pub mod text;
#[cfg(feature = "geometry")]
pub mod geometry;
diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs
index 7592a410..cdc8923e 100644
--- a/graphics/src/primitive.rs
+++ b/graphics/src/primitive.rs
@@ -3,7 +3,8 @@ use crate::core::alignment;
use crate::core::image;
use crate::core::svg;
use crate::core::text;
-use crate::core::{Background, Color, Font, Rectangle, Vector};
+use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
+use crate::text::paragraph;
use std::sync::Arc;
@@ -19,7 +20,7 @@ pub enum Primitive<T> {
/// The color of the text
color: Color,
/// The size of the text in logical pixels
- size: f32,
+ size: Pixels,
/// The line height of the text
line_height: text::LineHeight,
/// The font of the text
@@ -31,6 +32,15 @@ pub enum Primitive<T> {
/// The shaping strategy of the text.
shaping: text::Shaping,
},
+ /// A paragraph primitive
+ Paragraph {
+ /// The [`Paragraph`].
+ paragraph: paragraph::Weak,
+ /// The position of the [`Paragraph`].
+ position: Point,
+ /// The color of the [`Paragraph`].
+ color: Color,
+ },
/// A quad primitive
Quad {
/// The bounds of the quad
diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs
index c0cec60a..f93f4a6d 100644
--- a/graphics/src/renderer.rs
+++ b/graphics/src/renderer.rs
@@ -1,15 +1,15 @@
//! Create a renderer from a [`Backend`].
use crate::backend::{self, Backend};
-use crate::Primitive;
-
-use iced_core::image;
-use iced_core::layout;
-use iced_core::renderer;
-use iced_core::svg;
-use iced_core::text::{self, Text};
-use iced_core::{
- Background, Color, Element, Font, Point, Rectangle, Size, Vector,
+use crate::core;
+use crate::core::image;
+use crate::core::renderer;
+use crate::core::svg;
+use crate::core::text::Text;
+use crate::core::{
+ Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
};
+use crate::text;
+use crate::Primitive;
use std::borrow::Cow;
use std::marker::PhantomData;
@@ -18,15 +18,23 @@ use std::marker::PhantomData;
#[derive(Debug)]
pub struct Renderer<B: Backend, Theme> {
backend: B,
+ default_font: Font,
+ default_text_size: Pixels,
primitives: Vec<Primitive<B::Primitive>>,
theme: PhantomData<Theme>,
}
impl<B: Backend, T> Renderer<B, T> {
/// Creates a new [`Renderer`] from the given [`Backend`].
- pub fn new(backend: B) -> Self {
+ pub fn new(
+ backend: B,
+ default_font: Font,
+ default_text_size: Pixels,
+ ) -> Self {
Self {
backend,
+ default_font,
+ default_text_size,
primitives: Vec::new(),
theme: PhantomData,
}
@@ -88,16 +96,6 @@ impl<B: Backend, T> Renderer<B, T> {
impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> {
type Theme = T;
- fn layout<Message>(
- &mut self,
- element: &Element<'_, Message, Self>,
- limits: &layout::Limits,
- ) -> layout::Node {
- self.backend.trim_measurements();
-
- element.as_widget().layout(self, limits)
- }
-
fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) {
let current = self.start_layer();
@@ -137,77 +135,66 @@ impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> {
}
}
-impl<B, T> text::Renderer for Renderer<B, T>
+impl<B, T> core::text::Renderer for Renderer<B, T>
where
B: Backend + backend::Text,
{
type Font = Font;
+ type Paragraph = text::Paragraph;
- const ICON_FONT: Font = B::ICON_FONT;
- const CHECKMARK_ICON: char = B::CHECKMARK_ICON;
- const ARROW_DOWN_ICON: char = B::ARROW_DOWN_ICON;
+ const ICON_FONT: Font = Font::with_name("Iced-Icons");
+ const CHECKMARK_ICON: char = '\u{f00c}';
+ const ARROW_DOWN_ICON: char = '\u{e800}';
fn default_font(&self) -> Self::Font {
- self.backend().default_font()
+ self.default_font
}
- fn default_size(&self) -> f32 {
- self.backend().default_size()
+ fn default_size(&self) -> Pixels {
+ self.default_text_size
}
- fn measure(
- &self,
- content: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- ) -> Size {
- self.backend().measure(
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- )
+ fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
+ self.backend.load_font(bytes);
+ }
+
+ fn create_paragraph(&self, text: Text<'_, Self::Font>) -> text::Paragraph {
+ text::Paragraph::with_text(text, self.backend.font_system())
}
- fn hit_test(
+ fn resize_paragraph(
&self,
- content: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- point: Point,
- nearest_only: bool,
- ) -> Option<text::Hit> {
- self.backend().hit_test(
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- point,
- nearest_only,
- )
+ paragraph: &mut Self::Paragraph,
+ new_bounds: Size,
+ ) {
+ paragraph.resize(new_bounds, self.backend.font_system());
}
- fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- self.backend.load_font(bytes);
+ fn fill_paragraph(
+ &mut self,
+ paragraph: &Self::Paragraph,
+ position: Point,
+ color: Color,
+ ) {
+ self.primitives.push(Primitive::Paragraph {
+ paragraph: paragraph.downgrade(),
+ position,
+ color,
+ });
}
- fn fill_text(&mut self, text: Text<'_, Self::Font>) {
+ fn fill_text(
+ &mut self,
+ text: Text<'_, Self::Font>,
+ position: Point,
+ color: Color,
+ ) {
self.primitives.push(Primitive::Text {
content: text.content.to_string(),
- bounds: text.bounds,
+ bounds: Rectangle::new(position, text.bounds),
size: text.size,
line_height: text.line_height,
- color: text.color,
+ color,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
diff --git a/graphics/src/text.rs b/graphics/src/text.rs
new file mode 100644
index 00000000..bbe9d7cb
--- /dev/null
+++ b/graphics/src/text.rs
@@ -0,0 +1,113 @@
+pub mod cache;
+pub mod paragraph;
+
+pub use cache::Cache;
+pub use paragraph::Paragraph;
+
+pub use cosmic_text;
+
+use crate::core::font::{self, Font};
+use crate::core::text::Shaping;
+use crate::core::Size;
+
+use std::sync::{self, Arc, RwLock};
+
+#[allow(missing_debug_implementations)]
+pub struct FontSystem(RwLock<cosmic_text::FontSystem>);
+
+impl FontSystem {
+ pub fn new() -> Self {
+ FontSystem(RwLock::new(cosmic_text::FontSystem::new_with_fonts(
+ [cosmic_text::fontdb::Source::Binary(Arc::new(
+ include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
+ ))]
+ .into_iter(),
+ )))
+ }
+
+ pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem {
+ self.0.get_mut().expect("Lock font system")
+ }
+
+ pub fn write(&self) -> sync::RwLockWriteGuard<'_, cosmic_text::FontSystem> {
+ self.0.write().expect("Write font system")
+ }
+}
+
+impl Default for FontSystem {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
+ let (width, total_lines) = buffer
+ .layout_runs()
+ .fold((0.0, 0usize), |(width, total_lines), run| {
+ (run.line_w.max(width), total_lines + 1)
+ });
+
+ Size::new(width, total_lines as f32 * buffer.metrics().line_height)
+}
+
+pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> {
+ cosmic_text::Attrs::new()
+ .family(to_family(font.family))
+ .weight(to_weight(font.weight))
+ .stretch(to_stretch(font.stretch))
+ .style(to_style(font.style))
+}
+
+fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
+ match family {
+ font::Family::Name(name) => cosmic_text::Family::Name(name),
+ font::Family::SansSerif => cosmic_text::Family::SansSerif,
+ font::Family::Serif => cosmic_text::Family::Serif,
+ font::Family::Cursive => cosmic_text::Family::Cursive,
+ font::Family::Fantasy => cosmic_text::Family::Fantasy,
+ font::Family::Monospace => cosmic_text::Family::Monospace,
+ }
+}
+
+fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
+ match weight {
+ font::Weight::Thin => cosmic_text::Weight::THIN,
+ font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
+ font::Weight::Light => cosmic_text::Weight::LIGHT,
+ font::Weight::Normal => cosmic_text::Weight::NORMAL,
+ font::Weight::Medium => cosmic_text::Weight::MEDIUM,
+ font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
+ font::Weight::Bold => cosmic_text::Weight::BOLD,
+ font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
+ font::Weight::Black => cosmic_text::Weight::BLACK,
+ }
+}
+
+fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
+ match stretch {
+ font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
+ font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
+ font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
+ font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
+ font::Stretch::Normal => cosmic_text::Stretch::Normal,
+ font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
+ font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
+ font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
+ font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
+ }
+}
+
+fn to_style(style: font::Style) -> cosmic_text::Style {
+ match style {
+ font::Style::Normal => cosmic_text::Style::Normal,
+ font::Style::Italic => cosmic_text::Style::Italic,
+ font::Style::Oblique => cosmic_text::Style::Oblique,
+ }
+}
+
+pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
+ match shaping {
+ Shaping::Basic => cosmic_text::Shaping::Basic,
+ Shaping::Advanced => cosmic_text::Shaping::Advanced,
+ }
+}
diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs
new file mode 100644
index 00000000..8aea6715
--- /dev/null
+++ b/graphics/src/text/cache.rs
@@ -0,0 +1,120 @@
+use crate::core::{Font, Size};
+use crate::text;
+
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::collections::hash_map;
+use std::hash::{BuildHasher, Hash, Hasher};
+
+#[allow(missing_debug_implementations)]
+#[derive(Default)]
+pub struct Cache {
+ entries: FxHashMap<KeyHash, cosmic_text::Buffer>,
+ aliases: FxHashMap<KeyHash, KeyHash>,
+ recently_used: FxHashSet<KeyHash>,
+ hasher: HashBuilder,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+type HashBuilder = twox_hash::RandomXxHashBuilder64;
+
+#[cfg(target_arch = "wasm32")]
+type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
+
+impl Cache {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn get(&self, key: &KeyHash) -> Option<&cosmic_text::Buffer> {
+ self.entries.get(key)
+ }
+
+ pub fn allocate(
+ &mut self,
+ font_system: &mut cosmic_text::FontSystem,
+ key: Key<'_>,
+ ) -> (KeyHash, &mut cosmic_text::Buffer) {
+ let hash = key.hash(self.hasher.build_hasher());
+
+ if let Some(hash) = self.aliases.get(&hash) {
+ let _ = self.recently_used.insert(*hash);
+
+ return (*hash, self.entries.get_mut(hash).unwrap());
+ }
+
+ if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
+ let metrics = cosmic_text::Metrics::new(key.size, key.line_height);
+ let mut buffer = cosmic_text::Buffer::new(font_system, metrics);
+
+ buffer.set_size(
+ font_system,
+ key.bounds.width,
+ key.bounds.height.max(key.line_height),
+ );
+ buffer.set_text(
+ font_system,
+ key.content,
+ text::to_attributes(key.font),
+ text::to_shaping(key.shaping),
+ );
+
+ let bounds = text::measure(&buffer);
+ let _ = entry.insert(buffer);
+
+ for bounds in [
+ bounds,
+ Size {
+ width: key.bounds.width,
+ ..bounds
+ },
+ ] {
+ if key.bounds != bounds {
+ let _ = self.aliases.insert(
+ Key { bounds, ..key }.hash(self.hasher.build_hasher()),
+ hash,
+ );
+ }
+ }
+ }
+
+ let _ = self.recently_used.insert(hash);
+
+ (hash, self.entries.get_mut(&hash).unwrap())
+ }
+
+ pub fn trim(&mut self) {
+ self.entries
+ .retain(|key, _| self.recently_used.contains(key));
+
+ self.aliases
+ .retain(|_, value| self.recently_used.contains(value));
+
+ self.recently_used.clear();
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct Key<'a> {
+ pub content: &'a str,
+ pub size: f32,
+ pub line_height: f32,
+ pub font: Font,
+ pub bounds: Size,
+ pub shaping: text::Shaping,
+}
+
+impl Key<'_> {
+ fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
+ self.content.hash(&mut hasher);
+ self.size.to_bits().hash(&mut hasher);
+ self.line_height.to_bits().hash(&mut hasher);
+ self.font.hash(&mut hasher);
+ self.bounds.width.to_bits().hash(&mut hasher);
+ self.bounds.height.to_bits().hash(&mut hasher);
+ self.shaping.hash(&mut hasher);
+
+ hasher.finish()
+ }
+}
+
+pub type KeyHash = u64;
diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs
new file mode 100644
index 00000000..7b70376a
--- /dev/null
+++ b/graphics/src/text/paragraph.rs
@@ -0,0 +1,246 @@
+use crate::core;
+use crate::core::alignment;
+use crate::core::text::{Hit, LineHeight, Shaping, Text};
+use crate::core::{Font, Pixels, Point, Size};
+use crate::text::{self, FontSystem};
+
+use std::fmt;
+use std::sync::{self, Arc};
+
+#[derive(Clone, PartialEq, Default)]
+pub struct Paragraph(Arc<Internal>);
+
+struct Internal {
+ buffer: cosmic_text::Buffer,
+ content: String, // TODO: Reuse from `buffer` (?)
+ font: Font,
+ shaping: Shaping,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+ bounds: Size,
+ min_bounds: Size,
+}
+
+impl Paragraph {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self {
+ let mut font_system = font_system.write();
+
+ let mut buffer = cosmic_text::Buffer::new(
+ &mut font_system,
+ cosmic_text::Metrics::new(
+ text.size.into(),
+ text.line_height.to_absolute(text.size).into(),
+ ),
+ );
+
+ buffer.set_size(
+ &mut font_system,
+ text.bounds.width,
+ text.bounds.height,
+ );
+
+ buffer.set_text(
+ &mut font_system,
+ text.content,
+ text::to_attributes(text.font),
+ text::to_shaping(text.shaping),
+ );
+
+ let min_bounds = text::measure(&buffer);
+
+ Self(Arc::new(Internal {
+ buffer,
+ content: text.content.to_owned(),
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ bounds: text.bounds,
+ min_bounds,
+ }))
+ }
+
+ pub fn buffer(&self) -> &cosmic_text::Buffer {
+ &self.0.buffer
+ }
+
+ pub fn downgrade(&self) -> Weak {
+ Weak {
+ raw: Arc::downgrade(&self.0),
+ min_bounds: self.0.min_bounds,
+ horizontal_alignment: self.0.horizontal_alignment,
+ vertical_alignment: self.0.vertical_alignment,
+ }
+ }
+
+ pub fn resize(&mut self, new_bounds: Size, font_system: &FontSystem) {
+ if let Some(internal) = Arc::get_mut(&mut self.0) {
+ // If there is no strong reference holding on to the paragraph, we
+ // resize the buffer in-place
+ internal.buffer.set_size(
+ &mut font_system.write(),
+ new_bounds.width,
+ new_bounds.height,
+ );
+
+ internal.bounds = new_bounds;
+ internal.min_bounds = text::measure(&internal.buffer);
+ } else {
+ let metrics = self.0.buffer.metrics();
+
+ // If there is a strong reference somewhere, we recompute the buffer
+ // from scratch
+ *self = Self::with_text(
+ Text {
+ content: &self.0.content,
+ bounds: self.0.bounds,
+ size: Pixels(metrics.font_size),
+ line_height: LineHeight::Absolute(Pixels(
+ metrics.line_height,
+ )),
+ font: self.0.font,
+ horizontal_alignment: self.0.horizontal_alignment,
+ vertical_alignment: self.0.vertical_alignment,
+ shaping: self.0.shaping,
+ },
+ font_system,
+ );
+ }
+ }
+}
+
+impl core::text::Paragraph for Paragraph {
+ type Font = Font;
+
+ fn content(&self) -> &str {
+ &self.0.content
+ }
+
+ fn text_size(&self) -> Pixels {
+ Pixels(self.0.buffer.metrics().font_size)
+ }
+
+ fn line_height(&self) -> LineHeight {
+ LineHeight::Absolute(Pixels(self.0.buffer.metrics().line_height))
+ }
+
+ fn font(&self) -> Font {
+ self.0.font
+ }
+
+ fn shaping(&self) -> Shaping {
+ self.0.shaping
+ }
+
+ fn horizontal_alignment(&self) -> alignment::Horizontal {
+ self.0.horizontal_alignment
+ }
+
+ fn vertical_alignment(&self) -> alignment::Vertical {
+ self.0.vertical_alignment
+ }
+
+ fn bounds(&self) -> Size {
+ self.0.bounds
+ }
+
+ fn min_bounds(&self) -> Size {
+ self.0.min_bounds
+ }
+
+ fn hit_test(&self, point: Point) -> Option<Hit> {
+ let cursor = self.0.buffer.hit(point.x, point.y)?;
+
+ Some(Hit::CharOffset(cursor.index))
+ }
+
+ fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
+ let run = self.0.buffer.layout_runs().nth(line)?;
+
+ // TODO: Index represents a grapheme, not a glyph
+ let glyph = run.glyphs.get(index).or_else(|| run.glyphs.last())?;
+
+ let advance_last = if index == run.glyphs.len() {
+ glyph.w
+ } else {
+ 0.0
+ };
+
+ Some(Point::new(
+ glyph.x + glyph.x_offset * glyph.font_size + advance_last,
+ glyph.y - glyph.y_offset * glyph.font_size,
+ ))
+ }
+}
+
+impl PartialEq for Internal {
+ fn eq(&self, other: &Self) -> bool {
+ self.content == other.content
+ && self.font == other.font
+ && self.shaping == other.shaping
+ && self.horizontal_alignment == other.horizontal_alignment
+ && self.vertical_alignment == other.vertical_alignment
+ && self.bounds == other.bounds
+ && self.min_bounds == other.min_bounds
+ && self.buffer.metrics() == other.buffer.metrics()
+ }
+}
+
+impl Default for Internal {
+ fn default() -> Self {
+ Self {
+ buffer: cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
+ font_size: 1.0,
+ line_height: 1.0,
+ }),
+ content: String::new(),
+ font: Font::default(),
+ shaping: Shaping::default(),
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ bounds: Size::ZERO,
+ min_bounds: Size::ZERO,
+ }
+ }
+}
+
+impl fmt::Debug for Paragraph {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Paragraph")
+ .field("content", &self.0.content)
+ .field("font", &self.0.font)
+ .field("shaping", &self.0.shaping)
+ .field("horizontal_alignment", &self.0.horizontal_alignment)
+ .field("vertical_alignment", &self.0.vertical_alignment)
+ .field("bounds", &self.0.bounds)
+ .field("min_bounds", &self.0.min_bounds)
+ .finish()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Weak {
+ raw: sync::Weak<Internal>,
+ pub min_bounds: Size,
+ pub horizontal_alignment: alignment::Horizontal,
+ pub vertical_alignment: alignment::Vertical,
+}
+
+impl Weak {
+ pub fn upgrade(&self) -> Option<Paragraph> {
+ self.raw.upgrade().map(Paragraph)
+ }
+}
+
+impl PartialEq for Weak {
+ fn eq(&self, other: &Self) -> bool {
+ match (self.raw.upgrade(), other.raw.upgrade()) {
+ (Some(p1), Some(p2)) => p1 == p2,
+ _ => false,
+ }
+ }
+}
diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs
index 8b17a4b0..d1500089 100644
--- a/renderer/src/compositor.rs
+++ b/renderer/src/compositor.rs
@@ -224,16 +224,15 @@ impl Candidate {
match self {
Self::TinySkia => {
let (compositor, backend) =
- iced_tiny_skia::window::compositor::new(
- iced_tiny_skia::Settings {
- default_font: settings.default_font,
- default_text_size: settings.default_text_size,
- },
- );
+ iced_tiny_skia::window::compositor::new();
Ok((
Compositor::TinySkia(compositor),
- Renderer::TinySkia(iced_tiny_skia::Renderer::new(backend)),
+ Renderer::TinySkia(iced_tiny_skia::Renderer::new(
+ backend,
+ settings.default_font,
+ settings.default_text_size,
+ )),
))
}
#[cfg(feature = "wgpu")]
@@ -250,7 +249,11 @@ impl Candidate {
Ok((
Compositor::Wgpu(compositor),
- Renderer::Wgpu(iced_wgpu::Renderer::new(backend)),
+ Renderer::Wgpu(iced_wgpu::Renderer::new(
+ backend,
+ settings.default_font,
+ settings.default_text_size,
+ )),
))
}
#[cfg(not(feature = "wgpu"))]
diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs
index 7d1a02c2..2b282a0b 100644
--- a/renderer/src/lib.rs
+++ b/renderer/src/lib.rs
@@ -16,7 +16,10 @@ pub use geometry::Geometry;
use crate::core::renderer;
use crate::core::text::{self, Text};
-use crate::core::{Background, Font, Point, Rectangle, Size, Vector};
+use crate::core::{
+ Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
+};
+use crate::graphics::text::Paragraph;
use crate::graphics::Mesh;
use std::borrow::Cow;
@@ -142,6 +145,7 @@ impl<T> core::Renderer for Renderer<T> {
impl<T> text::Renderer for Renderer<T> {
type Font = Font;
+ type Paragraph = Paragraph;
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON;
@@ -152,59 +156,50 @@ impl<T> text::Renderer for Renderer<T> {
delegate!(self, renderer, renderer.default_font())
}
- fn default_size(&self) -> f32 {
+ fn default_size(&self) -> Pixels {
delegate!(self, renderer, renderer.default_size())
}
- fn measure(
- &self,
- content: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- ) -> Size {
- delegate!(
- self,
- renderer,
- renderer.measure(content, size, line_height, font, bounds, shaping)
- )
+ fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph {
+ delegate!(self, renderer, renderer.create_paragraph(text))
}
- fn hit_test(
+ fn resize_paragraph(
&self,
- content: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- point: Point,
- nearest_only: bool,
- ) -> Option<text::Hit> {
+ paragraph: &mut Self::Paragraph,
+ new_bounds: Size,
+ ) {
delegate!(
self,
renderer,
- renderer.hit_test(
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- point,
- nearest_only
- )
- )
+ renderer.resize_paragraph(paragraph, new_bounds)
+ );
}
fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
delegate!(self, renderer, renderer.load_font(bytes));
}
- fn fill_text(&mut self, text: Text<'_, Self::Font>) {
- delegate!(self, renderer, renderer.fill_text(text));
+ fn fill_paragraph(
+ &mut self,
+ text: &Self::Paragraph,
+ position: Point,
+ color: Color,
+ ) {
+ delegate!(
+ self,
+ renderer,
+ renderer.fill_paragraph(text, position, color)
+ );
+ }
+
+ fn fill_text(
+ &mut self,
+ text: Text<'_, Self::Font>,
+ position: Point,
+ color: Color,
+ ) {
+ delegate!(self, renderer, renderer.fill_text(text, position, color));
}
}
diff --git a/renderer/src/settings.rs b/renderer/src/settings.rs
index 2e51f339..08f2099e 100644
--- a/renderer/src/settings.rs
+++ b/renderer/src/settings.rs
@@ -1,4 +1,4 @@
-use crate::core::Font;
+use crate::core::{Font, Pixels};
use crate::graphics::Antialiasing;
/// The settings of a [`Backend`].
@@ -12,7 +12,7 @@ pub struct Settings {
/// The default size of text.
///
/// By default, it will be set to `16.0`.
- pub default_text_size: f32,
+ pub default_text_size: Pixels,
/// The antialiasing strategy that will be used for triangle primitives.
///
@@ -24,7 +24,7 @@ impl Default for Settings {
fn default() -> Settings {
Settings {
default_font: Font::default(),
- default_text_size: 16.0,
+ default_text_size: Pixels(16.0),
antialiasing: None,
}
}
diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs
index e529c004..a730102c 100644
--- a/runtime/src/user_interface.rs
+++ b/runtime/src/user_interface.rs
@@ -95,8 +95,11 @@ where
let Cache { mut state } = cache;
state.diff(root.as_widget());
- let base =
- renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds));
+ let base = root.as_widget().layout(
+ &state,
+ renderer,
+ &layout::Limits::new(Size::ZERO, bounds),
+ );
UserInterface {
root,
@@ -226,8 +229,9 @@ where
if shell.is_layout_invalid() {
let _ = ManuallyDrop::into_inner(manual_overlay);
- self.base = renderer.layout(
- &self.root,
+ self.base = self.root.as_widget().layout(
+ &self.state,
+ renderer,
&layout::Limits::new(Size::ZERO, self.bounds),
);
@@ -325,8 +329,9 @@ where
}
shell.revalidate_layout(|| {
- self.base = renderer.layout(
- &self.root,
+ self.base = self.root.as_widget().layout(
+ &self.state,
+ renderer,
&layout::Limits::new(Size::ZERO, self.bounds),
);
diff --git a/src/settings.rs b/src/settings.rs
index 0dd46584..794f89fd 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -1,6 +1,6 @@
//! Configure your application.
use crate::window;
-use crate::Font;
+use crate::{Font, Pixels};
/// The settings of an application.
#[derive(Debug, Clone)]
@@ -29,7 +29,7 @@ pub struct Settings<Flags> {
/// The text size that will be used by default.
///
/// The default value is `16.0`.
- pub default_text_size: f32,
+ pub default_text_size: Pixels,
/// If set to true, the renderer will try to perform antialiasing for some
/// primitives.
@@ -80,7 +80,7 @@ where
window: Default::default(),
flags: Default::default(),
default_font: Default::default(),
- default_text_size: 16.0,
+ default_text_size: Pixels(16.0),
antialiasing: false,
exit_on_close_request: true,
}
diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs
index a8add70b..ef587bac 100644
--- a/tiny_skia/src/backend.rs
+++ b/tiny_skia/src/backend.rs
@@ -1,16 +1,12 @@
-use crate::core::text;
-use crate::core::Gradient;
-use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector};
+use crate::core::{Background, Color, Gradient, Rectangle, Vector};
use crate::graphics::backend;
+use crate::graphics::text;
use crate::graphics::{Damage, Viewport};
use crate::primitive::{self, Primitive};
-use crate::Settings;
use std::borrow::Cow;
pub struct Backend {
- default_font: Font,
- default_text_size: f32,
text_pipeline: crate::text::Pipeline,
#[cfg(feature = "image")]
@@ -21,10 +17,8 @@ pub struct Backend {
}
impl Backend {
- pub fn new(settings: Settings) -> Self {
+ pub fn new() -> Self {
Self {
- default_font: settings.default_font,
- default_text_size: settings.default_text_size,
text_pipeline: crate::text::Pipeline::new(),
#[cfg(feature = "image")]
@@ -364,6 +358,32 @@ impl Backend {
}
}
}
+ Primitive::Paragraph {
+ paragraph,
+ position,
+ color,
+ } => {
+ let physical_bounds =
+ (Rectangle::new(*position, paragraph.min_bounds)
+ + translation)
+ * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw_paragraph(
+ paragraph,
+ *position + translation,
+ *color,
+ scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
Primitive::Text {
content,
bounds,
@@ -599,6 +619,12 @@ impl Backend {
}
}
+impl Default for Backend {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
fn into_color(color: Color) -> tiny_skia::Color {
tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
.expect("Convert color from iced to tiny_skia")
@@ -779,58 +805,8 @@ impl iced_graphics::Backend for Backend {
}
impl backend::Text for Backend {
- const ICON_FONT: Font = Font::with_name("Iced-Icons");
- const CHECKMARK_ICON: char = '\u{f00c}';
- const ARROW_DOWN_ICON: char = '\u{e800}';
-
- fn default_font(&self) -> Font {
- self.default_font
- }
-
- fn default_size(&self) -> f32 {
- self.default_text_size
- }
-
- fn measure(
- &self,
- contents: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- ) -> Size {
- self.text_pipeline.measure(
- contents,
- size,
- line_height,
- font,
- bounds,
- shaping,
- )
- }
-
- fn hit_test(
- &self,
- contents: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- point: Point,
- nearest_only: bool,
- ) -> Option<text::Hit> {
- self.text_pipeline.hit_test(
- contents,
- size,
- line_height,
- font,
- bounds,
- shaping,
- point,
- nearest_only,
- )
+ fn font_system(&self) -> &text::FontSystem {
+ self.text_pipeline.font_system()
}
fn load_font(&mut self, font: Cow<'static, [u8]>) {
@@ -840,7 +816,10 @@ impl backend::Text for Backend {
#[cfg(feature = "image")]
impl backend::Image for Backend {
- fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
+ fn dimensions(
+ &self,
+ handle: &crate::core::image::Handle,
+ ) -> crate::core::Size<u32> {
self.raster_pipeline.dimensions(handle)
}
}
@@ -850,7 +829,7 @@ impl backend::Svg for Backend {
fn viewport_dimensions(
&self,
handle: &crate::core::svg::Handle,
- ) -> Size<u32> {
+ ) -> crate::core::Size<u32> {
self.vector_pipeline.viewport_dimensions(handle)
}
}
diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs
index abffbfe6..ec27b218 100644
--- a/tiny_skia/src/settings.rs
+++ b/tiny_skia/src/settings.rs
@@ -1,4 +1,4 @@
-use crate::core::Font;
+use crate::core::{Font, Pixels};
/// The settings of a [`Backend`].
///
@@ -11,14 +11,14 @@ pub struct Settings {
/// The default size of text.
///
/// By default, it will be set to `16.0`.
- pub default_text_size: f32,
+ pub default_text_size: Pixels,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
default_font: Font::default(),
- default_text_size: 16.0,
+ default_text_size: Pixels(16.0),
}
}
}
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs
index 08fde4bf..e4c5ad9b 100644
--- a/tiny_skia/src/text.rs
+++ b/tiny_skia/src/text.rs
@@ -1,18 +1,19 @@
use crate::core::alignment;
-use crate::core::font::{self, Font};
-use crate::core::text::{Hit, LineHeight, Shaping};
-use crate::core::{Color, Pixels, Point, Rectangle, Size};
+use crate::core::text::{LineHeight, Shaping};
+use crate::core::{Color, Font, Pixels, Point, Rectangle};
+use crate::graphics::text::cache::{self, Cache};
+use crate::graphics::text::paragraph;
+use crate::graphics::text::FontSystem;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::hash_map;
-use std::hash::{BuildHasher, Hash, Hasher};
use std::sync::Arc;
#[allow(missing_debug_implementations)]
pub struct Pipeline {
- font_system: RefCell<cosmic_text::FontSystem>,
+ font_system: FontSystem,
glyph_cache: GlyphCache,
cache: RefCell<Cache>,
}
@@ -20,17 +21,16 @@ pub struct Pipeline {
impl Pipeline {
pub fn new() -> Self {
Pipeline {
- font_system: RefCell::new(cosmic_text::FontSystem::new_with_fonts(
- [cosmic_text::fontdb::Source::Binary(Arc::new(
- include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
- ))]
- .into_iter(),
- )),
+ font_system: FontSystem::new(),
glyph_cache: GlyphCache::new(),
cache: RefCell::new(Cache::new()),
}
}
+ pub fn font_system(&self) -> &FontSystem {
+ &self.font_system
+ }
+
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
self.font_system.get_mut().db_mut().load_font_source(
cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
@@ -39,12 +39,23 @@ impl Pipeline {
self.cache = RefCell::new(Cache::new());
}
+ pub fn draw_paragraph(
+ &mut self,
+ _paragraph: &paragraph::Weak,
+ _position: Point,
+ _color: Color,
+ _scale_factor: f32,
+ _pixels: &mut tiny_skia::PixmapMut<'_>,
+ _clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ }
+
pub fn draw(
&mut self,
content: &str,
bounds: Rectangle,
color: Color,
- size: f32,
+ size: Pixels,
line_height: LineHeight,
font: Font,
horizontal_alignment: alignment::Horizontal,
@@ -54,22 +65,22 @@ impl Pipeline {
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
) {
- let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+ let line_height = f32::from(line_height.to_absolute(size));
let font_system = self.font_system.get_mut();
- let key = Key {
+ let key = cache::Key {
bounds: bounds.size(),
content,
font,
- size,
+ size: size.into(),
line_height,
shaping,
};
- let (_, entry) = self.cache.get_mut().allocate(font_system, key);
+ let (_, buffer) = self.cache.get_mut().allocate(font_system, key);
- let max_width = entry.bounds.width * scale_factor;
- let total_height = entry.bounds.height * scale_factor;
+ let max_width = bounds.width * scale_factor;
+ let total_height = bounds.height * scale_factor;
let bounds = bounds * scale_factor;
@@ -87,7 +98,7 @@ impl Pipeline {
let mut swash = cosmic_text::SwashCache::new();
- for run in entry.buffer.layout_runs() {
+ for run in buffer.layout_runs() {
for glyph in run.glyphs {
let physical_glyph = glyph.physical((x, y), scale_factor);
@@ -122,130 +133,6 @@ impl Pipeline {
self.cache.get_mut().trim();
self.glyph_cache.trim();
}
-
- pub fn measure(
- &self,
- content: &str,
- size: f32,
- line_height: LineHeight,
- font: Font,
- bounds: Size,
- shaping: Shaping,
- ) -> Size {
- let mut measurement_cache = self.cache.borrow_mut();
-
- let line_height = f32::from(line_height.to_absolute(Pixels(size)));
-
- let (_, entry) = measurement_cache.allocate(
- &mut self.font_system.borrow_mut(),
- Key {
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- },
- );
-
- entry.bounds
- }
-
- pub fn hit_test(
- &self,
- content: &str,
- size: f32,
- line_height: LineHeight,
- font: Font,
- bounds: Size,
- shaping: Shaping,
- point: Point,
- _nearest_only: bool,
- ) -> Option<Hit> {
- let mut measurement_cache = self.cache.borrow_mut();
-
- let line_height = f32::from(line_height.to_absolute(Pixels(size)));
-
- let (_, entry) = measurement_cache.allocate(
- &mut self.font_system.borrow_mut(),
- Key {
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- },
- );
-
- let cursor = entry.buffer.hit(point.x, point.y)?;
-
- Some(Hit::CharOffset(cursor.index))
- }
-}
-
-fn measure(buffer: &cosmic_text::Buffer) -> Size {
- let (width, total_lines) = buffer
- .layout_runs()
- .fold((0.0, 0usize), |(width, total_lines), run| {
- (run.line_w.max(width), total_lines + 1)
- });
-
- Size::new(width, total_lines as f32 * buffer.metrics().line_height)
-}
-
-fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
- match family {
- font::Family::Name(name) => cosmic_text::Family::Name(name),
- font::Family::SansSerif => cosmic_text::Family::SansSerif,
- font::Family::Serif => cosmic_text::Family::Serif,
- font::Family::Cursive => cosmic_text::Family::Cursive,
- font::Family::Fantasy => cosmic_text::Family::Fantasy,
- font::Family::Monospace => cosmic_text::Family::Monospace,
- }
-}
-
-fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
- match weight {
- font::Weight::Thin => cosmic_text::Weight::THIN,
- font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
- font::Weight::Light => cosmic_text::Weight::LIGHT,
- font::Weight::Normal => cosmic_text::Weight::NORMAL,
- font::Weight::Medium => cosmic_text::Weight::MEDIUM,
- font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
- font::Weight::Bold => cosmic_text::Weight::BOLD,
- font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
- font::Weight::Black => cosmic_text::Weight::BLACK,
- }
-}
-
-fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
- match stretch {
- font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
- font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
- font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
- font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
- font::Stretch::Normal => cosmic_text::Stretch::Normal,
- font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
- font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
- font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
- font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
- }
-}
-
-fn to_style(style: font::Style) -> cosmic_text::Style {
- match style {
- font::Style::Normal => cosmic_text::Style::Normal,
- font::Style::Italic => cosmic_text::Style::Italic,
- font::Style::Oblique => cosmic_text::Style::Oblique,
- }
-}
-
-fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
- match shaping {
- Shaping::Basic => cosmic_text::Shaping::Basic,
- Shaping::Advanced => cosmic_text::Shaping::Advanced,
- }
}
#[derive(Debug, Clone, Default)]
@@ -358,135 +245,3 @@ impl GlyphCache {
}
}
}
-
-struct Cache {
- entries: FxHashMap<KeyHash, Entry>,
- measurements: FxHashMap<KeyHash, KeyHash>,
- recently_used: FxHashSet<KeyHash>,
- hasher: HashBuilder,
- trim_count: usize,
-}
-
-struct Entry {
- buffer: cosmic_text::Buffer,
- bounds: Size,
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-type HashBuilder = twox_hash::RandomXxHashBuilder64;
-
-#[cfg(target_arch = "wasm32")]
-type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
-
-impl Cache {
- const TRIM_INTERVAL: usize = 300;
-
- fn new() -> Self {
- Self {
- entries: FxHashMap::default(),
- measurements: FxHashMap::default(),
- recently_used: FxHashSet::default(),
- hasher: HashBuilder::default(),
- trim_count: 0,
- }
- }
-
- fn allocate(
- &mut self,
- font_system: &mut cosmic_text::FontSystem,
- key: Key<'_>,
- ) -> (KeyHash, &mut Entry) {
- let hash = key.hash(self.hasher.build_hasher());
-
- if let Some(hash) = self.measurements.get(&hash) {
- let _ = self.recently_used.insert(*hash);
-
- return (*hash, self.entries.get_mut(hash).unwrap());
- }
-
- if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
- let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2);
- let mut buffer = cosmic_text::Buffer::new(font_system, metrics);
-
- buffer.set_size(
- font_system,
- key.bounds.width,
- key.bounds.height.max(key.size * 1.2),
- );
- buffer.set_text(
- font_system,
- key.content,
- cosmic_text::Attrs::new()
- .family(to_family(key.font.family))
- .weight(to_weight(key.font.weight))
- .stretch(to_stretch(key.font.stretch))
- .style(to_style(key.font.style)),
- to_shaping(key.shaping),
- );
-
- let bounds = measure(&buffer);
-
- let _ = entry.insert(Entry { buffer, bounds });
-
- for bounds in [
- bounds,
- Size {
- width: key.bounds.width,
- ..bounds
- },
- ] {
- if key.bounds != bounds {
- let _ = self.measurements.insert(
- Key { bounds, ..key }.hash(self.hasher.build_hasher()),
- hash,
- );
- }
- }
- }
-
- let _ = self.recently_used.insert(hash);
-
- (hash, self.entries.get_mut(&hash).unwrap())
- }
-
- fn trim(&mut self) {
- if self.trim_count > Self::TRIM_INTERVAL {
- self.entries
- .retain(|key, _| self.recently_used.contains(key));
- self.measurements
- .retain(|_, value| self.recently_used.contains(value));
-
- self.recently_used.clear();
-
- self.trim_count = 0;
- } else {
- self.trim_count += 1;
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-struct Key<'a> {
- content: &'a str,
- size: f32,
- line_height: f32,
- font: Font,
- bounds: Size,
- shaping: Shaping,
-}
-
-impl Key<'_> {
- fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
- self.content.hash(&mut hasher);
- self.size.to_bits().hash(&mut hasher);
- self.line_height.to_bits().hash(&mut hasher);
- self.font.hash(&mut hasher);
- self.bounds.width.to_bits().hash(&mut hasher);
- self.bounds.height.to_bits().hash(&mut hasher);
- self.shaping.hash(&mut hasher);
-
- hasher.finish()
- }
-}
-
-type KeyHash = u64;
diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs
index 775cf9e5..a996fffc 100644
--- a/tiny_skia/src/window/compositor.rs
+++ b/tiny_skia/src/window/compositor.rs
@@ -28,9 +28,16 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
settings: Self::Settings,
_compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> {
- let (compositor, backend) = new(settings);
+ let (compositor, backend) = new();
- Ok((compositor, Renderer::new(backend)))
+ Ok((
+ compositor,
+ Renderer::new(
+ backend,
+ settings.default_font,
+ settings.default_text_size,
+ ),
+ ))
}
fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
@@ -113,12 +120,12 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
}
}
-pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) {
+pub fn new<Theme>() -> (Compositor<Theme>, Backend) {
(
Compositor {
_theme: PhantomData,
},
- Backend::new(settings),
+ Backend::new(),
)
}
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml
index 69568099..09740f54 100644
--- a/wgpu/Cargo.toml
+++ b/wgpu/Cargo.toml
@@ -21,20 +21,11 @@ guillotiere = "0.6"
futures = "0.3"
bitflags = "1.2"
once_cell = "1.0"
-rustc-hash = "1.1"
log = "0.4"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wgpu = { version = "0.16", features = ["webgl"] }
-[dependencies.twox-hash]
-version = "1.6"
-default-features = false
-
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
-version = "1.6.1"
-features = ["std"]
-
[dependencies.bytemuck]
version = "1.9"
features = ["derive"]
diff --git a/wgpu/fonts/Iced-Icons.ttf b/wgpu/fonts/Iced-Icons.ttf
deleted file mode 100644
index e3273141..00000000
--- a/wgpu/fonts/Iced-Icons.ttf
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs
index 68d1f805..65c63f19 100644
--- a/wgpu/src/backend.rs
+++ b/wgpu/src/backend.rs
@@ -1,5 +1,5 @@
-use crate::core;
-use crate::core::{Color, Font, Point, Size};
+use crate::core::{Color, Size};
+use crate::graphics;
use crate::graphics::backend;
use crate::graphics::color;
use crate::graphics::{Transformation, Viewport};
@@ -29,9 +29,6 @@ pub struct Backend {
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: image::Pipeline,
-
- default_font: Font,
- default_text_size: f32,
}
impl Backend {
@@ -57,9 +54,6 @@ impl Backend {
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline,
-
- default_font: settings.default_font,
- default_text_size: settings.default_text_size,
}
}
@@ -313,65 +307,11 @@ impl Backend {
impl crate::graphics::Backend for Backend {
type Primitive = primitive::Custom;
-
- fn trim_measurements(&mut self) {
- self.text_pipeline.trim_measurements();
- }
}
impl backend::Text for Backend {
- const ICON_FONT: Font = Font::with_name("Iced-Icons");
- const CHECKMARK_ICON: char = '\u{f00c}';
- const ARROW_DOWN_ICON: char = '\u{e800}';
-
- fn default_font(&self) -> Font {
- self.default_font
- }
-
- fn default_size(&self) -> f32 {
- self.default_text_size
- }
-
- fn measure(
- &self,
- contents: &str,
- size: f32,
- line_height: core::text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: core::text::Shaping,
- ) -> Size {
- self.text_pipeline.measure(
- contents,
- size,
- line_height,
- font,
- bounds,
- shaping,
- )
- }
-
- fn hit_test(
- &self,
- contents: &str,
- size: f32,
- line_height: core::text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: core::text::Shaping,
- point: Point,
- nearest_only: bool,
- ) -> Option<core::text::Hit> {
- self.text_pipeline.hit_test(
- contents,
- size,
- line_height,
- font,
- bounds,
- shaping,
- point,
- nearest_only,
- )
+ fn font_system(&self) -> &graphics::text::FontSystem {
+ self.text_pipeline.font_system()
}
fn load_font(&mut self, font: Cow<'static, [u8]>) {
@@ -381,14 +321,17 @@ impl backend::Text for Backend {
#[cfg(feature = "image")]
impl backend::Image for Backend {
- fn dimensions(&self, handle: &core::image::Handle) -> Size<u32> {
+ fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
self.image_pipeline.dimensions(handle)
}
}
#[cfg(feature = "svg")]
impl backend::Svg for Backend {
- fn viewport_dimensions(&self, handle: &core::svg::Handle) -> Size<u32> {
+ fn viewport_dimensions(
+ &self,
+ handle: &crate::core::svg::Handle,
+ ) -> Size<u32> {
self.image_pipeline.viewport_dimensions(handle)
}
}
diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs
index b8f32db1..7a5a0f7c 100644
--- a/wgpu/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -10,7 +10,7 @@ pub use text::Text;
use crate::core;
use crate::core::alignment;
-use crate::core::{Color, Font, Point, Rectangle, Size, Vector};
+use crate::core::{Color, Font, Pixels, Point, Rectangle, Size, Vector};
use crate::graphics;
use crate::graphics::color;
use crate::graphics::Viewport;
@@ -56,14 +56,14 @@ impl<'a> Layer<'a> {
Layer::new(Rectangle::with_size(viewport.logical_size()));
for (i, line) in lines.iter().enumerate() {
- let text = Text {
+ let text = text::Cached {
content: line.as_ref(),
bounds: Rectangle::new(
Point::new(11.0, 11.0 + 25.0 * i as f32),
Size::INFINITY,
),
color: Color::new(0.9, 0.9, 0.9, 1.0),
- size: 20.0,
+ size: Pixels(20.0),
line_height: core::text::LineHeight::default(),
font: Font::MONOSPACE,
horizontal_alignment: alignment::Horizontal::Left,
@@ -71,13 +71,13 @@ impl<'a> Layer<'a> {
shaping: core::text::Shaping::Basic,
};
- overlay.text.push(text);
+ overlay.text.push(Text::Cached(text.clone()));
- overlay.text.push(Text {
+ overlay.text.push(Text::Cached(text::Cached {
bounds: text.bounds + Vector::new(-1.0, -1.0),
color: Color::BLACK,
..text
- });
+ }));
}
overlay
@@ -113,6 +113,19 @@ impl<'a> Layer<'a> {
current_layer: usize,
) {
match primitive {
+ Primitive::Paragraph {
+ paragraph,
+ position,
+ color,
+ } => {
+ let layer = &mut layers[current_layer];
+
+ layer.text.push(Text::Managed {
+ paragraph: paragraph.clone(),
+ position: *position + translation,
+ color: *color,
+ });
+ }
Primitive::Text {
content,
bounds,
@@ -126,7 +139,7 @@ impl<'a> Layer<'a> {
} => {
let layer = &mut layers[current_layer];
- layer.text.push(Text {
+ layer.text.push(Text::Cached(text::Cached {
content,
bounds: *bounds + translation,
size: *size,
@@ -136,7 +149,7 @@ impl<'a> Layer<'a> {
horizontal_alignment: *horizontal_alignment,
vertical_alignment: *vertical_alignment,
shaping: *shaping,
- });
+ }));
}
Primitive::Quad {
bounds,
diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs
index ba1bdca8..b61615d6 100644
--- a/wgpu/src/layer/text.rs
+++ b/wgpu/src/layer/text.rs
@@ -1,10 +1,21 @@
use crate::core::alignment;
use crate::core::text;
-use crate::core::{Color, Font, Rectangle};
+use crate::core::{Color, Font, Pixels, Point, Rectangle};
+use crate::graphics::text::paragraph;
/// A paragraph of text.
-#[derive(Debug, Clone, Copy)]
-pub struct Text<'a> {
+#[derive(Debug, Clone)]
+pub enum Text<'a> {
+ Managed {
+ paragraph: paragraph::Weak,
+ position: Point,
+ color: Color,
+ },
+ Cached(Cached<'a>),
+}
+
+#[derive(Debug, Clone)]
+pub struct Cached<'a> {
/// The content of the [`Text`].
pub content: &'a str,
@@ -15,7 +26,7 @@ pub struct Text<'a> {
pub color: Color,
/// The size of the [`Text`] in logical pixels.
- pub size: f32,
+ pub size: Pixels,
/// The line height of the [`Text`].
pub line_height: text::LineHeight,
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index d1e4b7af..cd457072 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -22,7 +22,7 @@
)]
#![deny(
missing_debug_implementations,
- missing_docs,
+ //missing_docs,
unsafe_code,
unused_results,
clippy::extra_unused_lifetimes,
diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs
index 266a2c87..c9338fec 100644
--- a/wgpu/src/settings.rs
+++ b/wgpu/src/settings.rs
@@ -1,5 +1,5 @@
//! Configure a renderer.
-use crate::core::Font;
+use crate::core::{Font, Pixels};
use crate::graphics::Antialiasing;
/// The settings of a [`Backend`].
@@ -21,7 +21,7 @@ pub struct Settings {
/// The default size of text.
///
/// By default, it will be set to `16.0`.
- pub default_text_size: f32,
+ pub default_text_size: Pixels,
/// The antialiasing strategy that will be used for triangle primitives.
///
@@ -59,7 +59,7 @@ impl Default for Settings {
present_mode: wgpu::PresentMode::AutoVsync,
internal_backend: wgpu::Backends::all(),
default_font: Font::default(),
- default_text_size: 16.0,
+ default_text_size: Pixels(16.0),
antialiasing: None,
}
}
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index fb13545d..da2062fe 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -1,20 +1,17 @@
use crate::core::alignment;
-use crate::core::font::{self, Font};
-use crate::core::text::{Hit, LineHeight, Shaping};
-use crate::core::{Pixels, Point, Rectangle, Size};
+use crate::core::{Rectangle, Size};
use crate::graphics::color;
+use crate::graphics::text::cache::{self, Cache};
+use crate::graphics::text::{FontSystem, Paragraph};
use crate::layer::Text;
-use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
use std::cell::RefCell;
-use std::collections::hash_map;
-use std::hash::{BuildHasher, Hash, Hasher};
use std::sync::Arc;
#[allow(missing_debug_implementations)]
pub struct Pipeline {
- font_system: RefCell<glyphon::FontSystem>,
+ font_system: FontSystem,
renderers: Vec<glyphon::TextRenderer>,
atlas: glyphon::TextAtlas,
prepare_layer: usize,
@@ -28,12 +25,7 @@ impl Pipeline {
format: wgpu::TextureFormat,
) -> Self {
Pipeline {
- font_system: RefCell::new(glyphon::FontSystem::new_with_fonts(
- [glyphon::fontdb::Source::Binary(Arc::new(
- include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
- ))]
- .into_iter(),
- )),
+ font_system: FontSystem::new(),
renderers: Vec::new(),
atlas: glyphon::TextAtlas::with_color_mode(
device,
@@ -50,6 +42,10 @@ impl Pipeline {
}
}
+ pub fn font_system(&self) -> &FontSystem {
+ &self.font_system
+ }
+
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
let _ = self.font_system.get_mut().db_mut().load_font_source(
glyphon::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
@@ -80,97 +76,137 @@ impl Pipeline {
let renderer = &mut self.renderers[self.prepare_layer];
let cache = self.cache.get_mut();
- if self.prepare_layer == 0 {
- cache.trim(Purpose::Drawing);
+ enum Allocation {
+ Paragraph(Paragraph),
+ Cache(cache::KeyHash),
}
- let keys: Vec<_> = sections
+ let allocations: Vec<_> = sections
.iter()
- .map(|section| {
- let (key, _) = cache.allocate(
- font_system,
- Key {
- content: section.content,
- size: section.size,
- line_height: f32::from(
- section
- .line_height
- .to_absolute(Pixels(section.size)),
- ),
- font: section.font,
- bounds: Size {
- width: section.bounds.width,
- height: section.bounds.height,
+ .map(|section| match section {
+ Text::Managed { paragraph, .. } => {
+ paragraph.upgrade().map(Allocation::Paragraph)
+ }
+ Text::Cached(text) => {
+ let (key, _) = cache.allocate(
+ font_system,
+ cache::Key {
+ content: text.content,
+ size: text.size.into(),
+ line_height: f32::from(
+ text.line_height.to_absolute(text.size),
+ ),
+ font: text.font,
+ bounds: Size {
+ width: bounds.width,
+ height: bounds.height,
+ },
+ shaping: text.shaping,
},
- shaping: section.shaping,
- },
- Purpose::Drawing,
- );
+ );
- key
+ Some(Allocation::Cache(key))
+ }
})
.collect();
- let bounds = bounds * scale_factor;
-
- let text_areas =
- sections
- .iter()
- .zip(keys.iter())
- .filter_map(|(section, key)| {
- let entry = cache.get(key).expect("Get cached buffer");
-
- let x = section.bounds.x * scale_factor;
- let y = section.bounds.y * scale_factor;
-
- let max_width = entry.bounds.width * scale_factor;
- let total_height = entry.bounds.height * scale_factor;
-
- let left = match section.horizontal_alignment {
- alignment::Horizontal::Left => x,
- alignment::Horizontal::Center => x - max_width / 2.0,
- alignment::Horizontal::Right => x - max_width,
- };
-
- let top = match section.vertical_alignment {
- alignment::Vertical::Top => y,
- alignment::Vertical::Center => y - total_height / 2.0,
- alignment::Vertical::Bottom => y - total_height,
- };
-
- let section_bounds = Rectangle {
- x: left,
- y: top,
- width: section.bounds.width * scale_factor,
- height: section.bounds.height * scale_factor,
- };
-
- let clip_bounds = bounds.intersection(&section_bounds)?;
-
- Some(glyphon::TextArea {
- buffer: &entry.buffer,
- left,
- top,
- scale: scale_factor,
- bounds: glyphon::TextBounds {
- left: clip_bounds.x as i32,
- top: clip_bounds.y as i32,
- right: (clip_bounds.x + clip_bounds.width) as i32,
- bottom: (clip_bounds.y + clip_bounds.height) as i32,
- },
- default_color: {
- let [r, g, b, a] =
- color::pack(section.color).components();
-
- glyphon::Color::rgba(
- (r * 255.0) as u8,
- (g * 255.0) as u8,
- (b * 255.0) as u8,
- (a * 255.0) as u8,
- )
- },
- })
- });
+ let layer_bounds = bounds * scale_factor;
+
+ let text_areas = sections.iter().zip(allocations.iter()).filter_map(
+ |(section, allocation)| {
+ let (
+ buffer,
+ bounds,
+ horizontal_alignment,
+ vertical_alignment,
+ color,
+ ) = match section {
+ Text::Managed {
+ position, color, ..
+ } => {
+ use crate::core::text::Paragraph as _;
+
+ let Some(Allocation::Paragraph(paragraph)) = allocation
+ else {
+ return None;
+ };
+
+ (
+ paragraph.buffer(),
+ Rectangle::new(*position, paragraph.min_bounds()),
+ paragraph.horizontal_alignment(),
+ paragraph.vertical_alignment(),
+ *color,
+ )
+ }
+ Text::Cached(text) => {
+ let Some(Allocation::Cache(key)) = allocation else {
+ return None;
+ };
+
+ let buffer = cache.get(key).expect("Get cached buffer");
+
+ (
+ buffer,
+ text.bounds,
+ text.horizontal_alignment,
+ text.vertical_alignment,
+ text.color,
+ )
+ }
+ };
+
+ let x = bounds.x * scale_factor;
+ let y = bounds.y * scale_factor;
+
+ let max_width = bounds.width * scale_factor;
+ let total_height = bounds.height * scale_factor;
+
+ let left = match horizontal_alignment {
+ alignment::Horizontal::Left => x,
+ alignment::Horizontal::Center => x - max_width / 2.0,
+ alignment::Horizontal::Right => x - max_width,
+ };
+
+ let top = match vertical_alignment {
+ alignment::Vertical::Top => y,
+ alignment::Vertical::Center => y - total_height / 2.0,
+ alignment::Vertical::Bottom => y - total_height,
+ };
+
+ let section_bounds = Rectangle {
+ x: left,
+ y: top,
+ width: max_width,
+ height: total_height,
+ };
+
+ let clip_bounds = layer_bounds.intersection(&section_bounds)?;
+
+ Some(glyphon::TextArea {
+ buffer,
+ left,
+ top,
+ scale: scale_factor,
+ bounds: glyphon::TextBounds {
+ left: clip_bounds.x as i32,
+ top: clip_bounds.y as i32,
+ right: (clip_bounds.x + clip_bounds.width) as i32,
+ bottom: (clip_bounds.y + clip_bounds.height) as i32,
+ },
+ default_color: {
+ let [r, g, b, a] = color::pack(color).components();
+
+ glyphon::Color::rgba(
+ (r * 255.0) as u8,
+ (g * 255.0) as u8,
+ (b * 255.0) as u8,
+ (a * 255.0) as u8,
+ )
+ },
+ })
+ },
+ );
let result = renderer.prepare(
device,
@@ -219,287 +255,8 @@ impl Pipeline {
pub fn end_frame(&mut self) {
self.atlas.trim();
+ self.cache.get_mut().trim();
self.prepare_layer = 0;
}
-
- pub fn trim_measurements(&mut self) {
- self.cache.get_mut().trim(Purpose::Measuring);
- }
-
- pub fn measure(
- &self,
- content: &str,
- size: f32,
- line_height: LineHeight,
- font: Font,
- bounds: Size,
- shaping: Shaping,
- ) -> Size {
- let mut cache = self.cache.borrow_mut();
-
- let line_height = f32::from(line_height.to_absolute(Pixels(size)));
-
- let (_, entry) = cache.allocate(
- &mut self.font_system.borrow_mut(),
- Key {
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- },
- Purpose::Measuring,
- );
-
- entry.bounds
- }
-
- pub fn hit_test(
- &self,
- content: &str,
- size: f32,
- line_height: LineHeight,
- font: Font,
- bounds: Size,
- shaping: Shaping,
- point: Point,
- _nearest_only: bool,
- ) -> Option<Hit> {
- let mut cache = self.cache.borrow_mut();
-
- let line_height = f32::from(line_height.to_absolute(Pixels(size)));
-
- let (_, entry) = cache.allocate(
- &mut self.font_system.borrow_mut(),
- Key {
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- },
- Purpose::Measuring,
- );
-
- let cursor = entry.buffer.hit(point.x, point.y)?;
-
- Some(Hit::CharOffset(cursor.index))
- }
}
-
-fn measure(buffer: &glyphon::Buffer) -> Size {
- let (width, total_lines) = buffer
- .layout_runs()
- .fold((0.0, 0usize), |(width, total_lines), run| {
- (run.line_w.max(width), total_lines + 1)
- });
-
- Size::new(width, total_lines as f32 * buffer.metrics().line_height)
-}
-
-fn to_family(family: font::Family) -> glyphon::Family<'static> {
- match family {
- font::Family::Name(name) => glyphon::Family::Name(name),
- font::Family::SansSerif => glyphon::Family::SansSerif,
- font::Family::Serif => glyphon::Family::Serif,
- font::Family::Cursive => glyphon::Family::Cursive,
- font::Family::Fantasy => glyphon::Family::Fantasy,
- font::Family::Monospace => glyphon::Family::Monospace,
- }
-}
-
-fn to_weight(weight: font::Weight) -> glyphon::Weight {
- match weight {
- font::Weight::Thin => glyphon::Weight::THIN,
- font::Weight::ExtraLight => glyphon::Weight::EXTRA_LIGHT,
- font::Weight::Light => glyphon::Weight::LIGHT,
- font::Weight::Normal => glyphon::Weight::NORMAL,
- font::Weight::Medium => glyphon::Weight::MEDIUM,
- font::Weight::Semibold => glyphon::Weight::SEMIBOLD,
- font::Weight::Bold => glyphon::Weight::BOLD,
- font::Weight::ExtraBold => glyphon::Weight::EXTRA_BOLD,
- font::Weight::Black => glyphon::Weight::BLACK,
- }
-}
-
-fn to_stretch(stretch: font::Stretch) -> glyphon::Stretch {
- match stretch {
- font::Stretch::UltraCondensed => glyphon::Stretch::UltraCondensed,
- font::Stretch::ExtraCondensed => glyphon::Stretch::ExtraCondensed,
- font::Stretch::Condensed => glyphon::Stretch::Condensed,
- font::Stretch::SemiCondensed => glyphon::Stretch::SemiCondensed,
- font::Stretch::Normal => glyphon::Stretch::Normal,
- font::Stretch::SemiExpanded => glyphon::Stretch::SemiExpanded,
- font::Stretch::Expanded => glyphon::Stretch::Expanded,
- font::Stretch::ExtraExpanded => glyphon::Stretch::ExtraExpanded,
- font::Stretch::UltraExpanded => glyphon::Stretch::UltraExpanded,
- }
-}
-
-fn to_style(style: font::Style) -> glyphon::Style {
- match style {
- font::Style::Normal => glyphon::Style::Normal,
- font::Style::Italic => glyphon::Style::Italic,
- font::Style::Oblique => glyphon::Style::Oblique,
- }
-}
-
-fn to_shaping(shaping: Shaping) -> glyphon::Shaping {
- match shaping {
- Shaping::Basic => glyphon::Shaping::Basic,
- Shaping::Advanced => glyphon::Shaping::Advanced,
- }
-}
-
-struct Cache {
- entries: FxHashMap<KeyHash, Entry>,
- aliases: FxHashMap<KeyHash, KeyHash>,
- recently_measured: FxHashSet<KeyHash>,
- recently_drawn: FxHashSet<KeyHash>,
- hasher: HashBuilder,
-}
-
-struct Entry {
- buffer: glyphon::Buffer,
- bounds: Size,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum Purpose {
- Measuring,
- Drawing,
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-type HashBuilder = twox_hash::RandomXxHashBuilder64;
-
-#[cfg(target_arch = "wasm32")]
-type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
-
-impl Cache {
- fn new() -> Self {
- Self {
- entries: FxHashMap::default(),
- aliases: FxHashMap::default(),
- recently_measured: FxHashSet::default(),
- recently_drawn: FxHashSet::default(),
- hasher: HashBuilder::default(),
- }
- }
-
- fn get(&self, key: &KeyHash) -> Option<&Entry> {
- self.entries.get(key)
- }
-
- fn allocate(
- &mut self,
- font_system: &mut glyphon::FontSystem,
- key: Key<'_>,
- purpose: Purpose,
- ) -> (KeyHash, &mut Entry) {
- let hash = key.hash(self.hasher.build_hasher());
-
- let recently_used = match purpose {
- Purpose::Measuring => &mut self.recently_measured,
- Purpose::Drawing => &mut self.recently_drawn,
- };
-
- if let Some(hash) = self.aliases.get(&hash) {
- let _ = recently_used.insert(*hash);
-
- return (*hash, self.entries.get_mut(hash).unwrap());
- }
-
- if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
- let metrics = glyphon::Metrics::new(key.size, key.line_height);
- let mut buffer = glyphon::Buffer::new(font_system, metrics);
-
- buffer.set_size(
- font_system,
- key.bounds.width,
- key.bounds.height.max(key.line_height),
- );
- buffer.set_text(
- font_system,
- key.content,
- glyphon::Attrs::new()
- .family(to_family(key.font.family))
- .weight(to_weight(key.font.weight))
- .stretch(to_stretch(key.font.stretch))
- .style(to_style(key.font.style)),
- to_shaping(key.shaping),
- );
-
- let bounds = measure(&buffer);
- let _ = entry.insert(Entry { buffer, bounds });
-
- for bounds in [
- bounds,
- Size {
- width: key.bounds.width,
- ..bounds
- },
- ] {
- if key.bounds != bounds {
- let _ = self.aliases.insert(
- Key { bounds, ..key }.hash(self.hasher.build_hasher()),
- hash,
- );
- }
- }
- }
-
- let _ = recently_used.insert(hash);
-
- (hash, self.entries.get_mut(&hash).unwrap())
- }
-
- fn trim(&mut self, purpose: Purpose) {
- self.entries.retain(|key, _| {
- self.recently_measured.contains(key)
- || self.recently_drawn.contains(key)
- });
- self.aliases.retain(|_, value| {
- self.recently_measured.contains(value)
- || self.recently_drawn.contains(value)
- });
-
- match purpose {
- Purpose::Measuring => {
- self.recently_measured.clear();
- }
- Purpose::Drawing => {
- self.recently_drawn.clear();
- }
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-struct Key<'a> {
- content: &'a str,
- size: f32,
- line_height: f32,
- font: Font,
- bounds: Size,
- shaping: Shaping,
-}
-
-impl Key<'_> {
- fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
- self.content.hash(&mut hasher);
- self.size.to_bits().hash(&mut hasher);
- self.line_height.to_bits().hash(&mut hasher);
- self.font.hash(&mut hasher);
- self.bounds.width.to_bits().hash(&mut hasher);
- self.bounds.height.to_bits().hash(&mut hasher);
- self.shaping.hash(&mut hasher);
-
- hasher.finish()
- }
-}
-
-type KeyHash = u64;
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
index cd5b20cc..09e11fdc 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -216,7 +216,14 @@ impl<Theme> graphics::Compositor for Compositor<Theme> {
) -> Result<(Self, Self::Renderer), Error> {
let (compositor, backend) = new(settings, compatible_window)?;
- Ok((compositor, Renderer::new(backend)))
+ Ok((
+ compositor,
+ Renderer::new(
+ backend,
+ settings.default_font,
+ settings.default_text_size,
+ ),
+ ))
}
fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 5727c631..1788b6c4 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -159,19 +159,15 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- layout(
- renderer,
- limits,
- self.width,
- self.height,
- self.padding,
- |renderer, limits| {
- self.content.as_widget().layout(renderer, limits)
- },
- )
+ layout(limits, self.width, self.height, self.padding, |limits| {
+ self.content
+ .as_widget()
+ .layout(&tree.children[0], renderer, limits)
+ })
}
fn operate(
@@ -426,17 +422,16 @@ where
}
/// Computes the layout of a [`Button`].
-pub fn layout<Renderer>(
- renderer: &Renderer,
+pub fn layout(
limits: &layout::Limits,
width: Length,
height: Length,
padding: Padding,
- layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
+ layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits.width(width).height(height);
- let mut content = layout_content(renderer, &limits.pad(padding));
+ let mut content = layout_content(&limits.pad(padding));
let padding = padding.fit(content.size(), limits.max());
let size = limits.pad(padding).resolve(content.size()).pad(padding);
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 1a186432..d749355b 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -129,6 +129,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index 310a67ed..a66ce3ff 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -6,12 +6,11 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
-use crate::core::widget::Tree;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell,
- Widget,
+ Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget,
};
-use crate::{Row, Text};
pub use iced_style::checkbox::{Appearance, StyleSheet};
@@ -45,7 +44,7 @@ where
width: Length,
size: f32,
spacing: f32,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -118,7 +117,7 @@ where
/// Sets the text size of the [`Checkbox`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
- self.text_size = Some(text_size.into().0);
+ self.text_size = Some(text_size.into());
self
}
@@ -167,6 +166,14 @@ where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
+ }
+
fn width(&self) -> Length {
self.width
}
@@ -177,26 +184,35 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- Row::<(), Renderer>::new()
- .width(self.width)
- .spacing(self.spacing)
- .align_items(Alignment::Center)
- .push(Row::new().width(self.size).height(self.size))
- .push(
- Text::new(&self.label)
- .font(self.font.unwrap_or_else(|| renderer.default_font()))
- .width(self.width)
- .size(
- self.text_size
- .unwrap_or_else(|| renderer.default_size()),
- )
- .line_height(self.text_line_height)
- .shaping(self.text_shaping),
- )
- .layout(renderer, limits)
+ layout::next_to_each_other(
+ &limits.width(self.width),
+ self.spacing,
+ |_| layout::Node::new(Size::new(self.size, self.size)),
+ |limits| {
+ let state = tree
+ .state
+ .downcast_ref::<widget::text::State<Renderer::Paragraph>>();
+
+ widget::text::layout(
+ state,
+ renderer,
+ limits,
+ self.width,
+ Length::Shrink,
+ &self.label,
+ self.text_line_height,
+ self.text_size,
+ self.font,
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ self.text_shaping,
+ )
+ },
+ )
}
fn on_event(
@@ -244,7 +260,7 @@ where
fn draw(
&self,
- _tree: &Tree,
+ tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
@@ -283,24 +299,23 @@ where
line_height,
shaping,
} = &self.icon;
- let size = size.unwrap_or(bounds.height * 0.7);
+ let size = size.unwrap_or(Pixels(bounds.height * 0.7));
if self.is_checked {
- renderer.fill_text(text::Text {
- content: &code_point.to_string(),
- font: *font,
- size,
- line_height: *line_height,
- bounds: Rectangle {
- x: bounds.center_x(),
- y: bounds.center_y(),
- ..bounds
+ renderer.fill_text(
+ text::Text {
+ content: &code_point.to_string(),
+ font: *font,
+ size,
+ line_height: *line_height,
+ bounds: bounds.size(),
+ horizontal_alignment: alignment::Horizontal::Center,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: *shaping,
},
- color: custom_style.icon_color,
- horizontal_alignment: alignment::Horizontal::Center,
- vertical_alignment: alignment::Vertical::Center,
- shaping: *shaping,
- });
+ bounds.center(),
+ custom_style.icon_color,
+ );
}
}
@@ -311,16 +326,10 @@ where
renderer,
style,
label_layout,
- &self.label,
- self.text_size,
- self.text_line_height,
- self.font,
+ tree.state.downcast_ref(),
crate::text::Appearance {
color: custom_style.text_color,
},
- alignment::Horizontal::Left,
- alignment::Vertical::Center,
- self.text_shaping,
);
}
}
@@ -348,7 +357,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// Font size of the content.
- pub size: Option<f32>,
+ pub size: Option<Pixels>,
/// The line height of the icon.
pub line_height: text::LineHeight,
/// The shaping strategy of the icon.
diff --git a/widget/src/column.rs b/widget/src/column.rs
index c16477f3..107c3475 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -122,6 +122,7 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -138,6 +139,7 @@ where
self.spacing,
self.align_items,
&self.children,
+ &tree.children,
)
}
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 690ef27c..8c20ae8e 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -144,11 +144,6 @@ where
self
}
- /// Returns whether the [`ComboBox`] is currently focused or not.
- pub fn is_focused(&self) -> bool {
- self.state.is_focused()
- }
-
/// Sets the text sixe of the [`ComboBox`].
pub fn size(mut self, size: f32) -> Self {
self.text_input = self.text_input.size(size);
@@ -179,7 +174,6 @@ pub struct State<T>(RefCell<Inner<T>>);
#[derive(Debug, Clone)]
struct Inner<T> {
- text_input: text_input::State,
value: String,
options: Vec<T>,
option_matchers: Vec<String>,
@@ -216,7 +210,6 @@ where
);
Self(RefCell::new(Inner {
- text_input: text_input::State::new(),
value,
options,
option_matchers,
@@ -224,51 +217,12 @@ where
}))
}
- /// Focuses the [`ComboBox`].
- pub fn focused(self) -> Self {
- self.focus();
- self
- }
-
- /// Focuses the [`ComboBox`].
- pub fn focus(&self) {
- let mut inner = self.0.borrow_mut();
-
- inner.text_input.focus();
- }
-
- /// Unfocuses the [`ComboBox`].
- pub fn unfocus(&self) {
- let mut inner = self.0.borrow_mut();
-
- inner.text_input.unfocus();
- }
-
- /// Returns whether the [`ComboBox`] is currently focused or not.
- pub fn is_focused(&self) -> bool {
- let inner = self.0.borrow();
-
- inner.text_input.is_focused()
- }
-
fn value(&self) -> String {
let inner = self.0.borrow();
inner.value.clone()
}
- fn text_input_tree(&self) -> widget::Tree {
- let inner = self.0.borrow();
-
- inner.text_input_tree()
- }
-
- fn update_text_input(&self, tree: widget::Tree) {
- let mut inner = self.0.borrow_mut();
-
- inner.update_text_input(tree)
- }
-
fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
let inner = self.0.borrow();
@@ -288,21 +242,6 @@ where
}
}
-impl<T> Inner<T> {
- fn text_input_tree(&self) -> widget::Tree {
- widget::Tree {
- tag: widget::tree::Tag::of::<text_input::State>(),
- state: widget::tree::State::new(self.text_input.clone()),
- children: vec![],
- }
- }
-
- fn update_text_input(&mut self, tree: widget::Tree) {
- self.text_input =
- tree.state.downcast_ref::<text_input::State>().clone();
- }
-}
-
impl<T> Filtered<T>
where
T: Clone,
@@ -366,10 +305,11 @@ where
fn layout(
&self,
+ tree: &widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.text_input.layout(renderer, limits)
+ self.text_input.layout(tree, renderer, limits)
}
fn tag(&self) -> widget::tree::Tag {
@@ -385,6 +325,10 @@ where
})
}
+ fn children(&self) -> Vec<widget::Tree> {
+ vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _>)]
+ }
+
fn on_event(
&mut self,
tree: &mut widget::Tree,
@@ -398,7 +342,13 @@ where
) -> event::Status {
let menu = tree.state.downcast_mut::<Menu<T>>();
- let started_focused = self.state.is_focused();
+ let started_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
// This is intended to check whether or not the message buffer was empty,
// since `Shell` does not expose such functionality.
let mut published_message_to_shell = false;
@@ -408,9 +358,8 @@ where
let mut local_shell = Shell::new(&mut local_messages);
// Provide it to the widget
- let mut tree = self.state.text_input_tree();
let mut event_status = self.text_input.on_event(
- &mut tree,
+ &mut tree.children[0],
event.clone(),
layout,
cursor,
@@ -419,7 +368,6 @@ where
&mut local_shell,
viewport,
);
- self.state.update_text_input(tree);
// Then finally react to them here
for message in local_messages {
@@ -450,7 +398,15 @@ where
shell.invalidate_layout();
}
- if self.state.is_focused() {
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ if is_focused {
self.state.with_inner(|state| {
if !started_focused {
if let Some(on_option_hovered) = &mut self.on_option_hovered
@@ -589,9 +545,8 @@ where
published_message_to_shell = true;
// Unfocus the input
- let mut tree = state.text_input_tree();
let _ = self.text_input.on_event(
- &mut tree,
+ &mut tree.children[0],
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)),
@@ -602,21 +557,25 @@ where
&mut Shell::new(&mut vec![]),
viewport,
);
- state.update_text_input(tree);
}
});
- if started_focused
- && !self.state.is_focused()
- && !published_message_to_shell
- {
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ if started_focused && !is_focused && !published_message_to_shell {
if let Some(message) = self.on_close.take() {
shell.publish(message);
}
}
// Focus changed, invalidate widget tree to force a fresh `view`
- if started_focused != self.state.is_focused() {
+ if started_focused != is_focused {
shell.invalidate_widgets();
}
@@ -625,20 +584,24 @@ where
fn mouse_interaction(
&self,
- _tree: &widget::Tree,
+ tree: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- let tree = self.state.text_input_tree();
- self.text_input
- .mouse_interaction(&tree, layout, cursor, viewport, renderer)
+ self.text_input.mouse_interaction(
+ &tree.children[0],
+ layout,
+ cursor,
+ viewport,
+ renderer,
+ )
}
fn draw(
&self,
- _tree: &widget::Tree,
+ tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
@@ -646,16 +609,28 @@ where
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- let selection = if self.state.is_focused() || self.selection.is_empty()
- {
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ let selection = if is_focused || self.selection.is_empty() {
None
} else {
Some(&self.selection)
};
- let tree = self.state.text_input_tree();
- self.text_input
- .draw(&tree, renderer, theme, layout, cursor, selection);
+ self.text_input.draw(
+ &tree.children[0],
+ renderer,
+ theme,
+ layout,
+ cursor,
+ selection,
+ );
}
fn overlay<'b>(
@@ -664,14 +639,22 @@ where
layout: Layout<'_>,
_renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
- let Menu {
- menu,
- filtered_options,
- hovered_option,
- ..
- } = tree.state.downcast_mut::<Menu<T>>();
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ if is_focused {
+ let Menu {
+ menu,
+ filtered_options,
+ hovered_option,
+ ..
+ } = tree.state.downcast_mut::<Menu<T>>();
- if self.state.is_focused() {
let bounds = layout.bounds();
self.state.sync_filtered_options(filtered_options);
diff --git a/widget/src/container.rs b/widget/src/container.rs
index 1f1df861..c16c1117 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -5,7 +5,8 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
-use crate::core::widget::{self, Operation, Tree};
+use crate::core::widget::tree::{self, Tree};
+use crate::core::widget::{self, Operation};
use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
Point, Rectangle, Shell, Size, Vector, Widget,
@@ -135,12 +136,20 @@ where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ self.content.as_widget().tag()
+ }
+
+ fn state(&self) -> tree::State {
+ self.content.as_widget().state()
+ }
+
fn children(&self) -> Vec<Tree> {
- vec![Tree::new(&self.content)]
+ self.content.as_widget().children()
}
fn diff(&self, tree: &mut Tree) {
- tree.diff_children(std::slice::from_ref(&self.content))
+ self.content.as_widget().diff(tree);
}
fn width(&self) -> Length {
@@ -153,11 +162,11 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
- renderer,
limits,
self.width,
self.height,
@@ -166,9 +175,7 @@ where
self.padding,
self.horizontal_alignment,
self.vertical_alignment,
- |renderer, limits| {
- self.content.as_widget().layout(renderer, limits)
- },
+ |limits| self.content.as_widget().layout(tree, renderer, limits),
)
}
@@ -184,7 +191,7 @@ where
layout.bounds(),
&mut |operation| {
self.content.as_widget().operate(
- &mut tree.children[0],
+ tree,
layout.children().next().unwrap(),
renderer,
operation,
@@ -205,7 +212,7 @@ where
viewport: &Rectangle,
) -> event::Status {
self.content.as_widget_mut().on_event(
- &mut tree.children[0],
+ tree,
event,
layout.children().next().unwrap(),
cursor,
@@ -225,7 +232,7 @@ where
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
- &tree.children[0],
+ tree,
layout.children().next().unwrap(),
cursor,
viewport,
@@ -248,7 +255,7 @@ where
draw_background(renderer, &style, layout.bounds());
self.content.as_widget().draw(
- &tree.children[0],
+ tree,
renderer,
theme,
&renderer::Style {
@@ -269,7 +276,7 @@ where
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
self.content.as_widget_mut().overlay(
- &mut tree.children[0],
+ tree,
layout.children().next().unwrap(),
renderer,
)
@@ -291,8 +298,7 @@ where
}
/// Computes the layout of a [`Container`].
-pub fn layout<Renderer>(
- renderer: &Renderer,
+pub fn layout(
limits: &layout::Limits,
width: Length,
height: Length,
@@ -301,7 +307,7 @@ pub fn layout<Renderer>(
padding: Padding,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
- layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
+ layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits
.loose()
@@ -310,7 +316,7 @@ pub fn layout<Renderer>(
.width(width)
.height(height);
- let mut content = layout_content(renderer, &limits.pad(padding).loose());
+ let mut content = layout_content(&limits.pad(padding).loose());
let padding = padding.fit(content.size(), limits.max());
let size = limits.pad(padding).resolve(content.size());
diff --git a/widget/src/image.rs b/widget/src/image.rs
index 66bf2156..f73ee5d7 100644
--- a/widget/src/image.rs
+++ b/widget/src/image.rs
@@ -167,6 +167,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index 6e095667..1f52bf2f 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -105,6 +105,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index 761f45ad..412254f5 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -152,11 +152,12 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.with_element(|element| {
- element.as_widget().layout(renderer, limits)
+ element.as_widget().layout(tree, renderer, limits)
})
}
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index 19df2792..9b3b13b2 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -254,11 +254,12 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.with_element(|element| {
- element.as_widget().layout(renderer, limits)
+ element.as_widget().layout(tree, renderer, limits)
})
}
diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs
index b56545c8..5ab8ed1a 100644
--- a/widget/src/lazy/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -60,13 +60,13 @@ impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where
Renderer: core::Renderer,
{
- fn layout(&mut self, renderer: &Renderer) {
+ fn layout(&mut self, tree: &Tree, renderer: &Renderer) {
if self.layout.is_none() {
- self.layout =
- Some(self.element.as_widget().layout(
- renderer,
- &layout::Limits::new(Size::ZERO, self.size),
- ));
+ self.layout = Some(self.element.as_widget().layout(
+ tree,
+ renderer,
+ &layout::Limits::new(Size::ZERO, self.size),
+ ));
}
}
@@ -104,7 +104,7 @@ where
R: Deref<Target = Renderer>,
{
self.update(tree, layout.bounds().size(), view);
- self.layout(renderer.deref());
+ self.layout(tree, renderer.deref());
let content_layout = Layout::with_offset(
layout.position() - Point::ORIGIN,
@@ -144,6 +144,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -285,7 +286,7 @@ where
overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>,
tree| {
content.update(tree, layout.bounds().size(), &self.view);
- content.layout(renderer);
+ content.layout(tree, renderer);
let Content {
element,
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 490f7c48..95b45b02 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -120,10 +120,11 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.content.as_widget().layout(renderer, limits)
+ self.content.as_widget().layout(tree, renderer, limits)
}
fn operate(
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index f7bdeef6..71703e71 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -31,7 +31,7 @@ where
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
width: f32,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -85,7 +85,7 @@ where
/// Sets the text size of the [`Menu`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
- self.text_size = Some(text_size.into().0);
+ self.text_size = Some(text_size.into());
self
}
@@ -253,7 +253,7 @@ where
)
.width(self.width);
- let mut node = self.container.layout(renderer, &limits);
+ let mut node = self.container.layout(self.state, renderer, &limits);
node.move_to(if space_below > space_above {
position + Vector::new(0.0, self.target_height)
@@ -328,7 +328,7 @@ where
on_selected: Box<dyn FnMut(T) -> Message + 'a>,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -352,6 +352,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -361,8 +362,7 @@ where
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
- let text_line_height =
- self.text_line_height.to_absolute(Pixels(text_size));
+ let text_line_height = self.text_line_height.to_absolute(text_size);
let size = {
let intrinsic = Size::new(
@@ -407,9 +407,9 @@ where
.text_size
.unwrap_or_else(|| renderer.default_size());
- let option_height = f32::from(
- self.text_line_height.to_absolute(Pixels(text_size)),
- ) + self.padding.vertical();
+ let option_height =
+ f32::from(self.text_line_height.to_absolute(text_size))
+ + self.padding.vertical();
let new_hovered_option =
(cursor_position.y / option_height) as usize;
@@ -436,9 +436,9 @@ where
.text_size
.unwrap_or_else(|| renderer.default_size());
- let option_height = f32::from(
- self.text_line_height.to_absolute(Pixels(text_size)),
- ) + self.padding.vertical();
+ let option_height =
+ f32::from(self.text_line_height.to_absolute(text_size))
+ + self.padding.vertical();
*self.hovered_option =
Some((cursor_position.y / option_height) as usize);
@@ -490,7 +490,7 @@ where
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
let option_height =
- f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
+ f32::from(self.text_line_height.to_absolute(text_size))
+ self.padding.vertical();
let offset = viewport.y - bounds.y;
@@ -526,26 +526,24 @@ where
);
}
- renderer.fill_text(Text {
- content: &option.to_string(),
- bounds: Rectangle {
- x: bounds.x + self.padding.left,
- y: bounds.center_y(),
- width: f32::INFINITY,
- ..bounds
+ renderer.fill_text(
+ Text {
+ content: &option.to_string(),
+ bounds: Size::new(f32::INFINITY, bounds.height),
+ size: text_size,
+ line_height: self.text_line_height,
+ font: self.font.unwrap_or_else(|| renderer.default_font()),
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: self.text_shaping,
},
- size: text_size,
- line_height: self.text_line_height,
- font: self.font.unwrap_or_else(|| renderer.default_font()),
- color: if is_selected {
+ Point::new(bounds.x + self.padding.left, bounds.center_y()),
+ if is_selected {
appearance.selected_text_color
} else {
appearance.text_color
},
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: self.text_shaping,
- });
+ );
}
}
}
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index d8c98858..366d9a66 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -275,10 +275,12 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
+ tree,
renderer,
limits,
self.contents.layout(),
@@ -286,7 +288,9 @@ where
self.height,
self.spacing,
self.contents.iter(),
- |content, renderer, limits| content.layout(renderer, limits),
+ |content, tree, renderer, limits| {
+ content.layout(tree, renderer, limits)
+ },
)
}
@@ -471,6 +475,7 @@ where
/// Calculates the [`Layout`] of a [`PaneGrid`].
pub fn layout<Renderer, T>(
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
node: &Node,
@@ -478,19 +483,21 @@ pub fn layout<Renderer, T>(
height: Length,
spacing: f32,
contents: impl Iterator<Item = (Pane, T)>,
- layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node,
+ layout_content: impl Fn(T, &Tree, &Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits.width(width).height(height);
let size = limits.resolve(Size::ZERO);
let regions = node.pane_regions(spacing, size);
let children = contents
- .filter_map(|(pane, content)| {
+ .zip(tree.children.iter())
+ .filter_map(|((pane, content), tree)| {
let region = regions.get(&pane)?;
let size = Size::new(region.width, region.height);
let mut node = layout_content(
content,
+ tree,
renderer,
&layout::Limits::new(size, size),
);
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index e890e41a..8a74b4b9 100644
--- a/widget/src/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -150,18 +150,23 @@ where
pub(crate) fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
if let Some(title_bar) = &self.title_bar {
let max_size = limits.max();
- let title_bar_layout = title_bar
- .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+ let title_bar_layout = title_bar.layout(
+ &tree.children[1],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
let title_bar_size = title_bar_layout.size();
let mut body_layout = self.body.as_widget().layout(
+ &tree.children[0],
renderer,
&layout::Limits::new(
Size::ZERO,
@@ -179,7 +184,9 @@ where
vec![title_bar_layout, body_layout],
)
} else {
- self.body.as_widget().layout(renderer, limits)
+ self.body
+ .as_widget()
+ .layout(&tree.children[0], renderer, limits)
}
}
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index cac24e68..c0fb9936 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -213,23 +213,27 @@ where
pub(crate) fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.pad(self.padding);
let max_size = limits.max();
- let title_layout = self
- .content
- .as_widget()
- .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+ let title_layout = self.content.as_widget().layout(
+ &tree.children[0],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
let title_size = title_layout.size();
let mut node = if let Some(controls) = &self.controls {
- let mut controls_layout = controls
- .as_widget()
- .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+ let mut controls_layout = controls.as_widget().layout(
+ &tree.children[1],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
let controls_size = controls_layout.size();
let space_before_controls = max_size.width - controls_size.width;
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 0a1e2a99..719aa066 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -7,17 +7,18 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
-use crate::core::text::{self, Text};
+use crate::core::text::{self, Paragraph as _, Text};
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell,
- Size, Widget,
+ Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle,
+ Shell, Size, Widget,
};
use crate::overlay::menu::{self, Menu};
use crate::scrollable;
use std::borrow::Cow;
+use std::cell::RefCell;
pub use crate::style::pick_list::{Appearance, StyleSheet};
@@ -35,7 +36,7 @@ where
selected: Option<T>,
width: Length,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -101,7 +102,7 @@ where
/// Sets the text size of the [`PickList`].
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
- self.text_size = Some(size.into().0);
+ self.text_size = Some(size.into());
self
}
@@ -157,11 +158,11 @@ where
From<<Renderer::Theme as StyleSheet>::Style>,
{
fn tag(&self) -> tree::Tag {
- tree::Tag::of::<State>()
+ tree::Tag::of::<State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
- tree::State::new(State::new())
+ tree::State::new(State::<Renderer::Paragraph>::new())
}
fn width(&self) -> Length {
@@ -174,10 +175,12 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
+ tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
renderer,
limits,
self.width,
@@ -210,7 +213,7 @@ where
self.on_selected.as_ref(),
self.selected.as_ref(),
&self.options,
- || tree.state.downcast_mut::<State>(),
+ || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
)
}
@@ -250,7 +253,7 @@ where
self.selected.as_ref(),
&self.handle,
&self.style,
- || tree.state.downcast_ref::<State>(),
+ || tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
)
}
@@ -260,7 +263,7 @@ where
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
- let state = tree.state.downcast_mut::<State>();
+ let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
overlay(
layout,
@@ -295,28 +298,32 @@ where
}
}
-/// The local state of a [`PickList`].
+/// The state of a [`PickList`].
#[derive(Debug)]
-pub struct State {
+pub struct State<P: text::Paragraph> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
+ option_paragraphs: RefCell<Vec<P>>,
+ placeholder_paragraph: RefCell<P>,
}
-impl State {
+impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`] for a [`PickList`].
- pub fn new() -> Self {
+ fn new() -> Self {
Self {
menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
hovered_option: Option::default(),
+ option_paragraphs: RefCell::new(Vec::new()),
+ placeholder_paragraph: RefCell::new(Default::default()),
}
}
}
-impl Default for State {
+impl<P: text::Paragraph> Default for State<P> {
fn default() -> Self {
Self::new()
}
@@ -330,7 +337,7 @@ pub enum Handle<Font> {
/// This is the default.
Arrow {
/// Font size of the content.
- size: Option<f32>,
+ size: Option<Pixels>,
},
/// A custom static handle.
Static(Icon<Font>),
@@ -359,7 +366,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// Font size of the content.
- pub size: Option<f32>,
+ pub size: Option<Pixels>,
/// Line height of the content.
pub line_height: text::LineHeight,
/// The shaping strategy of the icon.
@@ -368,11 +375,12 @@ pub struct Icon<Font> {
/// Computes the layout of a [`PickList`].
pub fn layout<Renderer, T>(
+ state: &State<Renderer::Paragraph>,
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -386,38 +394,70 @@ where
use std::f32;
let limits = limits.width(width).height(Length::Shrink).pad(padding);
+ let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
- let max_width = match width {
- Length::Shrink => {
- let measure = |label: &str| -> f32 {
- let width = renderer.measure_width(
- label,
- text_size,
- font.unwrap_or_else(|| renderer.default_font()),
- text_shaping,
- );
-
- width.round()
- };
+ let mut paragraphs = state.option_paragraphs.borrow_mut();
+
+ paragraphs.resize_with(options.len(), Default::default);
+
+ let option_text = Text {
+ content: "",
+ bounds: Size::new(
+ f32::INFINITY,
+ text_line_height.to_absolute(text_size).into(),
+ ),
+ size: text_size,
+ line_height: text_line_height,
+ font,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text_shaping,
+ };
- let labels = options.iter().map(ToString::to_string);
+ for (option, paragraph) in options.iter().zip(paragraphs.iter_mut()) {
+ let label = option.to_string();
- let labels_width = labels
- .map(|label| measure(&label))
- .fold(100.0, |candidate, current| current.max(candidate));
+ renderer.update_paragraph(
+ paragraph,
+ Text {
+ content: &label,
+ ..option_text
+ },
+ );
+ }
- let placeholder_width = placeholder.map(measure).unwrap_or(100.0);
+ if let Some(placeholder) = placeholder {
+ let mut paragraph = state.placeholder_paragraph.borrow_mut();
+ renderer.update_paragraph(
+ &mut paragraph,
+ Text {
+ content: placeholder,
+ ..option_text
+ },
+ );
+ }
- labels_width.max(placeholder_width)
+ let max_width = match width {
+ Length::Shrink => {
+ let labels_width =
+ paragraphs.iter().fold(0.0, |width, paragraph| {
+ f32::max(width, paragraph.min_width())
+ });
+
+ labels_width.max(
+ placeholder
+ .map(|_| state.placeholder_paragraph.borrow().min_width())
+ .unwrap_or(0.0),
+ )
}
_ => 0.0,
};
let size = {
let intrinsic = Size::new(
- max_width + text_size + padding.left,
- f32::from(text_line_height.to_absolute(Pixels(text_size))),
+ max_width + text_size.0 + padding.left,
+ f32::from(text_line_height.to_absolute(text_size)),
);
limits.resolve(intrinsic).pad(padding)
@@ -428,7 +468,7 @@ where
/// Processes an [`Event`] and updates the [`State`] of a [`PickList`]
/// accordingly.
-pub fn update<'a, T, Message>(
+pub fn update<'a, T, P, Message>(
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
@@ -436,10 +476,11 @@ pub fn update<'a, T, Message>(
on_selected: &dyn Fn(T) -> Message,
selected: Option<&T>,
options: &[T],
- state: impl FnOnce() -> &'a mut State,
+ state: impl FnOnce() -> &'a mut State<P>,
) -> event::Status
where
T: PartialEq + Clone + 'a,
+ P: text::Paragraph + 'a,
{
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
@@ -534,9 +575,9 @@ pub fn mouse_interaction(
/// Returns the current overlay of a [`PickList`].
pub fn overlay<'a, T, Message, Renderer>(
layout: Layout<'_>,
- state: &'a mut State,
+ state: &'a mut State<Renderer::Paragraph>,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_shaping: text::Shaping,
font: Renderer::Font,
options: &'a [T],
@@ -591,7 +632,7 @@ pub fn draw<'a, T, Renderer>(
layout: Layout<'_>,
cursor: mouse::Cursor,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Renderer::Font,
@@ -599,7 +640,7 @@ pub fn draw<'a, T, Renderer>(
selected: Option<&T>,
handle: &Handle<Renderer::Font>,
style: &<Renderer::Theme as StyleSheet>::Style,
- state: impl FnOnce() -> &'a State,
+ state: impl FnOnce() -> &'a State<Renderer::Paragraph>,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -665,22 +706,26 @@ pub fn draw<'a, T, Renderer>(
if let Some((font, code_point, size, line_height, shaping)) = handle {
let size = size.unwrap_or_else(|| renderer.default_size());
- renderer.fill_text(Text {
- content: &code_point.to_string(),
- size,
- line_height,
- font,
- color: style.handle_color,
- bounds: Rectangle {
- x: bounds.x + bounds.width - padding.horizontal(),
- y: bounds.center_y(),
- height: f32::from(line_height.to_absolute(Pixels(size))),
- ..bounds
+ renderer.fill_text(
+ Text {
+ content: &code_point.to_string(),
+ size,
+ line_height,
+ font,
+ bounds: Size::new(
+ bounds.width,
+ f32::from(line_height.to_absolute(size)),
+ ),
+ horizontal_alignment: alignment::Horizontal::Right,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping,
},
- horizontal_alignment: alignment::Horizontal::Right,
- vertical_alignment: alignment::Vertical::Center,
- shaping,
- });
+ Point::new(
+ bounds.x + bounds.width - padding.horizontal(),
+ bounds.center_y(),
+ ),
+ style.handle_color,
+ );
}
let label = selected.map(ToString::to_string);
@@ -688,27 +733,26 @@ pub fn draw<'a, T, Renderer>(
if let Some(label) = label.as_deref().or(placeholder) {
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
- renderer.fill_text(Text {
- content: label,
- size: text_size,
- line_height: text_line_height,
- font,
- color: if is_selected {
+ renderer.fill_text(
+ Text {
+ content: label,
+ size: text_size,
+ line_height: text_line_height,
+ font,
+ bounds: Size::new(
+ bounds.width - padding.horizontal(),
+ f32::from(text_line_height.to_absolute(text_size)),
+ ),
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text_shaping,
+ },
+ Point::new(bounds.x + padding.left, bounds.center_y()),
+ if is_selected {
style.text_color
} else {
style.placeholder_color
},
- bounds: Rectangle {
- x: bounds.x + padding.left,
- y: bounds.center_y(),
- width: bounds.width - padding.horizontal(),
- height: f32::from(
- text_line_height.to_absolute(Pixels(text_size)),
- ),
- },
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: text_shaping,
- });
+ );
}
}
diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs
index 37c6bc72..8b1490af 100644
--- a/widget/src/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -95,6 +95,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs
index 51a541fd..589704a5 100644
--- a/widget/src/qr_code.rs
+++ b/widget/src/qr_code.rs
@@ -60,6 +60,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer<Theme>,
_limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 65d71ec2..cb908ec4 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -6,12 +6,12 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
-use crate::core::widget::Tree;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle,
- Shell, Widget,
+ Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, Shell, Size,
+ Widget,
};
-use crate::{Row, Text};
pub use iced_style::radio::{Appearance, StyleSheet};
@@ -80,7 +80,7 @@ where
width: Length,
size: f32,
spacing: f32,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -152,7 +152,7 @@ where
/// Sets the text size of the [`Radio`] button.
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
- self.text_size = Some(text_size.into().0);
+ self.text_size = Some(text_size.into());
self
}
@@ -193,6 +193,14 @@ where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
+ }
+
fn width(&self) -> Length {
self.width
}
@@ -203,25 +211,35 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- Row::<(), Renderer>::new()
- .width(self.width)
- .spacing(self.spacing)
- .align_items(Alignment::Center)
- .push(Row::new().width(self.size).height(self.size))
- .push(
- Text::new(&self.label)
- .width(self.width)
- .size(
- self.text_size
- .unwrap_or_else(|| renderer.default_size()),
- )
- .line_height(self.text_line_height)
- .shaping(self.text_shaping),
- )
- .layout(renderer, limits)
+ layout::next_to_each_other(
+ &limits.width(self.width),
+ self.spacing,
+ |_| layout::Node::new(Size::new(self.size, self.size)),
+ |limits| {
+ let state = tree
+ .state
+ .downcast_ref::<widget::text::State<Renderer::Paragraph>>();
+
+ widget::text::layout(
+ state,
+ renderer,
+ limits,
+ self.width,
+ Length::Shrink,
+ &self.label,
+ self.text_line_height,
+ self.text_size,
+ self.font,
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ self.text_shaping,
+ )
+ },
+ )
}
fn on_event(
@@ -267,7 +285,7 @@ where
fn draw(
&self,
- _state: &Tree,
+ tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
@@ -327,16 +345,10 @@ where
renderer,
style,
label_layout,
- &self.label,
- self.text_size,
- self.text_line_height,
- self.font,
+ tree.state.downcast_ref(),
crate::text::Appearance {
color: custom_style.text_color,
},
- alignment::Horizontal::Left,
- alignment::Vertical::Center,
- self.text_shaping,
);
}
}
diff --git a/widget/src/row.rs b/widget/src/row.rs
index 99b2a0bf..17c49e67 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -114,6 +114,7 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -127,6 +128,7 @@ where
self.spacing,
self.align_items,
&self.children,
+ &tree.children,
)
}
diff --git a/widget/src/rule.rs b/widget/src/rule.rs
index d703e6ae..032ff860 100644
--- a/widget/src/rule.rs
+++ b/widget/src/rule.rs
@@ -72,6 +72,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index a83ed985..ce96883d 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -230,6 +230,7 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -240,7 +241,11 @@ where
self.height,
&self.direction,
|renderer, limits| {
- self.content.as_widget().layout(renderer, limits)
+ self.content.as_widget().layout(
+ &tree.children[0],
+ renderer,
+ limits,
+ )
},
)
}
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index e41be7c9..b4c9198a 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -169,6 +169,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/space.rs b/widget/src/space.rs
index 9a5385e8..84331870 100644
--- a/widget/src/space.rs
+++ b/widget/src/space.rs
@@ -55,6 +55,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/svg.rs b/widget/src/svg.rs
index 1ccc5d62..f61a4ce2 100644
--- a/widget/src/svg.rs
+++ b/widget/src/svg.rs
@@ -106,6 +106,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index 61fc0055..209ef968 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -17,7 +17,7 @@ use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse::{self, click};
use crate::core::renderer;
-use crate::core::text::{self, Text};
+use crate::core::text::{self, Paragraph as _, Text};
use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
@@ -30,6 +30,8 @@ use crate::core::{
};
use crate::runtime::Command;
+use std::cell::RefCell;
+
pub use iced_style::text_input::{Appearance, StyleSheet};
/// A field that can be filled with text.
@@ -67,7 +69,7 @@ where
font: Option<Renderer::Font>,
width: Length,
padding: Padding,
- size: Option<f32>,
+ size: Option<Pixels>,
line_height: text::LineHeight,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
@@ -178,7 +180,7 @@ where
/// Sets the text size of the [`TextInput`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
- self.size = Some(size.into().0);
+ self.size = Some(size.into());
self
}
@@ -218,12 +220,8 @@ where
theme,
layout,
cursor,
- tree.state.downcast_ref::<State>(),
+ tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
value.unwrap_or(&self.value),
- &self.placeholder,
- self.size,
- self.line_height,
- self.font,
self.on_input.is_none(),
self.is_secure,
self.icon.as_ref(),
@@ -240,15 +238,15 @@ where
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
- tree::Tag::of::<State>()
+ tree::Tag::of::<State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
- tree::State::new(State::new())
+ tree::State::new(State::<Renderer::Paragraph>::new())
}
fn diff(&self, tree: &mut Tree) {
- let state = tree.state.downcast_mut::<State>();
+ let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
// Unfocus text input if it becomes disabled
if self.on_input.is_none() {
@@ -269,6 +267,7 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -278,8 +277,13 @@ where
self.width,
self.padding,
self.size,
+ self.font,
self.line_height,
self.icon.as_ref(),
+ tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
+ &self.value,
+ &self.placeholder,
+ self.is_secure,
)
}
@@ -290,7 +294,7 @@ where
_renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
- let state = tree.state.downcast_mut::<State>();
+ let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
operation.text_input(state, self.id.as_ref().map(|id| &id.0));
@@ -322,7 +326,7 @@ where
self.on_input.as_deref(),
self.on_paste.as_deref(),
&self.on_submit,
- || tree.state.downcast_mut::<State>(),
+ || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
)
}
@@ -341,12 +345,8 @@ where
theme,
layout,
cursor,
- tree.state.downcast_ref::<State>(),
+ tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
&self.value,
- &self.placeholder,
- self.size,
- self.line_height,
- self.font,
self.on_input.is_none(),
self.is_secure,
self.icon.as_ref(),
@@ -388,7 +388,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// The font size of the content.
- pub size: Option<f32>,
+ pub size: Option<Pixels>,
/// The spacing between the [`Icon`] and the text in a [`TextInput`].
pub spacing: f32,
/// The side of a [`TextInput`] where to display the [`Icon`].
@@ -465,29 +465,65 @@ pub fn layout<Renderer>(
limits: &layout::Limits,
width: Length,
padding: Padding,
- size: Option<f32>,
+ size: Option<Pixels>,
+ font: Option<Renderer::Font>,
line_height: text::LineHeight,
icon: Option<&Icon<Renderer::Font>>,
+ state: &State<Renderer::Paragraph>,
+ value: &Value,
+ placeholder: &str,
+ is_secure: bool,
) -> layout::Node
where
Renderer: text::Renderer,
{
+ let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = size.unwrap_or_else(|| renderer.default_size());
+
let padding = padding.fit(Size::ZERO, limits.max());
let limits = limits
.width(width)
.pad(padding)
- .height(line_height.to_absolute(Pixels(text_size)));
+ .height(line_height.to_absolute(text_size));
let text_bounds = limits.resolve(Size::ZERO);
- if let Some(icon) = icon {
- let icon_width = renderer.measure_width(
- &icon.code_point.to_string(),
- icon.size.unwrap_or_else(|| renderer.default_size()),
- icon.font,
- text::Shaping::Advanced,
+ let placeholder_text = Text {
+ font,
+ line_height,
+ content: placeholder,
+ bounds: Size::new(f32::INFINITY, text_bounds.height),
+ size: text_size,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text::Shaping::Advanced,
+ };
+
+ renderer.update_paragraph(
+ &mut state.placeholder_paragraph.borrow_mut(),
+ placeholder_text,
+ );
+
+ if is_secure {
+ renderer.update_paragraph(
+ &mut state.paragraph.borrow_mut(),
+ Text {
+ content: &value.secure().to_string(),
+ ..placeholder_text
+ },
+ );
+ } else {
+ renderer.update_paragraph(
+ &mut state.paragraph.borrow_mut(),
+ Text {
+ content: &value.to_string(),
+ ..placeholder_text
+ },
);
+ }
+
+ if let Some(icon) = icon {
+ let icon_width = 0.0; // TODO
let mut text_node = layout::Node::new(
text_bounds - Size::new(icon_width + icon.spacing, 0.0),
@@ -537,19 +573,31 @@ pub fn update<'a, Message, Renderer>(
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
value: &mut Value,
- size: Option<f32>,
+ size: Option<Pixels>,
line_height: text::LineHeight,
font: Option<Renderer::Font>,
is_secure: bool,
on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>,
- state: impl FnOnce() -> &'a mut State,
+ state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>,
) -> event::Status
where
Message: Clone,
Renderer: text::Renderer,
{
+ let update_cache = |state, value| {
+ replace_paragraph(
+ renderer,
+ state,
+ layout,
+ value,
+ font,
+ size,
+ line_height,
+ )
+ };
+
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
@@ -592,11 +640,7 @@ where
};
find_cursor_position(
- renderer,
text_layout.bounds(),
- font,
- size,
- line_height,
&value,
state,
target,
@@ -621,11 +665,7 @@ where
state.cursor.select_all(value);
} else {
let position = find_cursor_position(
- renderer,
text_layout.bounds(),
- font,
- size,
- line_height,
value,
state,
target,
@@ -671,11 +711,7 @@ where
};
let position = find_cursor_position(
- renderer,
text_layout.bounds(),
- font,
- size,
- line_height,
&value,
state,
target,
@@ -710,6 +746,8 @@ where
focus.updated_at = Instant::now();
+ update_cache(state, value);
+
return event::Status::Captured;
}
}
@@ -749,6 +787,8 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
+
+ update_cache(state, value);
}
keyboard::KeyCode::Delete => {
if platform::is_jump_modifier_pressed(modifiers)
@@ -769,6 +809,8 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
+
+ update_cache(state, value);
}
keyboard::KeyCode::Left => {
if platform::is_jump_modifier_pressed(modifiers)
@@ -844,6 +886,8 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
+
+ update_cache(state, value);
}
keyboard::KeyCode::V => {
if state.keyboard_modifiers.command()
@@ -876,6 +920,8 @@ where
shell.publish(message);
state.is_pasting = Some(content);
+
+ update_cache(state, value);
} else {
state.is_pasting = None;
}
@@ -979,12 +1025,8 @@ pub fn draw<Renderer>(
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor: mouse::Cursor,
- state: &State,
+ state: &State<Renderer::Paragraph>,
value: &Value,
- placeholder: &str,
- size: Option<f32>,
- line_height: text::LineHeight,
- font: Option<Renderer::Font>,
is_disabled: bool,
is_secure: bool,
icon: Option<&Icon<Renderer::Font>>,
@@ -1023,28 +1065,14 @@ pub fn draw<Renderer>(
appearance.background,
);
- if let Some(icon) = icon {
- let icon_layout = children_layout.next().unwrap();
+ if let Some(_icon) = icon {
+ let _icon_layout = children_layout.next().unwrap();
- renderer.fill_text(Text {
- content: &icon.code_point.to_string(),
- size: icon.size.unwrap_or_else(|| renderer.default_size()),
- line_height: text::LineHeight::default(),
- font: icon.font,
- color: appearance.icon_color,
- bounds: Rectangle {
- y: text_bounds.center_y(),
- ..icon_layout.bounds()
- },
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: text::Shaping::Advanced,
- });
+ // TODO
}
let text = value.to_string();
- let font = font.unwrap_or_else(|| renderer.default_font());
- let size = size.unwrap_or_else(|| renderer.default_size());
+ let paragraph = &state.paragraph.borrow() as &Renderer::Paragraph;
let (cursor, offset) = if let Some(focus) = state
.is_focused
@@ -1055,12 +1083,9 @@ pub fn draw<Renderer>(
cursor::State::Index(position) => {
let (text_value_width, offset) =
measure_cursor_and_scroll_offset(
- renderer,
+ paragraph,
text_bounds,
- value,
- size,
position,
- font,
);
let is_cursor_visible = ((focus.now - focus.updated_at)
@@ -1096,22 +1121,16 @@ pub fn draw<Renderer>(
let (left_position, left_offset) =
measure_cursor_and_scroll_offset(
- renderer,
+ paragraph,
text_bounds,
- value,
- size,
left,
- font,
);
let (right_position, right_offset) =
measure_cursor_and_scroll_offset(
- renderer,
+ paragraph,
text_bounds,
- value,
- size,
right,
- font,
);
let width = right_position - left_position;
@@ -1143,12 +1162,7 @@ pub fn draw<Renderer>(
(None, 0.0)
};
- let text_width = renderer.measure_width(
- if text.is_empty() { placeholder } else { &text },
- size,
- font,
- text::Shaping::Advanced,
- );
+ let text_width = paragraph.min_width();
let render = |renderer: &mut Renderer| {
if let Some((cursor, color)) = cursor {
@@ -1157,27 +1171,23 @@ pub fn draw<Renderer>(
renderer.with_translation(Vector::ZERO, |_| {});
}
- renderer.fill_text(Text {
- content: if text.is_empty() { placeholder } else { &text },
- color: if text.is_empty() {
+ let placeholder_paragraph = state.placeholder_paragraph.borrow();
+
+ renderer.fill_paragraph(
+ if text.is_empty() {
+ &placeholder_paragraph
+ } else {
+ paragraph
+ },
+ Point::new(text_bounds.x, text_bounds.center_y()),
+ if text.is_empty() {
theme.placeholder_color(style)
} else if is_disabled {
theme.disabled_color(style)
} else {
theme.value_color(style)
},
- font,
- bounds: Rectangle {
- y: text_bounds.center_y(),
- width: f32::INFINITY,
- ..text_bounds
- },
- size,
- line_height,
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: text::Shaping::Advanced,
- });
+ );
};
if text_width > text_bounds.width {
@@ -1208,7 +1218,9 @@ pub fn mouse_interaction(
/// The state of a [`TextInput`].
#[derive(Debug, Default, Clone)]
-pub struct State {
+pub struct State<P: text::Paragraph> {
+ paragraph: RefCell<P>,
+ placeholder_paragraph: RefCell<P>,
is_focused: Option<Focus>,
is_dragging: bool,
is_pasting: Option<Value>,
@@ -1225,7 +1237,7 @@ struct Focus {
is_window_focused: bool,
}
-impl State {
+impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`], representing an unfocused [`TextInput`].
pub fn new() -> Self {
Self::default()
@@ -1234,6 +1246,8 @@ impl State {
/// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused() -> Self {
Self {
+ paragraph: RefCell::new(P::default()),
+ placeholder_paragraph: RefCell::new(P::default()),
is_focused: None,
is_dragging: false,
is_pasting: None,
@@ -1292,7 +1306,7 @@ impl State {
}
}
-impl operation::Focusable for State {
+impl<P: text::Paragraph> operation::Focusable for State<P> {
fn is_focused(&self) -> bool {
State::is_focused(self)
}
@@ -1306,7 +1320,7 @@ impl operation::Focusable for State {
}
}
-impl operation::TextInput for State {
+impl<P: text::Paragraph> operation::TextInput for State<P> {
fn move_cursor_to_front(&mut self) {
State::move_cursor_to_front(self)
}
@@ -1336,17 +1350,11 @@ mod platform {
}
}
-fn offset<Renderer>(
- renderer: &Renderer,
+fn offset<P: text::Paragraph>(
text_bounds: Rectangle,
- font: Renderer::Font,
- size: f32,
value: &Value,
- state: &State,
-) -> f32
-where
- Renderer: text::Renderer,
-{
+ state: &State<P>,
+) -> f32 {
if state.is_focused() {
let cursor = state.cursor();
@@ -1356,12 +1364,9 @@ where
};
let (_, offset) = measure_cursor_and_scroll_offset(
- renderer,
+ &state.paragraph.borrow() as &P,
text_bounds,
- value,
- size,
focus_position,
- font,
);
offset
@@ -1370,63 +1375,35 @@ where
}
}
-fn measure_cursor_and_scroll_offset<Renderer>(
- renderer: &Renderer,
+fn measure_cursor_and_scroll_offset(
+ paragraph: &impl text::Paragraph,
text_bounds: Rectangle,
- value: &Value,
- size: f32,
cursor_index: usize,
- font: Renderer::Font,
-) -> (f32, f32)
-where
- Renderer: text::Renderer,
-{
- let text_before_cursor = value.until(cursor_index).to_string();
+) -> (f32, f32) {
+ let grapheme_position = paragraph
+ .grapheme_position(0, cursor_index)
+ .unwrap_or(Point::ORIGIN);
- let text_value_width = renderer.measure_width(
- &text_before_cursor,
- size,
- font,
- text::Shaping::Advanced,
- );
+ let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
- let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0);
-
- (text_value_width, offset)
+ (grapheme_position.x, offset)
}
/// Computes the position of the text cursor at the given X coordinate of
/// a [`TextInput`].
-fn find_cursor_position<Renderer>(
- renderer: &Renderer,
+fn find_cursor_position<P: text::Paragraph>(
text_bounds: Rectangle,
- font: Option<Renderer::Font>,
- size: Option<f32>,
- line_height: text::LineHeight,
value: &Value,
- state: &State,
+ state: &State<P>,
x: f32,
-) -> Option<usize>
-where
- Renderer: text::Renderer,
-{
- let font = font.unwrap_or_else(|| renderer.default_font());
- let size = size.unwrap_or_else(|| renderer.default_size());
-
- let offset = offset(renderer, text_bounds, font, size, value, state);
+) -> Option<usize> {
+ let offset = offset(text_bounds, value, state);
let value = value.to_string();
- let char_offset = renderer
- .hit_test(
- &value,
- size,
- line_height,
- font,
- Size::INFINITY,
- text::Shaping::Advanced,
- Point::new(x + offset, text_bounds.height / 2.0),
- true,
- )
+ let char_offset = state
+ .paragraph
+ .borrow()
+ .hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?;
Some(
@@ -1438,4 +1415,33 @@ where
)
}
+fn replace_paragraph<Renderer>(
+ renderer: &Renderer,
+ state: &mut State<Renderer::Paragraph>,
+ layout: Layout<'_>,
+ value: &Value,
+ font: Option<Renderer::Font>,
+ text_size: Option<Pixels>,
+ line_height: text::LineHeight,
+) where
+ Renderer: text::Renderer,
+{
+ let font = font.unwrap_or_else(|| renderer.default_font());
+ let text_size = text_size.unwrap_or_else(|| renderer.default_size());
+
+ let mut children_layout = layout.children();
+ let text_bounds = children_layout.next().unwrap().bounds();
+
+ *state.paragraph.get_mut() = renderer.create_paragraph(Text {
+ font,
+ line_height,
+ content: &value.to_string(),
+ bounds: Size::new(f32::INFINITY, text_bounds.height),
+ size: text_size,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ shaping: text::Shaping::Advanced,
+ });
+}
+
const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index c8187181..4ddc8db6 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -6,12 +6,12 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
-use crate::core::widget::Tree;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle,
- Shell, Widget,
+ Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size,
+ Widget,
};
-use crate::{Row, Text};
pub use crate::style::toggler::{Appearance, StyleSheet};
@@ -42,7 +42,7 @@ where
label: Option<String>,
width: Length,
size: f32,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_alignment: alignment::Horizontal,
text_shaping: text::Shaping,
@@ -105,7 +105,7 @@ where
/// Sets the text size o the [`Toggler`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
- self.text_size = Some(text_size.into().0);
+ self.text_size = Some(text_size.into());
self
}
@@ -160,6 +160,14 @@ where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
+ }
+
fn width(&self) -> Length {
self.width
}
@@ -170,32 +178,41 @@ where
fn layout(
&self,
+ tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let mut row = Row::<(), Renderer>::new()
- .width(self.width)
- .spacing(self.spacing)
- .align_items(Alignment::Center);
-
- if let Some(label) = &self.label {
- row = row.push(
- Text::new(label)
- .horizontal_alignment(self.text_alignment)
- .font(self.font.unwrap_or_else(|| renderer.default_font()))
- .width(self.width)
- .size(
- self.text_size
- .unwrap_or_else(|| renderer.default_size()),
+ let limits = limits.width(self.width);
+
+ layout::next_to_each_other(
+ &limits,
+ self.spacing,
+ |_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
+ |limits| {
+ if let Some(label) = self.label.as_deref() {
+ let state = tree
+ .state
+ .downcast_ref::<widget::text::State<Renderer::Paragraph>>();
+
+ widget::text::layout(
+ state,
+ renderer,
+ limits,
+ self.width,
+ Length::Shrink,
+ label,
+ self.text_line_height,
+ self.text_size,
+ self.font,
+ self.text_alignment,
+ alignment::Vertical::Top,
+ self.text_shaping,
)
- .line_height(self.text_line_height)
- .shaping(self.text_shaping),
- );
- }
-
- row = row.push(Row::new().width(2.0 * self.size).height(self.size));
-
- row.layout(renderer, limits)
+ } else {
+ layout::Node::new(Size::ZERO)
+ }
+ },
+ )
}
fn on_event(
@@ -243,7 +260,7 @@ where
fn draw(
&self,
- _state: &Tree,
+ tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
@@ -259,28 +276,21 @@ where
const SPACE_RATIO: f32 = 0.05;
let mut children = layout.children();
+ let toggler_layout = children.next().unwrap();
- if let Some(label) = &self.label {
+ if self.label.is_some() {
let label_layout = children.next().unwrap();
crate::text::draw(
renderer,
style,
label_layout,
- label,
- self.text_size,
- self.text_line_height,
- self.font,
+ tree.state.downcast_ref(),
Default::default(),
- self.text_alignment,
- alignment::Vertical::Center,
- self.text_shaping,
);
}
- let toggler_layout = children.next().unwrap();
let bounds = toggler_layout.bounds();
-
let is_mouse_over = cursor.is_over(layout.bounds());
let style = if is_mouse_over {
diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs
index faa3f3e1..0444850e 100644
--- a/widget/src/tooltip.rs
+++ b/widget/src/tooltip.rs
@@ -107,11 +107,14 @@ where
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
fn children(&self) -> Vec<widget::Tree> {
- vec![widget::Tree::new(&self.content)]
+ vec![
+ widget::Tree::new(&self.content),
+ widget::Tree::new(&self.tooltip as &dyn Widget<Message, _>),
+ ]
}
fn diff(&self, tree: &mut widget::Tree) {
- tree.diff_children(std::slice::from_ref(&self.content))
+ tree.diff_children(&[self.content.as_widget(), &self.tooltip])
}
fn state(&self) -> widget::tree::State {
@@ -132,10 +135,11 @@ where
fn layout(
&self,
+ tree: &widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.content.as_widget().layout(renderer, limits)
+ self.content.as_widget().layout(tree, renderer, limits)
}
fn on_event(
@@ -214,8 +218,10 @@ where
) -> Option<overlay::Element<'b, Message, Renderer>> {
let state = tree.state.downcast_ref::<State>();
+ let mut children = tree.children.iter_mut();
+
let content = self.content.as_widget_mut().overlay(
- &mut tree.children[0],
+ children.next().unwrap(),
layout,
renderer,
);
@@ -225,6 +231,7 @@ where
layout.position(),
Box::new(Overlay {
tooltip: &self.tooltip,
+ state: children.next().unwrap(),
cursor_position,
content_bounds: layout.bounds(),
snap_within_viewport: self.snap_within_viewport,
@@ -295,6 +302,7 @@ where
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
{
tooltip: &'b Text<'a, Renderer>,
+ state: &'b widget::Tree,
cursor_position: Point,
content_bounds: Rectangle,
snap_within_viewport: bool,
@@ -320,6 +328,7 @@ where
let text_layout = Widget::<(), Renderer>::layout(
self.tooltip,
+ self.state,
renderer,
&layout::Limits::new(
Size::ZERO,
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index efca302a..a11fec75 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -166,6 +166,7 @@ where
fn layout(
&self,
+ _tree: &Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {