diff --git a/CHANGELOG.md b/CHANGELOG.md index fd930a3665..fdd832e495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Explicit text caching. [#2058](https://github.com/iced-rs/iced/pull/2058) - `Theme::Custom::with_fn` for custom extended palette generation. [#2067](https://github.com/iced-rs/iced/pull/2067) ### Changed diff --git a/core/src/element.rs b/core/src/element.rs index d2c6358b6c..02f16bcba5 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -306,10 +306,11 @@ where fn layout( &self, + tree: &mut 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: &mut 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 04954fb9b2..caf315b6a5 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,36 @@ 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 mut 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(); + + let (left_y, right_y) = if left_size.height > right_size.height { + (0.0, (left_size.height - right_size.height) / 2.0) + } else { + ((right_size.height - left_size.height) / 2.0, 0.0) + }; + + left_node.move_to(Point::new(0.0, left_y)); + right_node.move_to(Point::new(left_size.width + spacing, right_y)); + + 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 8b9678497b..c02b63d817 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( spacing: f32, align_items: Alignment, items: &[Element<'_, Message, Renderer>], + trees: &mut [widget::Tree], ) -> Node where Renderer: crate::Renderer, @@ -81,7 +83,7 @@ where let mut nodes: Vec = 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.iter_mut()).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/overlay.rs b/core/src/overlay.rs index 2e05db9366..f71f25f734 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -25,7 +25,7 @@ where /// /// [`Node`]: layout::Node fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 29b404b83f..689e69be38 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -54,7 +54,7 @@ where /// Computes the layout of the [`Element`] in the given bounds. pub fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, translation: Vector, @@ -150,7 +150,7 @@ where Renderer: crate::Renderer, { fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 691686cdd5..a0bae6bbbc 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -61,7 +61,7 @@ where Renderer: crate::Renderer, { fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, @@ -71,7 +71,7 @@ where layout::Node::with_children( bounds, self.children - .iter() + .iter_mut() .map(|child| child.layout(renderer, bounds, translation)) .collect(), ) diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 7c73d2e431..1b327e563c 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( - &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 5d49699edf..55d58a5918 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 { + _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 { 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 { + None + } } diff --git a/core/src/text.rs b/core/src/text.rs index fc8aa20ee2..0e3617b1b0 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 + 'static; /// The icon font of the backend. const ICON_FONT: Self::Font; @@ -151,62 +151,151 @@ 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; + + /// Loads a [`Self::Font`] from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>); - /// Measures the text in the given bounds and returns the minimum boundaries - /// that can fit the contents. - fn measure( + /// 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>, + ) { + match compare(paragraph, text) { + Difference::None => {} + Difference::Bounds => { + self.resize_paragraph(paragraph, text.bounds); + } + Difference::Shape => { + *paragraph = self.create_paragraph(text); + } + } + } + + /// 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; + + /// Returns the content of the [`Paragraph`]. + fn content(&self) -> &str; + + /// 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 [`Self::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; + + /// Returns the distance to the given grapheme index in the [`Paragraph`]. + fn grapheme_position(&self, line: usize, index: usize) -> Option; + + /// 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 } +} - /// Tests whether the provided point is within the boundaries of text - /// laid out with the given parameters, returning information about - /// the nearest character. +/// The difference detected in some text. +/// +/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some +/// [`Text`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Difference { + /// No difference. /// - /// 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; + /// The text can be reused as it is! + None, - /// Loads a [`Self::Font`] from its bytes. - fn load_font(&mut self, font: Cow<'static, [u8]>); + /// A bounds difference. + /// + /// This normally means a relayout is necessary, but the shape of the text can + /// be reused. + Bounds, - /// Draws the given [`Text`]. - fn fill_text(&mut self, text: Text<'_, Self::Font>); + /// A shape difference. + /// + /// The contents, alignment, sizes, fonts, or any other essential attributes + /// of the shape of the text have changed. A complete reshape and relayout of + /// the text is necessary. + Shape, +} + +/// Compares a [`Paragraph`] with some desired [`Text`] and returns the +/// [`Difference`]. +pub fn compare( + paragraph: &impl Paragraph, + text: Text<'_, Font>, +) -> Difference { + 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 + { + Difference::Shape + } else if paragraph.bounds() != text.bounds { + Difference::Bounds + } else { + Difference::None + } } diff --git a/core/src/widget.rs b/core/src/widget.rs index d6a99208e1..294d59847e 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -55,6 +55,7 @@ where /// user interface. fn layout( &self, + tree: &mut 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 79df2b027e..53ed463e39 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -3,9 +3,9 @@ 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}; +use crate::widget::tree::{self, Tree}; +use crate::{Color, Element, Layout, Length, Pixels, Point, Rectangle, Widget}; use std::borrow::Cow; @@ -19,7 +19,7 @@ where Renderer::Theme: StyleSheet, { content: Cow<'a, str>, - size: Option, + size: Option, line_height: LineHeight, width: Length, height: Length, @@ -53,7 +53,7 @@ where /// Sets the size of the [`Text`]. pub fn size(mut self, size: impl Into) -> Self { - self.size = Some(size.into().0); + self.size = Some(size.into()); self } @@ -117,11 +117,23 @@ where } } +/// The internal state of a [`Text`] widget. +#[derive(Debug, Default)] +pub struct State(P); + impl<'a, Message, Renderer> Widget for Text<'a, Renderer> where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(State(Renderer::Paragraph::default())) + } + fn width(&self) -> Length { self.width } @@ -132,30 +144,29 @@ where fn layout( &self, + tree: &mut 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_mut::>(), + 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 +174,63 @@ where _cursor_position: mouse::Cursor, _viewport: &Rectangle, ) { + let state = tree.state.downcast_ref::>(); + 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( + state: &mut State, + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + content: &str, + line_height: LineHeight, + size: Option, + font: Option, + 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 State(ref mut paragraph) = state; + + renderer.update_paragraph( + 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 +245,31 @@ pub fn draw( renderer: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, - content: &str, - size: Option, - line_height: LineHeight, - font: Option, + state: &State, appearance: Appearance, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, - shaping: Shaping, ) where Renderer: text::Renderer, { + let State(ref paragraph) = state; 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> diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 0af40c33d9..202cca9a50 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -107,6 +107,88 @@ impl Tree { } } +/// Reconciliates the `current_children` with the provided list of widgets using +/// custom logic both for diffing and creating new widget state. +/// +/// The algorithm will try to minimize the impact of diffing by querying the +/// `maybe_changed` closure. +pub fn diff_children_custom_with_search( + current_children: &mut Vec, + new_children: &[T], + diff: impl Fn(&mut Tree, &T), + maybe_changed: impl Fn(usize) -> bool, + new_state: impl Fn(&T) -> Tree, +) { + if new_children.is_empty() { + current_children.clear(); + return; + } + + if current_children.is_empty() { + current_children.extend(new_children.iter().map(new_state)); + return; + } + + let first_maybe_changed = maybe_changed(0); + let last_maybe_changed = maybe_changed(current_children.len() - 1); + + if current_children.len() > new_children.len() { + if !first_maybe_changed && last_maybe_changed { + current_children.truncate(new_children.len()); + } else { + let difference_index = if first_maybe_changed { + 0 + } else { + (1..current_children.len()) + .find(|&i| maybe_changed(i)) + .unwrap_or(0) + }; + + let _ = current_children.splice( + difference_index + ..difference_index + + (current_children.len() - new_children.len()), + std::iter::empty(), + ); + } + } + + if current_children.len() < new_children.len() { + let first_maybe_changed = maybe_changed(0); + let last_maybe_changed = maybe_changed(current_children.len() - 1); + + if !first_maybe_changed && last_maybe_changed { + current_children.extend( + new_children[current_children.len()..].iter().map(new_state), + ); + } else { + let difference_index = if first_maybe_changed { + 0 + } else { + (1..current_children.len()) + .find(|&i| maybe_changed(i)) + .unwrap_or(0) + }; + + let _ = current_children.splice( + difference_index..difference_index, + new_children[difference_index + ..difference_index + + (new_children.len() - current_children.len())] + .iter() + .map(new_state), + ); + } + } + + // TODO: Merge loop with extend logic (?) + for (child_state, new) in + current_children.iter_mut().zip(new_children.iter()) + { + diff(child_state, new); + } +} + /// The identifier of some widget state. #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct Tag(any::TypeId); diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 736a9d531b..7dc981d94d 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 2e6f95d583..4f347667e2 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 4b300116da..13b0825094 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: &mut 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 713bc62d42..32a14cbe1e 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: &mut 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 39c1609d83..e451cb0619 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 3bc7f46bd8..8ab3b49312 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -26,6 +26,7 @@ mod rainbow { fn layout( &self, + _tree: &mut widget::Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index d0b2d89103..7945bd208d 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; @@ -142,12 +142,11 @@ pub fn main() -> Result<(), Box> { // 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 3898d76e49..bf01c3b44f 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -254,6 +254,7 @@ where fn layout( &self, + _tree: &mut Tree, _renderer: &iced::Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index 20fbe9f377..c5bb4791ce 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -175,6 +175,7 @@ where fn layout( &self, + _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 4aa708869c..c050d3cc0a 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -287,10 +287,15 @@ mod modal { fn layout( &self, + tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.base.as_widget().layout(renderer, limits) + self.base.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ) } fn on_event( @@ -401,7 +406,7 @@ mod modal { Message: Clone, { fn layout( - &self, + &mut self, renderer: &Renderer, _bounds: Size, position: Point, @@ -410,7 +415,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 47b272a95e..8570a38e6d 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -328,10 +328,15 @@ mod toast { fn layout( &self, + tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.content.as_widget().layout(renderer, limits) + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ) } fn tag(&self) -> widget::tree::Tag { @@ -502,7 +507,7 @@ mod toast { for Overlay<'a, 'b, Message> { fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, @@ -519,6 +524,7 @@ mod toast { 10.0, Alignment::End, self.toasts, + self.state, ) .translate(Vector::new(position.x, position.y)) } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 3334d84f12..3c62bfbc8f 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -9,18 +9,21 @@ publish = false iced.workspace = true iced.features = ["async-std", "debug"] +once_cell.workspace = true serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -once_cell.workspace = true +uuid = { version = "1.0", features = ["v4", "fast-rng", "serde"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std.workspace = true directories-next = "2.0" +tracing-subscriber = "0.3" [target.'cfg(target_arch = "wasm32")'.dependencies] iced.workspace = true iced.features = ["debug", "webgl"] +uuid = { version = "1.0", features = ["js"] } web-sys = { workspace = true, features = ["Window", "Storage"] } wasm-timer.workspace = true diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 62c179268d..501bf67ec3 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -3,8 +3,8 @@ use iced::font::{self, Font}; use iced::keyboard; use iced::theme::{self, Theme}; use iced::widget::{ - self, button, checkbox, column, container, row, scrollable, text, - text_input, Text, + self, button, checkbox, column, container, keyed_column, row, scrollable, + text, text_input, Text, }; use iced::window; use iced::{Application, Element}; @@ -12,10 +12,14 @@ use iced::{Color, Command, Length, Settings, Subscription}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use uuid::Uuid; static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); pub fn main() -> iced::Result { + #[cfg(not(target_arch = "wasm32"))] + tracing_subscriber::fmt::init(); + Todos::run(Settings { window: window::Settings { size: (500, 800), @@ -220,17 +224,19 @@ impl Application for Todos { tasks.iter().filter(|task| filter.matches(task)); let tasks: Element<_> = if filtered_tasks.count() > 0 { - column( + keyed_column( tasks .iter() .enumerate() .filter(|(_, task)| filter.matches(task)) .map(|(i, task)| { - task.view(i).map(move |message| { - Message::TaskMessage(i, message) - }) - }) - .collect(), + ( + task.id, + task.view(i).map(move |message| { + Message::TaskMessage(i, message) + }), + ) + }), ) .spacing(10) .into() @@ -279,6 +285,8 @@ impl Application for Todos { #[derive(Debug, Clone, Serialize, Deserialize)] struct Task { + #[serde(default = "Uuid::new_v4")] + id: Uuid, description: String, completed: bool, @@ -314,6 +322,7 @@ impl Task { fn new(description: String) -> Self { Task { + id: Uuid::new_v4(), description, completed: false, state: TaskState::Idle, diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 3387b48165..3e3a8ad7ed 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 { #[cfg(target_arch = "wasm32")] @@ -578,7 +578,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 23a19845f9..ff6986490e 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -30,6 +30,8 @@ half.workspace = true log.workspace = true raw-window-handle.workspace = true thiserror.workspace = true +cosmic-text.workspace = true +rustc-hash.workspace = true lyon_path.workspace = true lyon_path.optional = true @@ -39,3 +41,9 @@ image.optional = true kamadak-exif.workspace = true kamadak-exif.optional = true + +twox-hash.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +twox-hash.workspace = true +twox-hash.features = ["std"] diff --git a/tiny_skia/fonts/Iced-Icons.ttf b/graphics/fonts/Iced-Icons.ttf similarity index 100% rename from tiny_skia/fonts/Iced-Icons.ttf rename to graphics/fonts/Iced-Icons.ttf diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 59e95bf8b3..c2ac82bad9 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; - - /// Loads a [`Font`] from its bytes. + /// 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 2f29956e8d..3276c2d4bc 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -40,6 +40,32 @@ impl Damage for Primitive { 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 c584f3cd52..0bf7ec9794 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 4e814eb0d3..01a358cab9 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -10,7 +10,7 @@ #![forbid(rust_2018_idioms)] #![deny( missing_debug_implementations, - missing_docs, + //missing_docs, unsafe_code, unused_results, clippy::extra_unused_lifetimes, @@ -35,6 +35,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 7592a41067..8a97e6e7cd 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 { /// 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 { /// The shaping strategy of the text. shaping: text::Shaping, }, + /// A paragraph primitive + Paragraph { + /// The [`paragraph::Weak`] reference. + 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 c0cec60aad..d4df29a576 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 { backend: B, + default_font: Font, + default_text_size: Pixels, primitives: Vec>, theme: PhantomData, } impl Renderer { /// 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 Renderer { impl iced_core::Renderer for Renderer { type Theme = T; - fn layout( - &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,89 @@ impl iced_core::Renderer for Renderer { } } -impl text::Renderer for Renderer +impl core::text::Renderer for Renderer 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( + 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 update_paragraph( &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, - ) + paragraph: &mut Self::Paragraph, + text: Text<'_, Self::Font>, + ) { + let font_system = self.backend.font_system(); + + if paragraph.version() != font_system.version() { + // The font system has changed, paragraph fonts may be outdated + *paragraph = self.create_paragraph(text); + } else { + match core::text::compare(paragraph, text) { + core::text::Difference::None => {} + core::text::Difference::Bounds => { + self.resize_paragraph(paragraph, text.bounds); + } + core::text::Difference::Shape => { + *paragraph = self.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 { - 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 0000000000..bc06aa3c21 --- /dev/null +++ b/graphics/src/text.rs @@ -0,0 +1,137 @@ +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::borrow::Cow; +use std::sync::{self, Arc, RwLock}; + +#[allow(missing_debug_implementations)] +pub struct FontSystem { + raw: RwLock, + version: Version, +} + +impl FontSystem { + pub fn new() -> Self { + FontSystem { + raw: 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(), + )), + version: Version::default(), + } + } + + pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem { + self.raw.get_mut().expect("Lock font system") + } + + pub fn write( + &self, + ) -> (sync::RwLockWriteGuard<'_, cosmic_text::FontSystem>, Version) { + (self.raw.write().expect("Write font system"), self.version) + } + + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + let _ = self.get_mut().db_mut().load_font_source( + cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), + ); + + self.version = Version(self.version.0 + 1); + } + + pub fn version(&self) -> Version { + self.version + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct Version(u32); + +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 0000000000..577c468772 --- /dev/null +++ b/graphics/src/text/cache.rs @@ -0,0 +1,132 @@ +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, + aliases: FxHashMap, + recently_used: FxHashSet, + hasher: HashBuilder, +} + +#[cfg(not(target_arch = "wasm32"))] +type HashBuilder = twox_hash::RandomXxHashBuilder64; + +#[cfg(target_arch = "wasm32")] +type HashBuilder = std::hash::BuildHasherDefault; + +impl Cache { + pub fn new() -> Self { + Self::default() + } + + pub fn get(&self, key: &KeyHash) -> Option<&Entry> { + self.entries.get(key) + } + + pub 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.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.max(f32::MIN_POSITIVE), + ); + 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(Entry { + buffer, + min_bounds: 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 _ = 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(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; + +#[allow(missing_debug_implementations)] +pub struct Entry { + pub buffer: cosmic_text::Buffer, + pub min_bounds: Size, +} diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs new file mode 100644 index 0000000000..e4350cff75 --- /dev/null +++ b/graphics/src/text/paragraph.rs @@ -0,0 +1,302 @@ +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)] +pub struct Paragraph(Option>); + +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, + version: text::Version, +} + +impl Paragraph { + pub fn new() -> Self { + Self::default() + } + + pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self { + log::trace!("Allocating paragraph: {}", text.content); + + let (mut font_system, version) = 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(Some(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, + version, + }))) + } + + pub fn buffer(&self) -> &cosmic_text::Buffer { + &self.internal().buffer + } + + pub fn version(&self) -> text::Version { + self.internal().version + } + + pub fn downgrade(&self) -> Weak { + let paragraph = self.internal(); + + Weak { + raw: Arc::downgrade(paragraph), + min_bounds: paragraph.min_bounds, + horizontal_alignment: paragraph.horizontal_alignment, + vertical_alignment: paragraph.vertical_alignment, + } + } + + pub fn resize(&mut self, new_bounds: Size, font_system: &FontSystem) { + let paragraph = self + .0 + .take() + .expect("paragraph should always be initialized"); + + match Arc::try_unwrap(paragraph) { + Ok(mut internal) => { + let (mut font_system, _) = font_system.write(); + + internal.buffer.set_size( + &mut font_system, + new_bounds.width, + new_bounds.height, + ); + + internal.bounds = new_bounds; + internal.min_bounds = text::measure(&internal.buffer); + + self.0 = Some(Arc::new(internal)); + } + Err(internal) => { + let metrics = internal.buffer.metrics(); + + // If there is a strong reference somewhere, we recompute the + // buffer from scratch + *self = Self::with_text( + Text { + content: &internal.content, + bounds: internal.bounds, + size: Pixels(metrics.font_size), + line_height: LineHeight::Absolute(Pixels( + metrics.line_height, + )), + font: internal.font, + horizontal_alignment: internal.horizontal_alignment, + vertical_alignment: internal.vertical_alignment, + shaping: internal.shaping, + }, + font_system, + ); + } + } + } + + fn internal(&self) -> &Arc { + self.0 + .as_ref() + .expect("paragraph should always be initialized") + } +} + +impl core::text::Paragraph for Paragraph { + type Font = Font; + + fn content(&self) -> &str { + &self.internal().content + } + + fn text_size(&self) -> Pixels { + Pixels(self.internal().buffer.metrics().font_size) + } + + fn line_height(&self) -> LineHeight { + LineHeight::Absolute(Pixels( + self.internal().buffer.metrics().line_height, + )) + } + + fn font(&self) -> Font { + self.internal().font + } + + fn shaping(&self) -> Shaping { + self.internal().shaping + } + + fn horizontal_alignment(&self) -> alignment::Horizontal { + self.internal().horizontal_alignment + } + + fn vertical_alignment(&self) -> alignment::Vertical { + self.internal().vertical_alignment + } + + fn bounds(&self) -> Size { + self.internal().bounds + } + + fn min_bounds(&self) -> Size { + self.internal().min_bounds + } + + fn hit_test(&self, point: Point) -> Option { + let cursor = self.internal().buffer.hit(point.x, point.y)?; + + Some(Hit::CharOffset(cursor.index)) + } + + fn grapheme_position(&self, line: usize, index: usize) -> Option { + let run = self.internal().buffer.layout_runs().nth(line)?; + + // index represents a grapheme, not a glyph + // Let's find the first glyph for the given grapheme cluster + let mut last_start = None; + let mut graphemes_seen = 0; + + let glyph = run + .glyphs + .iter() + .find(|glyph| { + if graphemes_seen == index { + return true; + } + + if Some(glyph.start) != last_start { + last_start = Some(glyph.start); + graphemes_seen += 1; + } + + false + }) + .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 Default for Paragraph { + fn default() -> Self { + Self(Some(Arc::new(Internal::default()))) + } +} + +impl fmt::Debug for Paragraph { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let paragraph = self.internal(); + + f.debug_struct("Paragraph") + .field("content", ¶graph.content) + .field("font", ¶graph.font) + .field("shaping", ¶graph.shaping) + .field("horizontal_alignment", ¶graph.horizontal_alignment) + .field("vertical_alignment", ¶graph.vertical_alignment) + .field("bounds", ¶graph.bounds) + .field("min_bounds", ¶graph.min_bounds) + .finish() + } +} + +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, + version: text::Version::default(), + } + } +} + +#[derive(Debug, Clone)] +pub struct Weak { + raw: sync::Weak, + pub min_bounds: Size, + pub horizontal_alignment: alignment::Horizontal, + pub vertical_alignment: alignment::Vertical, +} + +impl Weak { + pub fn upgrade(&self) -> Option { + self.raw.upgrade().map(Some).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 8b17a4b0c7..d15000895d 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 6dcc0e968b..8bdf231d4c 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -29,7 +29,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; @@ -155,6 +158,7 @@ impl core::Renderer for Renderer { impl text::Renderer for Renderer { type Font = Font; + type Paragraph = Paragraph; const ICON_FONT: Font = iced_tiny_skia::Renderer::::ICON_FONT; const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::::CHECKMARK_ICON; @@ -165,59 +169,50 @@ impl text::Renderer for Renderer { 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 { + 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 318ae718ed..432eb8a0d4 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. @@ -10,7 +10,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. /// @@ -22,7 +22,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 165a85aee0..dae9e0ac6c 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( + &mut 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( + &mut 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( + &mut self.state, + renderer, &layout::Limits::new(Size::ZERO, self.bounds), ); diff --git a/src/settings.rs b/src/settings.rs index 8659603c1b..d9778d7e3b 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 { /// 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 a8add70b35..c721d96eaf 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, @@ -385,7 +405,7 @@ impl Backend { let clip_mask = (!physical_bounds.is_within(&clip_bounds)) .then_some(clip_mask as &_); - self.text_pipeline.draw( + self.text_pipeline.draw_cached( content, *bounds + translation, *color, @@ -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 { - 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 { + fn dimensions( + &self, + handle: &crate::core::image::Handle, + ) -> crate::core::Size { self.raster_pipeline.dimensions(handle) } } @@ -850,7 +829,7 @@ impl backend::Svg for Backend { fn viewport_dimensions( &self, handle: &crate::core::svg::Handle, - ) -> Size { + ) -> crate::core::Size { self.vector_pipeline.viewport_dimensions(handle) } } diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs index abffbfe6ab..ec27b2186e 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 dabbe95220..cb3ef54c7d 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,18 +1,18 @@ 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, + font_system: FontSystem, glyph_cache: GlyphCache, cache: RefCell, } @@ -20,31 +20,57 @@ 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]>) { - let _ = self.font_system.get_mut().db_mut().load_font_source( - cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), - ); + self.font_system.load_font(bytes); self.cache = RefCell::new(Cache::new()); } - pub fn draw( + pub fn draw_paragraph( + &mut self, + paragraph: ¶graph::Weak, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + use crate::core::text::Paragraph as _; + + let Some(paragraph) = paragraph.upgrade() else { + return; + }; + + draw( + self.font_system.get_mut(), + &mut self.glyph_cache, + paragraph.buffer(), + Rectangle::new(position, paragraph.min_bounds()), + color, + paragraph.horizontal_alignment(), + paragraph.vertical_alignment(), + scale_factor, + pixels, + clip_mask, + ); + } + + pub fn draw_cached( &mut self, content: &str, bounds: Rectangle, color: Color, - size: f32, + size: Pixels, line_height: LineHeight, font: Font, horizontal_alignment: alignment::Horizontal, @@ -54,197 +80,103 @@ 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 max_width = entry.bounds.width * scale_factor; - let total_height = entry.bounds.height * scale_factor; - - let bounds = bounds * scale_factor; - - let x = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => bounds.x - max_width / 2.0, - alignment::Horizontal::Right => bounds.x - max_width, - }; - - let y = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => bounds.y - total_height / 2.0, - alignment::Vertical::Bottom => bounds.y - total_height, - }; - - let mut swash = cosmic_text::SwashCache::new(); - - for run in entry.buffer.layout_runs() { - for glyph in run.glyphs { - let physical_glyph = glyph.physical((x, y), scale_factor); - - if let Some((buffer, placement)) = self.glyph_cache.allocate( - physical_glyph.cache_key, - color, - font_system, - &mut swash, - ) { - let pixmap = tiny_skia::PixmapRef::from_bytes( - buffer, - placement.width, - placement.height, - ) - .expect("Create glyph pixel map"); - - pixels.draw_pixmap( - physical_glyph.x + placement.left, - physical_glyph.y - placement.top - + (run.line_y * scale_factor).round() as i32, - pixmap, - &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::identity(), - clip_mask, - ); - } - } - } + let width = entry.min_bounds.width; + let height = entry.min_bounds.height; + + draw( + font_system, + &mut self.glyph_cache, + &entry.buffer, + Rectangle { + width, + height, + ..bounds + }, + color, + horizontal_alignment, + vertical_alignment, + scale_factor, + pixels, + clip_mask, + ); } pub fn trim_cache(&mut self) { 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 { - 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, +fn draw( + font_system: &mut cosmic_text::FontSystem, + glyph_cache: &mut GlyphCache, + buffer: &cosmic_text::Buffer, + bounds: Rectangle, + color: Color, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, +) { + let bounds = bounds * scale_factor; + + let x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - bounds.width / 2.0, + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + let y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - bounds.height / 2.0, + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + let mut swash = cosmic_text::SwashCache::new(); + + for run in buffer.layout_runs() { + for glyph in run.glyphs { + let physical_glyph = glyph.physical((x, y), scale_factor); + + if let Some((buffer, placement)) = glyph_cache.allocate( + physical_glyph.cache_key, + color, + font_system, + &mut swash, + ) { + let pixmap = tiny_skia::PixmapRef::from_bytes( + buffer, + placement.width, + placement.height, + ) + .expect("Create glyph pixel map"); + + pixels.draw_pixmap( + physical_glyph.x + placement.left, + physical_glyph.y - placement.top + + (run.line_y * scale_factor).round() as i32, + pixmap, + &tiny_skia::PixmapPaint::default(), + tiny_skia::Transform::identity(), + clip_mask, + ); + } + } } } @@ -358,138 +290,3 @@ impl GlyphCache { } } } - -struct Cache { - entries: FxHashMap, - measurements: FxHashMap, - recently_used: FxHashSet, - 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; - -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.line_height.max(f32::MIN_POSITIVE), - ); - 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(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 c0da5142ce..828e522f23 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -28,9 +28,16 @@ impl crate::graphics::Compositor for Compositor { 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( @@ -114,12 +121,12 @@ impl crate::graphics::Compositor for Compositor { } } -pub fn new(settings: Settings) -> (Compositor, Backend) { +pub fn new() -> (Compositor, Backend) { ( Compositor { _theme: PhantomData, }, - Backend::new(settings), + Backend::new(), ) } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 97594f1a75..a460c12783 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -33,8 +33,6 @@ guillotiere.workspace = true log.workspace = true once_cell.workspace = true raw-window-handle.workspace = true -rustc-hash.workspace = true -twox-hash.workspace = true wgpu.workspace = true lyon.workspace = true @@ -45,7 +43,3 @@ resvg.optional = true tracing.workspace = true tracing.optional = true - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -twox-hash.workspace = true -twox-hash.features = ["std"] diff --git a/wgpu/fonts/Iced-Icons.ttf b/wgpu/fonts/Iced-Icons.ttf deleted file mode 100644 index e3273141c4..0000000000 Binary files a/wgpu/fonts/Iced-Icons.ttf and /dev/null differ diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 68d1f80507..65c63f192b 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 { - 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 { + fn dimensions(&self, handle: &crate::core::image::Handle) -> Size { self.image_pipeline.dimensions(handle) } } #[cfg(feature = "svg")] impl backend::Svg for Backend { - fn viewport_dimensions(&self, handle: &core::svg::Handle) -> Size { + fn viewport_dimensions( + &self, + handle: &crate::core::svg::Handle, + ) -> Size { self.image_pipeline.viewport_dimensions(handle) } } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index b8f32db15a..7a5a0f7ce3 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 ba1bdca828..b61615d65d 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 b9f54560a6..2f48375143 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -23,7 +23,7 @@ #![forbid(rust_2018_idioms)] #![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 266a2c87fc..c9338fec3f 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 9c42be0e82..bd4f3e067c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,20 +1,16 @@ 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, + font_system: FontSystem, renderers: Vec, atlas: glyphon::TextAtlas, prepare_layer: usize, @@ -28,12 +24,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,10 +41,12 @@ 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())), - ); + self.font_system.load_font(bytes); self.cache = RefCell::new(Cache::new()); } @@ -63,7 +56,7 @@ impl Pipeline { device: &wgpu::Device, queue: &wgpu::Queue, sections: &[Text<'_>], - bounds: Rectangle, + layer_bounds: Rectangle, scale_factor: f32, target_size: Size, ) { @@ -80,97 +73,139 @@ 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: text.bounds.width, + height: text.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(§ion_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 = layer_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 entry = cache.get(key).expect("Get cached buffer"); + + ( + &entry.buffer, + Rectangle::new( + text.bounds.position(), + entry.min_bounds, + ), + text.horizontal_alignment, + text.vertical_alignment, + text.color, + ) + } + }; + + let bounds = bounds * scale_factor; + + let left = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => { + bounds.x - bounds.width / 2.0 + } + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + let top = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => { + bounds.y - bounds.height / 2.0 + } + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + let section_bounds = Rectangle { + x: left, + y: top, + ..bounds + }; + + let clip_bounds = layer_bounds.intersection(§ion_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,290 +254,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 { - 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, - aliases: FxHashMap, - recently_measured: FxHashSet, - recently_drawn: FxHashSet, - 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; - -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.max(f32::MIN_POSITIVE), - ); - 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(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 9e9c63db74..a9521a15cd 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -216,7 +216,14 @@ impl graphics::Compositor for Compositor { ) -> 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( diff --git a/widget/src/button.rs b/widget/src/button.rs index 18a95c9e24..4915bd49e0 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -159,19 +159,17 @@ where fn layout( &self, + tree: &mut 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( + &mut tree.children[0], + renderer, + limits, + ) + }) } fn operate( @@ -426,17 +424,16 @@ where } /// Computes the layout of a [`Button`]. -pub fn layout( - 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 1a18643229..390f4d9268 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -129,6 +129,7 @@ where fn layout( &self, + _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index e574c3cc17..d7fdf33961 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, + text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option, @@ -62,7 +61,7 @@ where const DEFAULT_SIZE: f32 = 20.0; /// The default spacing of a [`Checkbox`]. - const DEFAULT_SPACING: f32 = 15.0; + const DEFAULT_SPACING: f32 = 10.0; /// Creates a new [`Checkbox`]. /// @@ -118,7 +117,7 @@ where /// Sets the text size of the [`Checkbox`]. pub fn text_size(mut self, text_size: impl Into) -> 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::>() + } + + fn state(&self) -> tree::State { + tree::State::new(widget::text::State::::default()) + } + fn width(&self) -> Length { self.width } @@ -177,26 +184,35 @@ where fn layout( &self, + tree: &mut 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_mut::>(); + + 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 { /// The unicode code point that will be used as the icon. pub code_point: char, /// Font size of the content. - pub size: Option, + pub size: Option, /// 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 c16477f394..f2347cc99d 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -122,6 +122,7 @@ where fn layout( &self, + tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -138,6 +139,7 @@ where self.spacing, self.align_items, &self.children, + &mut tree.children, ) } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 0dc123543e..d69152818c 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -146,11 +146,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); @@ -181,7 +176,6 @@ pub struct State(RefCell>); #[derive(Debug, Clone)] struct Inner { - text_input: text_input::State, value: String, options: Vec, option_matchers: Vec, @@ -218,7 +212,6 @@ where ); Self(RefCell::new(Inner { - text_input: text_input::State::new(), value, options, option_matchers, @@ -226,51 +219,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(&self, f: impl FnOnce(&Inner) -> O) -> O { let inner = self.0.borrow(); @@ -290,21 +244,6 @@ where } } -impl Inner { - fn text_input_tree(&self) -> widget::Tree { - widget::Tree { - tag: widget::tree::Tag::of::(), - 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::().clone(); - } -} - impl Filtered where T: Clone, @@ -368,10 +307,11 @@ where fn layout( &self, + tree: &mut 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 { @@ -387,6 +327,10 @@ where }) } + fn children(&self) -> Vec { + vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _>)] + } + fn on_event( &mut self, tree: &mut widget::Tree, @@ -400,7 +344,13 @@ where ) -> event::Status { let menu = tree.state.downcast_mut::>(); - let started_focused = self.state.is_focused(); + let started_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + + 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; @@ -410,9 +360,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, @@ -421,7 +370,6 @@ where &mut local_shell, viewport, ); - self.state.update_text_input(tree); // Then finally react to them here for message in local_messages { @@ -452,7 +400,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.is_focused() + }; + + if is_focused { self.state.with_inner(|state| { if !started_focused { if let Some(on_option_hovered) = &mut self.on_option_hovered @@ -591,9 +547,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, )), @@ -604,21 +559,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.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(); } @@ -627,20 +586,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, @@ -648,16 +611,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.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>( @@ -666,14 +641,22 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option> { - let Menu { - menu, - filtered_options, - hovered_option, - .. - } = tree.state.downcast_mut::>(); + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + + text_input_state.is_focused() + }; + + if is_focused { + let Menu { + menu, + filtered_options, + hovered_option, + .. + } = tree.state.downcast_mut::>(); - 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 1f1df86171..ee7a496583 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 { - 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: &mut 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> { 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, +pub fn layout( limits: &layout::Limits, width: Length, height: Length, @@ -301,7 +307,7 @@ pub fn layout( 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( .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/helpers.rs b/widget/src/helpers.rs index 289720eb20..3c9c2b2986 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -6,6 +6,7 @@ use crate::container::{self, Container}; use crate::core; use crate::core::widget::operation; use crate::core::{Element, Length, Pixels}; +use crate::keyed; use crate::overlay; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; @@ -63,14 +64,22 @@ where } /// Creates a new [`Column`] with the given children. -/// -/// [`Column`]: crate::Column pub fn column( children: Vec>, ) -> Column<'_, Message, Renderer> { Column::with_children(children) } +/// Creates a new [`keyed::Column`] with the given children. +pub fn keyed_column<'a, Key, Message, Renderer>( + children: impl IntoIterator)>, +) -> keyed::Column<'a, Key, Message, Renderer> +where + Key: Copy + PartialEq, +{ + keyed::Column::with_children(children) +} + /// Creates a new [`Row`] with the given children. /// /// [`Row`]: crate::Row diff --git a/widget/src/image.rs b/widget/src/image.rs index 66bf215688..3c83c87bb2 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -167,6 +167,7 @@ where fn layout( &self, + _tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 6e09566793..724d121e95 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -105,6 +105,7 @@ where fn layout( &self, + _tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/keyed.rs b/widget/src/keyed.rs new file mode 100644 index 0000000000..ad531e6642 --- /dev/null +++ b/widget/src/keyed.rs @@ -0,0 +1,53 @@ +//! Use widgets that can provide hints to ensure continuity. +//! +//! # What is continuity? +//! Continuity is the feeling of persistence of state. +//! +//! In a graphical user interface, users expect widgets to have a +//! certain degree of continuous state. For instance, a text input +//! that is focused should stay focused even if the widget tree +//! changes slightly. +//! +//! Continuity is tricky in `iced` and the Elm Architecture because +//! the whole widget tree is rebuilt during every `view` call. This is +//! very convenient from a developer perspective because you can build +//! extremely dynamic interfaces without worrying about changing state. +//! +//! However, the tradeoff is that determining what changed becomes hard +//! for `iced`. If you have a list of things, adding an element at the +//! top may cause a loss of continuity on every element on the list! +//! +//! # How can we keep continuity? +//! The good news is that user interfaces generally have a static widget +//! structure. This structure can be relied on to ensure some degree of +//! continuity. `iced` already does this. +//! +//! However, sometimes you have a certain part of your interface that is +//! quite dynamic. For instance, a list of things where items may be added +//! or removed at any place. +//! +//! There are different ways to mitigate this during the reconciliation +//! stage, but they involve comparing trees at certain depths and +//! backtracking... Quite computationally expensive. +//! +//! One approach that is cheaper consists in letting the user provide some hints +//! about the identities of the different widgets so that they can be compared +//! directly without going deeper. +//! +//! The widgets in this module will all ask for a "hint" of some sort. In order +//! to help them keep continuity, you need to make sure the hint stays the same +//! for the same items in your user interface between `view` calls. +pub mod column; + +pub use column::Column; + +/// Creates a [`Column`] with the given children. +#[macro_export] +macro_rules! keyed_column { + () => ( + $crate::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::keyed::Column::with_children(vec![$($crate::core::Element::from($x)),+]) + ); +} diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs new file mode 100644 index 0000000000..19016679f3 --- /dev/null +++ b/widget/src/keyed/column.rs @@ -0,0 +1,320 @@ +//! Distribute content vertically. +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Operation; +use crate::core::{ + Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, + Shell, Widget, +}; + +/// A container that distributes its contents vertically. +#[allow(missing_debug_implementations)] +pub struct Column<'a, Key, Message, Renderer = crate::Renderer> +where + Key: Copy + PartialEq, +{ + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align_items: Alignment, + keys: Vec, + children: Vec>, +} + +impl<'a, Key, Message, Renderer> Column<'a, Key, Message, Renderer> +where + Key: Copy + PartialEq, +{ + /// Creates an empty [`Column`]. + pub fn new() -> Self { + Self::with_children(Vec::new()) + } + + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl IntoIterator)>, + ) -> Self { + let (keys, children) = children.into_iter().fold( + (Vec::new(), Vec::new()), + |(mut keys, mut children), (key, child)| { + keys.push(key); + children.push(child); + + (keys, children) + }, + ); + + Column { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + align_items: Alignment::Start, + keys, + children, + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Column`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Column`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`Column`]. + pub fn max_width(mut self, max_width: impl Into) -> Self { + self.max_width = max_width.into().0; + self + } + + /// Sets the horizontal alignment of the contents of the [`Column`] . + pub fn align_items(mut self, align: Alignment) -> Self { + self.align_items = align; + self + } + + /// Adds an element to the [`Column`]. + pub fn push( + mut self, + key: Key, + child: impl Into>, + ) -> Self { + self.keys.push(key); + self.children.push(child.into()); + self + } +} + +impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer> +where + Key: Copy + PartialEq, +{ + fn default() -> Self { + Self::new() + } +} + +struct State +where + Key: Copy + PartialEq, +{ + keys: Vec, +} + +impl<'a, Key, Message, Renderer> Widget + for Column<'a, Key, Message, Renderer> +where + Renderer: crate::core::Renderer, + Key: Copy + PartialEq + 'static, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + keys: self.keys.clone(), + }) + } + + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + let Tree { + state, children, .. + } = tree; + + let state = state.downcast_mut::>(); + + tree::diff_children_custom_with_search( + children, + &self.children, + |tree, child| child.as_widget().diff(tree), + |index| { + self.keys.get(index).or_else(|| self.keys.last()).copied() + != Some(state.keys[index]) + }, + |child| Tree::new(child.as_widget()), + ); + + if state.keys != self.keys { + state.keys = self.keys.clone(); + } + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .max_width(self.max_width) + .width(self.width) + .height(self.height); + + layout::flex::resolve( + layout::flex::Axis::Vertical, + renderer, + &limits, + self.padding, + self.spacing, + self.align_items, + &self.children, + &mut tree.children, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }) + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } +} + +impl<'a, Key, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Key: Copy + PartialEq + 'static, + Message: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(column: Column<'a, Key, Message, Renderer>) -> Self { + Self::new(column) + } +} diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 761f45ad7d..bf695a579f 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -152,11 +152,14 @@ where fn layout( &self, + tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.with_element(|element| { - element.as_widget().layout(renderer, limits) + element + .as_widget() + .layout(&mut tree.children[0], renderer, limits) }) } @@ -326,7 +329,7 @@ where Renderer: core::Renderer, { fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 19df279284..fe99a7f2cd 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -254,11 +254,18 @@ where fn layout( &self, + tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { + let t = tree.state.downcast_mut::>>>(); + self.with_element(|element| { - element.as_widget().layout(renderer, limits) + element.as_widget().layout( + &mut t.borrow_mut().as_mut().unwrap().children[0], + renderer, + limits, + ) }) } @@ -566,7 +573,7 @@ where S: 'static + Default, { fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index b56545c864..0b819455af 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: &mut 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, { 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: &mut 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, @@ -362,7 +363,7 @@ where Renderer: core::Renderer, { fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 5cb0f8def4..7e204171ac 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -30,6 +30,7 @@ pub mod button; pub mod checkbox; pub mod combo_box; pub mod container; +pub mod keyed; pub mod overlay; pub mod pane_grid; pub mod pick_list; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 490f7c4827..65d44dd5d7 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -120,10 +120,11 @@ where fn layout( &self, + tree: &mut 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 711a88912f..b293f9fa09 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, + text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option, @@ -85,7 +85,7 @@ where /// Sets the text size of the [`Menu`]. pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); + self.text_size = Some(text_size.into()); self } @@ -232,7 +232,7 @@ where Renderer::Theme: StyleSheet + container::StyleSheet, { fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, @@ -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 Message + 'a>, on_option_hovered: Option<&'a dyn Fn(T) -> Message>, padding: Padding, - text_size: Option, + text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option, @@ -352,6 +352,7 @@ where fn layout( &self, + _tree: &mut 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 408336226f..f868a64812 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -275,10 +275,12 @@ where fn layout( &self, + tree: &mut 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( + tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, node: &Node, @@ -478,19 +483,26 @@ pub fn layout( height: Length, spacing: f32, contents: impl Iterator, - layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, + layout_content: impl Fn( + T, + &mut 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_mut()) + .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 218adcd59e..826ea66349 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: &mut 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( + &mut 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( + &mut tree.children[0], renderer, &layout::Limits::new( Size::ZERO, @@ -179,7 +184,11 @@ where vec![title_bar_layout, body_layout], ) } else { - self.body.as_widget().layout(renderer, limits) + self.body.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ) } } diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 47457337f3..5ae7a6a008 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: &mut 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( + &mut 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( + &mut 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 26fe5ca2fb..056a5e65d0 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -7,12 +7,12 @@ 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; @@ -35,7 +35,7 @@ where selected: Option, width: Length, padding: Padding, - text_size: Option, + text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option, @@ -101,7 +101,7 @@ where /// Sets the text size of the [`PickList`]. pub fn text_size(mut self, size: impl Into) -> Self { - self.text_size = Some(size.into().0); + self.text_size = Some(size.into()); self } @@ -157,11 +157,11 @@ where From<::Style>, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::>() } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::::new()) } fn width(&self) -> Length { @@ -174,10 +174,12 @@ where fn layout( &self, + tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { layout( + tree.state.downcast_mut::>(), renderer, limits, self.width, @@ -210,7 +212,7 @@ where self.on_selected.as_ref(), self.selected.as_ref(), &self.options, - || tree.state.downcast_mut::(), + || tree.state.downcast_mut::>(), ) } @@ -250,7 +252,7 @@ where self.selected.as_ref(), &self.handle, &self.style, - || tree.state.downcast_ref::(), + || tree.state.downcast_ref::>(), ) } @@ -260,7 +262,7 @@ where layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - let state = tree.state.downcast_mut::(); + let state = tree.state.downcast_mut::>(); overlay( layout, @@ -295,28 +297,32 @@ where } } -/// The local state of a [`PickList`]. +/// The state of a [`PickList`]. #[derive(Debug)] -pub struct State { +pub struct State { menu: menu::State, keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option, + options: Vec

, + placeholder: P, } -impl State { +impl State

{ /// 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(), + options: Vec::new(), + placeholder: P::default(), } } } -impl Default for State { +impl Default for State

{ fn default() -> Self { Self::new() } @@ -330,7 +336,7 @@ pub enum Handle { /// This is the default. Arrow { /// Font size of the content. - size: Option, + size: Option, }, /// A custom static handle. Static(Icon), @@ -359,7 +365,7 @@ pub struct Icon { /// The unicode code point that will be used as the icon. pub code_point: char, /// Font size of the content. - pub size: Option, + pub size: Option, /// Line height of the content. pub line_height: text::LineHeight, /// The shaping strategy of the icon. @@ -368,11 +374,12 @@ pub struct Icon { /// Computes the layout of a [`PickList`]. pub fn layout( + state: &mut State, renderer: &Renderer, limits: &layout::Limits, width: Length, padding: Padding, - text_size: Option, + text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option, @@ -386,38 +393,67 @@ 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() - }; + state.options.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(state.options.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 { + renderer.update_paragraph( + &mut state.placeholder, + Text { + content: placeholder, + ..option_text + }, + ); + } - labels_width.max(placeholder_width) + let max_width = match width { + Length::Shrink => { + let labels_width = + state.options.iter().fold(0.0, |width, paragraph| { + f32::max(width, paragraph.min_width()) + }); + + labels_width.max( + placeholder + .map(|_| state.placeholder.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 +464,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 +472,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

, ) -> event::Status where T: PartialEq + Clone + 'a, + P: text::Paragraph + 'a, { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) @@ -534,9 +571,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, padding: Padding, - text_size: Option, + text_size: Option, text_shaping: text::Shaping, font: Renderer::Font, options: &'a [T], @@ -591,7 +628,7 @@ pub fn draw<'a, T, Renderer>( layout: Layout<'_>, cursor: mouse::Cursor, padding: Padding, - text_size: Option, + text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Renderer::Font, @@ -599,7 +636,7 @@ pub fn draw<'a, T, Renderer>( selected: Option<&T>, handle: &Handle, style: &::Style, - state: impl FnOnce() -> &'a State, + state: impl FnOnce() -> &'a State, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -665,22 +702,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 +729,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 37c6bc72e7..07de72d5ca 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -95,6 +95,7 @@ where fn layout( &self, + _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 75409091ab..1dc4da7fac 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -60,6 +60,7 @@ impl<'a, Message, Theme> Widget> for QRCode<'a> { fn layout( &self, + _tree: &mut Tree, _renderer: &Renderer, _limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 12bbd9c73b..57acc03384 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, + text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option, @@ -152,7 +152,7 @@ where /// Sets the text size of the [`Radio`] button. pub fn text_size(mut self, text_size: impl Into) -> 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::>() + } + + fn state(&self) -> tree::State { + tree::State::new(widget::text::State::::default()) + } + fn width(&self) -> Length { self.width } @@ -203,25 +211,35 @@ where fn layout( &self, + tree: &mut 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_mut::>(); + + 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 99b2a0bf0a..71cf05097f 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -114,6 +114,7 @@ where fn layout( &self, + tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -127,6 +128,7 @@ where self.spacing, self.align_items, &self.children, + &mut tree.children, ) } diff --git a/widget/src/rule.rs b/widget/src/rule.rs index d703e6ae97..b5c5fa5576 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -72,6 +72,7 @@ where fn layout( &self, + _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index d0c77e6b55..def28821eb 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -230,6 +230,7 @@ where fn layout( &self, + tree: &mut 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( + &mut tree.children[0], + renderer, + limits, + ) }, ) } diff --git a/widget/src/slider.rs b/widget/src/slider.rs index bd73ea79b4..2c4a291394 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -169,6 +169,7 @@ where fn layout( &self, + _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/space.rs b/widget/src/space.rs index 9a5385e8ba..e5a8f1698c 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -55,6 +55,7 @@ where fn layout( &self, + _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 1ccc5d621c..2d01d1ab0a 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -106,6 +106,7 @@ where fn layout( &self, + _tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index f7a90880a0..bfd196fd2c 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; @@ -67,7 +67,7 @@ where font: Option, width: Length, padding: Padding, - size: Option, + size: Option, line_height: text::LineHeight, on_input: Option Message + 'a>>, on_paste: Option Message + 'a>>, @@ -178,7 +178,7 @@ where /// Sets the text size of the [`TextInput`]. pub fn size(mut self, size: impl Into) -> Self { - self.size = Some(size.into().0); + self.size = Some(size.into()); self } @@ -218,12 +218,8 @@ where theme, layout, cursor, - tree.state.downcast_ref::(), + tree.state.downcast_ref::>(), 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 +236,15 @@ where Renderer::Theme: StyleSheet, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::>() } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::::new()) } fn diff(&self, tree: &mut Tree) { - let state = tree.state.downcast_mut::(); + let state = tree.state.downcast_mut::>(); // Unfocus text input if it becomes disabled if self.on_input.is_none() { @@ -269,6 +265,7 @@ where fn layout( &self, + tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -278,8 +275,13 @@ where self.width, self.padding, self.size, + self.font, self.line_height, self.icon.as_ref(), + tree.state.downcast_mut::>(), + &self.value, + &self.placeholder, + self.is_secure, ) } @@ -290,7 +292,7 @@ where _renderer: &Renderer, operation: &mut dyn Operation, ) { - let state = tree.state.downcast_mut::(); + let state = tree.state.downcast_mut::>(); 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 +324,7 @@ where self.on_input.as_deref(), self.on_paste.as_deref(), &self.on_submit, - || tree.state.downcast_mut::(), + || tree.state.downcast_mut::>(), ) } @@ -341,12 +343,8 @@ where theme, layout, cursor, - tree.state.downcast_ref::(), + tree.state.downcast_ref::>(), &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 +386,7 @@ pub struct Icon { /// The unicode code point that will be used as the icon. pub code_point: char, /// The font size of the content. - pub size: Option, + pub size: Option, /// 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 +463,68 @@ pub fn layout( limits: &layout::Limits, width: Length, padding: Padding, - size: Option, + size: Option, + font: Option, line_height: text::LineHeight, icon: Option<&Icon>, + state: &mut State, + 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); + 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, placeholder_text); + + let secure_value = is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + renderer.update_paragraph( + &mut state.value, + Text { + content: &value.to_string(), + ..placeholder_text + }, + ); + 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 icon_text = Text { + line_height, + content: &icon.code_point.to_string(), + font: icon.font, + size: icon.size.unwrap_or_else(|| renderer.default_size()), + bounds: Size::new(f32::INFINITY, text_bounds.height), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }; + + renderer.update_paragraph(&mut state.icon, icon_text); + + let icon_width = state.icon.min_width(); let mut text_node = layout::Node::new( text_bounds - Size::new(icon_width + icon.spacing, 0.0), @@ -537,19 +574,31 @@ pub fn update<'a, Message, Renderer>( clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, value: &mut Value, - size: Option, + size: Option, line_height: text::LineHeight, font: Option, is_secure: bool, on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, on_submit: &Option, - state: impl FnOnce() -> &'a mut State, + state: impl FnOnce() -> &'a mut State, ) -> 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 +641,7 @@ where }; find_cursor_position( - renderer, text_layout.bounds(), - font, - size, - line_height, &value, state, target, @@ -621,11 +666,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 +712,7 @@ where }; let position = find_cursor_position( - renderer, text_layout.bounds(), - font, - size, - line_height, &value, state, target, @@ -710,6 +747,8 @@ where focus.updated_at = Instant::now(); + update_cache(state, value); + return event::Status::Captured; } } @@ -749,6 +788,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 +810,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 +887,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 +921,8 @@ where shell.publish(message); state.is_pasting = Some(content); + + update_cache(state, value); } else { state.is_pasting = None; } @@ -979,12 +1026,8 @@ pub fn draw( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - state: &State, + state: &State, value: &Value, - placeholder: &str, - size: Option, - line_height: text::LineHeight, - font: Option, is_disabled: bool, is_secure: bool, icon: Option<&Icon>, @@ -1023,28 +1066,17 @@ pub fn draw( appearance.background, ); - if let Some(icon) = icon { + if icon.is_some() { 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, - }); + renderer.fill_paragraph( + &state.icon, + icon_layout.bounds().center(), + appearance.icon_color, + ); } let text = value.to_string(); - let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size()); let (cursor, offset) = if let Some(focus) = state .is_focused @@ -1055,12 +1087,9 @@ pub fn draw( cursor::State::Index(position) => { let (text_value_width, offset) = measure_cursor_and_scroll_offset( - renderer, + &state.value, text_bounds, - value, - size, position, - font, ); let is_cursor_visible = ((focus.now - focus.updated_at) @@ -1096,22 +1125,16 @@ pub fn draw( let (left_position, left_offset) = measure_cursor_and_scroll_offset( - renderer, + &state.value, text_bounds, - value, - size, left, - font, ); let (right_position, right_offset) = measure_cursor_and_scroll_offset( - renderer, + &state.value, text_bounds, - value, - size, right, - font, ); let width = right_position - left_position; @@ -1143,12 +1166,7 @@ pub fn draw( (None, 0.0) }; - let text_width = renderer.measure_width( - if text.is_empty() { placeholder } else { &text }, - size, - font, - text::Shaping::Advanced, - ); + let text_width = state.value.min_width(); let render = |renderer: &mut Renderer| { if let Some((cursor, color)) = cursor { @@ -1157,27 +1175,21 @@ pub fn draw( renderer.with_translation(Vector::ZERO, |_| {}); } - renderer.fill_text(Text { - content: if text.is_empty() { placeholder } else { &text }, - color: if text.is_empty() { + renderer.fill_paragraph( + if text.is_empty() { + &state.placeholder + } else { + &state.value + }, + 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 +1220,10 @@ pub fn mouse_interaction( /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] -pub struct State { +pub struct State { + value: P, + placeholder: P, + icon: P, is_focused: Option, is_dragging: bool, is_pasting: Option, @@ -1225,7 +1240,7 @@ struct Focus { is_window_focused: bool, } -impl State { +impl State

{ /// Creates a new [`State`], representing an unfocused [`TextInput`]. pub fn new() -> Self { Self::default() @@ -1234,6 +1249,9 @@ impl State { /// Creates a new [`State`], representing a focused [`TextInput`]. pub fn focused() -> Self { Self { + value: P::default(), + placeholder: P::default(), + icon: P::default(), is_focused: None, is_dragging: false, is_pasting: None, @@ -1292,7 +1310,7 @@ impl State { } } -impl operation::Focusable for State { +impl operation::Focusable for State

{ fn is_focused(&self) -> bool { State::is_focused(self) } @@ -1306,7 +1324,7 @@ impl operation::Focusable for State { } } -impl operation::TextInput for State { +impl operation::TextInput for State

{ fn move_cursor_to_front(&mut self) { State::move_cursor_to_front(self) } @@ -1336,17 +1354,11 @@ mod platform { } } -fn offset( - renderer: &Renderer, +fn offset( text_bounds: Rectangle, - font: Renderer::Font, - size: f32, value: &Value, - state: &State, -) -> f32 -where - Renderer: text::Renderer, -{ + state: &State

, +) -> f32 { if state.is_focused() { let cursor = state.cursor(); @@ -1356,12 +1368,9 @@ where }; let (_, offset) = measure_cursor_and_scroll_offset( - renderer, + &state.value, text_bounds, - value, - size, focus_position, - font, ); offset @@ -1370,63 +1379,34 @@ where } } -fn measure_cursor_and_scroll_offset( - 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(); - - let text_value_width = renderer.measure_width( - &text_before_cursor, - size, - font, - text::Shaping::Advanced, - ); +) -> (f32, f32) { + let grapheme_position = paragraph + .grapheme_position(0, cursor_index) + .unwrap_or(Point::ORIGIN); - let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + let offset = ((grapheme_position.x + 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, +fn find_cursor_position( text_bounds: Rectangle, - font: Option, - size: Option, - line_height: text::LineHeight, value: &Value, - state: &State, + state: &State

, x: f32, -) -> Option -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 { + 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 + .value + .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) .map(text::Hit::cursor)?; Some( @@ -1438,4 +1418,33 @@ where ) } +fn replace_paragraph( + renderer: &Renderer, + state: &mut State, + layout: Layout<'_>, + value: &Value, + font: Option, + text_size: Option, + 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.value = 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 3793f5b068..2440317fa8 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, width: Length, size: f32, - text_size: Option, + text_size: Option, text_line_height: text::LineHeight, text_alignment: alignment::Horizontal, text_shaping: text::Shaping, @@ -85,7 +85,7 @@ where text_line_height: text::LineHeight::default(), text_alignment: alignment::Horizontal::Left, text_shaping: text::Shaping::Basic, - spacing: 0.0, + spacing: Self::DEFAULT_SIZE / 2.0, font: None, style: Default::default(), } @@ -105,7 +105,7 @@ where /// Sets the text size o the [`Toggler`]. pub fn text_size(mut self, text_size: impl Into) -> 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::>() + } + + fn state(&self) -> tree::State { + tree::State::new(widget::text::State::::default()) + } + fn width(&self) -> Length { self.width } @@ -170,32 +178,41 @@ where fn layout( &self, + tree: &mut 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_mut::>(); + + 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 faa3f3e185..534d901a02 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 { - vec![widget::Tree::new(&self.content)] + vec![ + widget::Tree::new(&self.content), + widget::Tree::new(&self.tooltip as &dyn Widget), + ] } 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: &mut 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> { let state = tree.state.downcast_ref::(); + 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 mut widget::Tree, cursor_position: Point, content_bounds: Rectangle, snap_within_viewport: bool, @@ -311,7 +319,7 @@ where Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, { fn layout( - &self, + &mut self, renderer: &Renderer, bounds: Size, position: Point, @@ -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 efca302a63..1efcd63b45 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -166,6 +166,7 @@ where fn layout( &self, + _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node {