diff options
90 files changed, 2692 insertions, 1786 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index fd930a36..fdd832e4 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 d2c6358b..02f16bcb 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 04954fb9..caf315b6 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 8b967849..c02b63d8 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -19,6 +19,7 @@  use crate::Element;  use crate::layout::{Limits, Node}; +use crate::widget;  use crate::{Alignment, Padding, Point, Size};  /// The main axis of a flex layout. @@ -66,6 +67,7 @@ pub fn resolve<Message, Renderer>(      spacing: f32,      align_items: Alignment,      items: &[Element<'_, Message, Renderer>], +    trees: &mut [widget::Tree],  ) -> Node  where      Renderer: crate::Renderer, @@ -81,7 +83,7 @@ where      let mut nodes: Vec<Node> = Vec::with_capacity(items.len());      nodes.resize(items.len(), Node::default()); -    for (i, child) in items.iter().enumerate() { +    for (i, (child, tree)) in items.iter().zip(trees.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 2e05db93..f71f25f7 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 29b404b8..689e69be 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 691686cd..a0bae6bb 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 7c73d2e4..1b327e56 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -5,26 +5,13 @@ mod null;  #[cfg(debug_assertions)]  pub use null::Null; -use crate::layout; -use crate::{Background, BorderRadius, Color, Element, Rectangle, Vector}; +use crate::{Background, BorderRadius, Color, Rectangle, Vector};  /// A component that can be used by widgets to draw themselves on a screen.  pub trait Renderer: Sized {      /// The supported theme of the [`Renderer`].      type Theme; -    /// Lays out the elements of a user interface. -    /// -    /// You should override this if you need to perform any operations before or -    /// after layouting. For instance, trimming the measurements cache. -    fn layout<Message>( -        &mut self, -        element: &Element<'_, Message, Self>, -        limits: &layout::Limits, -    ) -> layout::Node { -        element.as_widget().layout(self, limits) -    } -      /// Draws the primitives recorded in the given closure in a new layer.      ///      /// The layer will clip its contents to the provided `bounds`. diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 5d49699e..55d58a59 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -1,6 +1,7 @@ +use crate::alignment;  use crate::renderer::{self, Renderer};  use crate::text::{self, Text}; -use crate::{Background, Font, Point, Rectangle, Size, Vector}; +use crate::{Background, Color, Font, Pixels, Point, Rectangle, Size, Vector};  use std::borrow::Cow; @@ -41,6 +42,7 @@ impl Renderer for Null {  impl text::Renderer for Null {      type Font = Font; +    type Paragraph = ();      const ICON_FONT: Font = Font::DEFAULT;      const CHECKMARK_ICON: char = '0'; @@ -50,37 +52,83 @@ impl text::Renderer for Null {          Font::default()      } -    fn default_size(&self) -> f32 { -        16.0 +    fn default_size(&self) -> Pixels { +        Pixels(16.0)      }      fn load_font(&mut self, _font: Cow<'static, [u8]>) {} -    fn measure( -        &self, -        _content: &str, -        _size: f32, -        _line_height: text::LineHeight, -        _font: Font, -        _bounds: Size, -        _shaping: text::Shaping, -    ) -> Size { -        Size::new(0.0, 20.0) +    fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph {      } -    fn hit_test( +    fn resize_paragraph(          &self, -        _contents: &str, -        _size: f32, -        _line_height: text::LineHeight, -        _font: Self::Font, -        _bounds: Size, -        _shaping: text::Shaping, -        _point: Point, -        _nearest_only: bool, -    ) -> Option<text::Hit> { +        _paragraph: &mut Self::Paragraph, +        _new_bounds: Size, +    ) { +    } + +    fn fill_paragraph( +        &mut self, +        _paragraph: &Self::Paragraph, +        _position: Point, +        _color: Color, +    ) { +    } + +    fn fill_text( +        &mut self, +        _paragraph: Text<'_, Self::Font>, +        _position: Point, +        _color: Color, +    ) { +    } +} + +impl text::Paragraph for () { +    type Font = Font; + +    fn content(&self) -> &str { +        "" +    } + +    fn text_size(&self) -> Pixels { +        Pixels(16.0) +    } + +    fn font(&self) -> Self::Font { +        Font::default() +    } + +    fn line_height(&self) -> text::LineHeight { +        text::LineHeight::default() +    } + +    fn shaping(&self) -> text::Shaping { +        text::Shaping::default() +    } + +    fn horizontal_alignment(&self) -> alignment::Horizontal { +        alignment::Horizontal::Left +    } + +    fn vertical_alignment(&self) -> alignment::Vertical { +        alignment::Vertical::Top +    } + +    fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> {          None      } -    fn fill_text(&mut self, _text: Text<'_, Self::Font>) {} +    fn bounds(&self) -> Size { +        Size::ZERO +    } + +    fn min_bounds(&self) -> Size { +        Size::ZERO +    } + +    fn hit_test(&self, _point: Point) -> Option<text::Hit> { +        None +    }  } diff --git a/core/src/text.rs b/core/src/text.rs index fc8aa20e..0e3617b1 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -1,6 +1,6 @@  //! Draw and interact with text.  use crate::alignment; -use crate::{Color, Pixels, Point, Rectangle, Size}; +use crate::{Color, Pixels, Point, Size};  use std::borrow::Cow;  use std::hash::{Hash, Hasher}; @@ -12,17 +12,14 @@ pub struct Text<'a, Font> {      pub content: &'a str,      /// The bounds of the paragraph. -    pub bounds: Rectangle, +    pub bounds: Size,      /// The size of the [`Text`] in logical pixels. -    pub size: f32, +    pub size: Pixels,      /// The line height of the [`Text`].      pub line_height: LineHeight, -    /// The color of the [`Text`]. -    pub color: Color, -      /// The font of the [`Text`].      pub font: Font, @@ -132,7 +129,10 @@ impl Hit {  /// A renderer capable of measuring and drawing [`Text`].  pub trait Renderer: crate::Renderer {      /// The font type used. -    type Font: Copy; +    type Font: Copy + PartialEq; + +    /// The [`Paragraph`] of this [`Renderer`]. +    type Paragraph: Paragraph<Font = Self::Font> + 'static;      /// The icon font of the backend.      const ICON_FONT: Self::Font; @@ -151,62 +151,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<Hit>; + +    /// Returns the distance to the given grapheme index in the [`Paragraph`]. +    fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; + +    /// Returns the minimum width that can fit the contents of the [`Paragraph`]. +    fn min_width(&self) -> f32 { +        self.min_bounds().width +    } + +    /// Returns the minimum height that can fit the contents of the [`Paragraph`]. +    fn min_height(&self) -> f32 { +        self.min_bounds().height      } +} -    /// 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<Hit>; +    /// 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<Font: PartialEq>( +    paragraph: &impl Paragraph<Font = Font>, +    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 d6a99208..294d5984 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 79df2b02..53ed463e 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<f32>, +    size: Option<Pixels>,      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<Pixels>) -> 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: Paragraph>(P); +  impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer>  where      Renderer: text::Renderer,      Renderer::Theme: StyleSheet,  { +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<State<Renderer::Paragraph>>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(State(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::<State<Renderer::Paragraph>>(), +            renderer, +            limits, +            self.width, +            self.height,              &self.content, -            size,              self.line_height, -            self.font.unwrap_or_else(|| renderer.default_font()), -            limits.max(), +            self.size, +            self.font, +            self.horizontal_alignment, +            self.vertical_alignment,              self.shaping, -        ); - -        let size = limits.resolve(bounds); - -        layout::Node::new(size) +        )      }      fn draw(          &self, -        _state: &Tree, +        tree: &Tree,          renderer: &mut Renderer,          theme: &Renderer::Theme,          style: &renderer::Style, @@ -163,22 +174,63 @@ where          _cursor_position: mouse::Cursor,          _viewport: &Rectangle,      ) { +        let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); +          draw(              renderer,              style,              layout, -            &self.content, -            self.size, -            self.line_height, -            self.font, +            state,              theme.appearance(self.style.clone()), -            self.horizontal_alignment, -            self.vertical_alignment, -            self.shaping,          );      }  } +/// Produces the [`layout::Node`] of a [`Text`] widget. +pub fn layout<Renderer>( +    state: &mut State<Renderer::Paragraph>, +    renderer: &Renderer, +    limits: &layout::Limits, +    width: Length, +    height: Length, +    content: &str, +    line_height: LineHeight, +    size: Option<Pixels>, +    font: Option<Renderer::Font>, +    horizontal_alignment: alignment::Horizontal, +    vertical_alignment: alignment::Vertical, +    shaping: Shaping, +) -> layout::Node +where +    Renderer: text::Renderer, +{ +    let limits = limits.width(width).height(height); +    let bounds = limits.max(); + +    let size = size.unwrap_or_else(|| renderer.default_size()); +    let font = font.unwrap_or_else(|| renderer.default_font()); + +    let 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>(      renderer: &mut Renderer,      style: &renderer::Style,      layout: Layout<'_>, -    content: &str, -    size: Option<f32>, -    line_height: LineHeight, -    font: Option<Renderer::Font>, +    state: &State<Renderer::Paragraph>,      appearance: Appearance, -    horizontal_alignment: alignment::Horizontal, -    vertical_alignment: alignment::Vertical, -    shaping: Shaping,  ) where      Renderer: text::Renderer,  { +    let 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<Text<'a, Renderer>> diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 0af40c33..202cca9a 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<T>( +    current_children: &mut Vec<Tree>, +    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 736a9d53..7dc981d9 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -3,8 +3,8 @@ use iced::mouse;  use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};  use iced::widget::{column, row, text, Slider};  use iced::{ -    Color, Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, -    Size, Vector, +    Color, Element, Length, Pixels, Point, Rectangle, Renderer, Sandbox, +    Settings, Size, Vector,  };  use palette::{      self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue, @@ -168,7 +168,7 @@ impl Theme {          let mut text = canvas::Text {              horizontal_alignment: alignment::Horizontal::Center,              vertical_alignment: alignment::Vertical::Top, -            size: 15.0, +            size: Pixels(15.0),              ..canvas::Text::default()          }; diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index 2e6f95d5..4f347667 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -40,7 +40,6 @@ impl Sandbox for Example {              Message::Selected(language) => {                  self.selected_language = Some(language);                  self.text = language.hello().to_string(); -                self.languages.unfocus();              }              Message::OptionHovered(language) => {                  self.text = language.hello().to_string(); diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index 4b300116..13b08250 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 713bc62d..32a14cbe 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 39c1609d..e451cb06 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -591,7 +591,7 @@ mod grid {                  let text = Text {                      color: Color::WHITE, -                    size: 14.0, +                    size: 14.0.into(),                      position: Point::new(frame.width(), frame.height()),                      horizontal_alignment: alignment::Horizontal::Right,                      vertical_alignment: alignment::Vertical::Bottom, diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 3bc7f46b..8ab3b493 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 d0b2d891..7945bd20 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<dyn std::error::Error>> {      // Initialize iced      let mut debug = Debug::new(); -    let mut renderer = Renderer::new(Backend::new( -        &device, -        &queue, -        Settings::default(), -        format, -    )); +    let mut renderer = Renderer::new( +        Backend::new(&device, &queue, Settings::default(), format), +        Font::default(), +        Pixels(16.0), +    );      let mut state = program::State::new(          controls, diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 3898d76e..bf01c3b4 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<Theme>,          limits: &layout::Limits,      ) -> layout::Node { diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index 20fbe9f3..c5bb4791 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 4aa70886..c050d3cc 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 47b272a9..8570a38e 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 3334d84f..3c62bfbc 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 62c17926..501bf67e 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<text_input::Id> = 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 3387b481..3e3a8ad7 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 23a19845..ff698649 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.ttfBinary files differ index e3273141..e3273141 100644 --- a/tiny_skia/fonts/Iced-Icons.ttf +++ b/graphics/fonts/Iced-Icons.ttf diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 59e95bf8..c2ac82ba 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -1,8 +1,8 @@  //! Write a graphics backend. -use iced_core::image; -use iced_core::svg; -use iced_core::text; -use iced_core::{Font, Point, Size}; +use crate::core::image; +use crate::core::svg; +use crate::core::Size; +use crate::text;  use std::borrow::Cow; @@ -12,70 +12,15 @@ use std::borrow::Cow;  pub trait Backend {      /// The custom kind of primitives this [`Backend`] supports.      type Primitive; - -    /// Trims the measurements cache. -    /// -    /// This method is currently necessary to properly trim the text cache in -    /// `iced_wgpu` and `iced_glow` because of limitations in the text rendering -    /// pipeline. It will be removed in the future. -    fn trim_measurements(&mut self) {}  }  /// A graphics backend that supports text rendering.  pub trait Text { -    /// The icon font of the backend. -    const ICON_FONT: Font; - -    /// The `char` representing a ✔ icon in the [`ICON_FONT`]. -    /// -    /// [`ICON_FONT`]: Self::ICON_FONT -    const CHECKMARK_ICON: char; - -    /// The `char` representing a â–¼ icon in the built-in [`ICON_FONT`]. -    /// -    /// [`ICON_FONT`]: Self::ICON_FONT -    const ARROW_DOWN_ICON: char; - -    /// Returns the default [`Font`]. -    fn default_font(&self) -> Font; - -    /// Returns the default size of text. -    fn default_size(&self) -> f32; - -    /// Measures the text contents with the given size and font, -    /// returning the size of a laid out paragraph that fits in the provided -    /// bounds. -    fn measure( -        &self, -        contents: &str, -        size: f32, -        line_height: text::LineHeight, -        font: Font, -        bounds: Size, -        shaping: text::Shaping, -    ) -> Size; - -    /// Tests whether the provided point is within the boundaries of [`Text`] -    /// laid out with the given parameters, returning information about -    /// the nearest character. -    /// -    /// If nearest_only is true, the hit test does not consider whether the -    /// the point is interior to any glyph bounds, returning only the character -    /// with the nearest centeroid. -    fn hit_test( -        &self, -        contents: &str, -        size: f32, -        line_height: text::LineHeight, -        font: Font, -        bounds: Size, -        shaping: text::Shaping, -        point: Point, -        nearest_only: bool, -    ) -> Option<text::Hit>; - -    /// Loads a [`Font`] from its bytes. +    /// Loads a font from its bytes.      fn load_font(&mut self, font: Cow<'static, [u8]>); + +    /// Returns the [`cosmic_text::FontSystem`] of the [`Backend`]. +    fn font_system(&self) -> &text::FontSystem;  }  /// A graphics backend that supports image rendering. diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 2f29956e..3276c2d4 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -40,6 +40,32 @@ impl<T: Damage> Damage for Primitive<T> {                  bounds.expand(1.5)              } +            Self::Paragraph { +                paragraph, +                position, +                .. +            } => { +                let mut bounds = +                    Rectangle::new(*position, paragraph.min_bounds); + +                bounds.x = match paragraph.horizontal_alignment { +                    alignment::Horizontal::Left => bounds.x, +                    alignment::Horizontal::Center => { +                        bounds.x - bounds.width / 2.0 +                    } +                    alignment::Horizontal::Right => bounds.x - bounds.width, +                }; + +                bounds.y = match paragraph.vertical_alignment { +                    alignment::Vertical::Top => bounds.y, +                    alignment::Vertical::Center => { +                        bounds.y - bounds.height / 2.0 +                    } +                    alignment::Vertical::Bottom => bounds.y - bounds.height, +                }; + +                bounds.expand(1.5) +            }              Self::Quad { bounds, .. }              | Self::Image { bounds, .. }              | Self::Svg { bounds, .. } => bounds.expand(1.0), diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index c584f3cd..0bf7ec97 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -1,6 +1,6 @@  use crate::core::alignment;  use crate::core::text::{LineHeight, Shaping}; -use crate::core::{Color, Font, Point}; +use crate::core::{Color, Font, Pixels, Point};  /// A bunch of text that can be drawn to a canvas  #[derive(Debug, Clone)] @@ -19,7 +19,7 @@ pub struct Text {      /// The color of the text      pub color: Color,      /// The size of the text -    pub size: f32, +    pub size: Pixels,      /// The line height of the text.      pub line_height: LineHeight,      /// The font of the text @@ -38,7 +38,7 @@ impl Default for Text {              content: String::new(),              position: Point::ORIGIN,              color: Color::BLACK, -            size: 16.0, +            size: Pixels(16.0),              line_height: LineHeight::Relative(1.2),              font: Font::default(),              horizontal_alignment: alignment::Horizontal::Left, diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 4e814eb0..01a358ca 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 7592a410..8a97e6e7 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -3,7 +3,8 @@ use crate::core::alignment;  use crate::core::image;  use crate::core::svg;  use crate::core::text; -use crate::core::{Background, Color, Font, Rectangle, Vector}; +use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; +use crate::text::paragraph;  use std::sync::Arc; @@ -19,7 +20,7 @@ pub enum Primitive<T> {          /// The color of the text          color: Color,          /// The size of the text in logical pixels -        size: f32, +        size: Pixels,          /// The line height of the text          line_height: text::LineHeight,          /// The font of the text @@ -31,6 +32,15 @@ pub enum Primitive<T> {          /// The shaping strategy of the text.          shaping: text::Shaping,      }, +    /// A paragraph primitive +    Paragraph { +        /// The [`paragraph::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 c0cec60a..d4df29a5 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,15 +1,15 @@  //! Create a renderer from a [`Backend`].  use crate::backend::{self, Backend}; -use crate::Primitive; - -use iced_core::image; -use iced_core::layout; -use iced_core::renderer; -use iced_core::svg; -use iced_core::text::{self, Text}; -use iced_core::{ -    Background, Color, Element, Font, Point, Rectangle, Size, Vector, +use crate::core; +use crate::core::image; +use crate::core::renderer; +use crate::core::svg; +use crate::core::text::Text; +use crate::core::{ +    Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,  }; +use crate::text; +use crate::Primitive;  use std::borrow::Cow;  use std::marker::PhantomData; @@ -18,15 +18,23 @@ use std::marker::PhantomData;  #[derive(Debug)]  pub struct Renderer<B: Backend, Theme> {      backend: B, +    default_font: Font, +    default_text_size: Pixels,      primitives: Vec<Primitive<B::Primitive>>,      theme: PhantomData<Theme>,  }  impl<B: Backend, T> Renderer<B, T> {      /// Creates a new [`Renderer`] from the given [`Backend`]. -    pub fn new(backend: B) -> Self { +    pub fn new( +        backend: B, +        default_font: Font, +        default_text_size: Pixels, +    ) -> Self {          Self {              backend, +            default_font, +            default_text_size,              primitives: Vec::new(),              theme: PhantomData,          } @@ -88,16 +96,6 @@ impl<B: Backend, T> Renderer<B, T> {  impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> {      type Theme = T; -    fn layout<Message>( -        &mut self, -        element: &Element<'_, Message, Self>, -        limits: &layout::Limits, -    ) -> layout::Node { -        self.backend.trim_measurements(); - -        element.as_widget().layout(self, limits) -    } -      fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) {          let current = self.start_layer(); @@ -137,77 +135,89 @@ impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> {      }  } -impl<B, T> text::Renderer for Renderer<B, T> +impl<B, T> core::text::Renderer for Renderer<B, T>  where      B: Backend + backend::Text,  {      type Font = Font; +    type Paragraph = text::Paragraph; -    const ICON_FONT: Font = B::ICON_FONT; -    const CHECKMARK_ICON: char = B::CHECKMARK_ICON; -    const ARROW_DOWN_ICON: char = B::ARROW_DOWN_ICON; +    const ICON_FONT: Font = Font::with_name("Iced-Icons"); +    const CHECKMARK_ICON: char = '\u{f00c}'; +    const ARROW_DOWN_ICON: char = '\u{e800}';      fn default_font(&self) -> Self::Font { -        self.backend().default_font() +        self.default_font      } -    fn default_size(&self) -> f32 { -        self.backend().default_size() +    fn default_size(&self) -> Pixels { +        self.default_text_size      } -    fn measure( +    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<text::Hit> { -        self.backend().hit_test( -            content, -            size, -            line_height, -            font, -            bounds, -            shaping, -            point, -            nearest_only, -        ) +        paragraph: &mut Self::Paragraph, +        new_bounds: Size, +    ) { +        paragraph.resize(new_bounds, self.backend.font_system());      } -    fn load_font(&mut self, bytes: Cow<'static, [u8]>) { -        self.backend.load_font(bytes); +    fn fill_paragraph( +        &mut self, +        paragraph: &Self::Paragraph, +        position: Point, +        color: Color, +    ) { +        self.primitives.push(Primitive::Paragraph { +            paragraph: paragraph.downgrade(), +            position, +            color, +        });      } -    fn fill_text(&mut self, text: Text<'_, Self::Font>) { +    fn fill_text( +        &mut self, +        text: Text<'_, Self::Font>, +        position: Point, +        color: Color, +    ) {          self.primitives.push(Primitive::Text {              content: text.content.to_string(), -            bounds: text.bounds, +            bounds: Rectangle::new(position, text.bounds),              size: text.size,              line_height: text.line_height, -            color: text.color, +            color,              font: text.font,              horizontal_alignment: text.horizontal_alignment,              vertical_alignment: text.vertical_alignment, diff --git a/graphics/src/text.rs b/graphics/src/text.rs new file mode 100644 index 00000000..bc06aa3c --- /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<cosmic_text::FontSystem>, +    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 00000000..577c4687 --- /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<KeyHash, Entry>, +    aliases: FxHashMap<KeyHash, KeyHash>, +    recently_used: FxHashSet<KeyHash>, +    hasher: HashBuilder, +} + +#[cfg(not(target_arch = "wasm32"))] +type HashBuilder = twox_hash::RandomXxHashBuilder64; + +#[cfg(target_arch = "wasm32")] +type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; + +impl Cache { +    pub fn new() -> Self { +        Self::default() +    } + +    pub fn get(&self, key: &KeyHash) -> Option<&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<H: Hasher>(self, mut hasher: H) -> KeyHash { +        self.content.hash(&mut hasher); +        self.size.to_bits().hash(&mut hasher); +        self.line_height.to_bits().hash(&mut hasher); +        self.font.hash(&mut hasher); +        self.bounds.width.to_bits().hash(&mut hasher); +        self.bounds.height.to_bits().hash(&mut hasher); +        self.shaping.hash(&mut hasher); + +        hasher.finish() +    } +} + +pub type KeyHash = u64; + +#[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 00000000..e4350cff --- /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<Arc<Internal>>); + +struct Internal { +    buffer: cosmic_text::Buffer, +    content: String, // TODO: Reuse from `buffer` (?) +    font: Font, +    shaping: Shaping, +    horizontal_alignment: alignment::Horizontal, +    vertical_alignment: alignment::Vertical, +    bounds: Size, +    min_bounds: Size, +    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<Internal> { +        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<Hit> { +        let cursor = self.internal().buffer.hit(point.x, point.y)?; + +        Some(Hit::CharOffset(cursor.index)) +    } + +    fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> { +        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<Internal>, +    pub min_bounds: Size, +    pub horizontal_alignment: alignment::Horizontal, +    pub vertical_alignment: alignment::Vertical, +} + +impl Weak { +    pub fn upgrade(&self) -> Option<Paragraph> { +        self.raw.upgrade().map(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 8b17a4b0..d1500089 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -224,16 +224,15 @@ impl Candidate {          match self {              Self::TinySkia => {                  let (compositor, backend) = -                    iced_tiny_skia::window::compositor::new( -                        iced_tiny_skia::Settings { -                            default_font: settings.default_font, -                            default_text_size: settings.default_text_size, -                        }, -                    ); +                    iced_tiny_skia::window::compositor::new();                  Ok((                      Compositor::TinySkia(compositor), -                    Renderer::TinySkia(iced_tiny_skia::Renderer::new(backend)), +                    Renderer::TinySkia(iced_tiny_skia::Renderer::new( +                        backend, +                        settings.default_font, +                        settings.default_text_size, +                    )),                  ))              }              #[cfg(feature = "wgpu")] @@ -250,7 +249,11 @@ impl Candidate {                  Ok((                      Compositor::Wgpu(compositor), -                    Renderer::Wgpu(iced_wgpu::Renderer::new(backend)), +                    Renderer::Wgpu(iced_wgpu::Renderer::new( +                        backend, +                        settings.default_font, +                        settings.default_text_size, +                    )),                  ))              }              #[cfg(not(feature = "wgpu"))] diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 6dcc0e96..8bdf231d 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<T> core::Renderer for Renderer<T> {  impl<T> text::Renderer for Renderer<T> {      type Font = Font; +    type Paragraph = Paragraph;      const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;      const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON; @@ -165,59 +169,50 @@ impl<T> text::Renderer for Renderer<T> {          delegate!(self, renderer, renderer.default_font())      } -    fn default_size(&self) -> f32 { +    fn default_size(&self) -> Pixels {          delegate!(self, renderer, renderer.default_size())      } -    fn measure( -        &self, -        content: &str, -        size: f32, -        line_height: text::LineHeight, -        font: Font, -        bounds: Size, -        shaping: text::Shaping, -    ) -> Size { -        delegate!( -            self, -            renderer, -            renderer.measure(content, size, line_height, font, bounds, shaping) -        ) +    fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph { +        delegate!(self, renderer, renderer.create_paragraph(text))      } -    fn hit_test( +    fn resize_paragraph(          &self, -        content: &str, -        size: f32, -        line_height: text::LineHeight, -        font: Font, -        bounds: Size, -        shaping: text::Shaping, -        point: Point, -        nearest_only: bool, -    ) -> Option<text::Hit> { +        paragraph: &mut Self::Paragraph, +        new_bounds: Size, +    ) {          delegate!(              self,              renderer, -            renderer.hit_test( -                content, -                size, -                line_height, -                font, -                bounds, -                shaping, -                point, -                nearest_only -            ) -        ) +            renderer.resize_paragraph(paragraph, new_bounds) +        );      }      fn load_font(&mut self, bytes: Cow<'static, [u8]>) {          delegate!(self, renderer, renderer.load_font(bytes));      } -    fn fill_text(&mut self, text: Text<'_, Self::Font>) { -        delegate!(self, renderer, renderer.fill_text(text)); +    fn fill_paragraph( +        &mut self, +        text: &Self::Paragraph, +        position: Point, +        color: Color, +    ) { +        delegate!( +            self, +            renderer, +            renderer.fill_paragraph(text, position, color) +        ); +    } + +    fn fill_text( +        &mut self, +        text: Text<'_, Self::Font>, +        position: Point, +        color: Color, +    ) { +        delegate!(self, renderer, renderer.fill_text(text, position, color));      }  } diff --git a/renderer/src/settings.rs b/renderer/src/settings.rs index 318ae718..432eb8a0 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 165a85ae..dae9e0ac 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 8659603c..d9778d7e 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,6 +1,6 @@  //! Configure your application.  use crate::window; -use crate::Font; +use crate::{Font, Pixels};  /// The settings of an application.  #[derive(Debug, Clone)] @@ -29,7 +29,7 @@ pub struct Settings<Flags> {      /// The text size that will be used by default.      ///      /// The default value is `16.0`. -    pub default_text_size: f32, +    pub default_text_size: Pixels,      /// If set to true, the renderer will try to perform antialiasing for some      /// primitives. @@ -80,7 +80,7 @@ where              window: Default::default(),              flags: Default::default(),              default_font: Default::default(), -            default_text_size: 16.0, +            default_text_size: Pixels(16.0),              antialiasing: false,              exit_on_close_request: true,          } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index a8add70b..c721d96e 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<text::Hit> { -        self.text_pipeline.hit_test( -            contents, -            size, -            line_height, -            font, -            bounds, -            shaping, -            point, -            nearest_only, -        ) +    fn font_system(&self) -> &text::FontSystem { +        self.text_pipeline.font_system()      }      fn load_font(&mut self, font: Cow<'static, [u8]>) { @@ -840,7 +816,10 @@ impl backend::Text for Backend {  #[cfg(feature = "image")]  impl backend::Image for Backend { -    fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { +    fn dimensions( +        &self, +        handle: &crate::core::image::Handle, +    ) -> crate::core::Size<u32> {          self.raster_pipeline.dimensions(handle)      }  } @@ -850,7 +829,7 @@ impl backend::Svg for Backend {      fn viewport_dimensions(          &self,          handle: &crate::core::svg::Handle, -    ) -> Size<u32> { +    ) -> crate::core::Size<u32> {          self.vector_pipeline.viewport_dimensions(handle)      }  } diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs index abffbfe6..ec27b218 100644 --- a/tiny_skia/src/settings.rs +++ b/tiny_skia/src/settings.rs @@ -1,4 +1,4 @@ -use crate::core::Font; +use crate::core::{Font, Pixels};  /// The settings of a [`Backend`].  /// @@ -11,14 +11,14 @@ pub struct Settings {      /// The default size of text.      ///      /// By default, it will be set to `16.0`. -    pub default_text_size: f32, +    pub default_text_size: Pixels,  }  impl Default for Settings {      fn default() -> Settings {          Settings {              default_font: Font::default(), -            default_text_size: 16.0, +            default_text_size: Pixels(16.0),          }      }  } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index dabbe952..cb3ef54c 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<cosmic_text::FontSystem>, +    font_system: FontSystem,      glyph_cache: GlyphCache,      cache: RefCell<Cache>,  } @@ -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<Hit> { -        let mut measurement_cache = self.cache.borrow_mut(); - -        let line_height = f32::from(line_height.to_absolute(Pixels(size))); - -        let (_, entry) = measurement_cache.allocate( -            &mut self.font_system.borrow_mut(), -            Key { -                content, -                size, -                line_height, -                font, -                bounds, -                shaping, -            }, -        ); - -        let cursor = entry.buffer.hit(point.x, point.y)?; - -        Some(Hit::CharOffset(cursor.index)) -    } -} - -fn measure(buffer: &cosmic_text::Buffer) -> Size { -    let (width, total_lines) = buffer -        .layout_runs() -        .fold((0.0, 0usize), |(width, total_lines), run| { -            (run.line_w.max(width), total_lines + 1) -        }); - -    Size::new(width, total_lines as f32 * buffer.metrics().line_height) -} - -fn to_family(family: font::Family) -> cosmic_text::Family<'static> { -    match family { -        font::Family::Name(name) => cosmic_text::Family::Name(name), -        font::Family::SansSerif => cosmic_text::Family::SansSerif, -        font::Family::Serif => cosmic_text::Family::Serif, -        font::Family::Cursive => cosmic_text::Family::Cursive, -        font::Family::Fantasy => cosmic_text::Family::Fantasy, -        font::Family::Monospace => cosmic_text::Family::Monospace, -    } -} - -fn to_weight(weight: font::Weight) -> cosmic_text::Weight { -    match weight { -        font::Weight::Thin => cosmic_text::Weight::THIN, -        font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT, -        font::Weight::Light => cosmic_text::Weight::LIGHT, -        font::Weight::Normal => cosmic_text::Weight::NORMAL, -        font::Weight::Medium => cosmic_text::Weight::MEDIUM, -        font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD, -        font::Weight::Bold => cosmic_text::Weight::BOLD, -        font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD, -        font::Weight::Black => cosmic_text::Weight::BLACK, -    } -} - -fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch { -    match stretch { -        font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed, -        font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed, -        font::Stretch::Condensed => cosmic_text::Stretch::Condensed, -        font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed, -        font::Stretch::Normal => cosmic_text::Stretch::Normal, -        font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded, -        font::Stretch::Expanded => cosmic_text::Stretch::Expanded, -        font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded, -        font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded, -    } -} - -fn to_style(style: font::Style) -> cosmic_text::Style { -    match style { -        font::Style::Normal => cosmic_text::Style::Normal, -        font::Style::Italic => cosmic_text::Style::Italic, -        font::Style::Oblique => cosmic_text::Style::Oblique, -    }  } -fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { -    match shaping { -        Shaping::Basic => cosmic_text::Shaping::Basic, -        Shaping::Advanced => cosmic_text::Shaping::Advanced, +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<KeyHash, Entry>, -    measurements: FxHashMap<KeyHash, KeyHash>, -    recently_used: FxHashSet<KeyHash>, -    hasher: HashBuilder, -    trim_count: usize, -} - -struct Entry { -    buffer: cosmic_text::Buffer, -    bounds: Size, -} - -#[cfg(not(target_arch = "wasm32"))] -type HashBuilder = twox_hash::RandomXxHashBuilder64; - -#[cfg(target_arch = "wasm32")] -type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; - -impl Cache { -    const TRIM_INTERVAL: usize = 300; - -    fn new() -> Self { -        Self { -            entries: FxHashMap::default(), -            measurements: FxHashMap::default(), -            recently_used: FxHashSet::default(), -            hasher: HashBuilder::default(), -            trim_count: 0, -        } -    } - -    fn allocate( -        &mut self, -        font_system: &mut cosmic_text::FontSystem, -        key: Key<'_>, -    ) -> (KeyHash, &mut Entry) { -        let hash = key.hash(self.hasher.build_hasher()); - -        if let Some(hash) = self.measurements.get(&hash) { -            let _ = self.recently_used.insert(*hash); - -            return (*hash, self.entries.get_mut(hash).unwrap()); -        } - -        if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { -            let metrics = cosmic_text::Metrics::new( -                key.size, -                key.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<H: Hasher>(self, mut hasher: H) -> KeyHash { -        self.content.hash(&mut hasher); -        self.size.to_bits().hash(&mut hasher); -        self.line_height.to_bits().hash(&mut hasher); -        self.font.hash(&mut hasher); -        self.bounds.width.to_bits().hash(&mut hasher); -        self.bounds.height.to_bits().hash(&mut hasher); -        self.shaping.hash(&mut hasher); - -        hasher.finish() -    } -} - -type KeyHash = u64; diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index c0da5142..828e522f 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -28,9 +28,16 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {          settings: Self::Settings,          _compatible_window: Option<&W>,      ) -> Result<(Self, Self::Renderer), Error> { -        let (compositor, backend) = new(settings); +        let (compositor, backend) = new(); -        Ok((compositor, Renderer::new(backend))) +        Ok(( +            compositor, +            Renderer::new( +                backend, +                settings.default_font, +                settings.default_text_size, +            ), +        ))      }      fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( @@ -114,12 +121,12 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {      }  } -pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) { +pub fn new<Theme>() -> (Compositor<Theme>, Backend) {      (          Compositor {              _theme: PhantomData,          }, -        Backend::new(settings), +        Backend::new(),      )  } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 97594f1a..a460c127 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.ttfBinary files differ deleted file mode 100644 index e3273141..00000000 --- a/wgpu/fonts/Iced-Icons.ttf +++ /dev/null diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 68d1f805..65c63f19 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -1,5 +1,5 @@ -use crate::core; -use crate::core::{Color, Font, Point, Size}; +use crate::core::{Color, Size}; +use crate::graphics;  use crate::graphics::backend;  use crate::graphics::color;  use crate::graphics::{Transformation, Viewport}; @@ -29,9 +29,6 @@ pub struct Backend {      #[cfg(any(feature = "image", feature = "svg"))]      image_pipeline: image::Pipeline, - -    default_font: Font, -    default_text_size: f32,  }  impl Backend { @@ -57,9 +54,6 @@ impl Backend {              #[cfg(any(feature = "image", feature = "svg"))]              image_pipeline, - -            default_font: settings.default_font, -            default_text_size: settings.default_text_size,          }      } @@ -313,65 +307,11 @@ impl Backend {  impl crate::graphics::Backend for Backend {      type Primitive = primitive::Custom; - -    fn trim_measurements(&mut self) { -        self.text_pipeline.trim_measurements(); -    }  }  impl backend::Text for Backend { -    const ICON_FONT: Font = Font::with_name("Iced-Icons"); -    const CHECKMARK_ICON: char = '\u{f00c}'; -    const ARROW_DOWN_ICON: char = '\u{e800}'; - -    fn default_font(&self) -> Font { -        self.default_font -    } - -    fn default_size(&self) -> f32 { -        self.default_text_size -    } - -    fn measure( -        &self, -        contents: &str, -        size: f32, -        line_height: core::text::LineHeight, -        font: Font, -        bounds: Size, -        shaping: core::text::Shaping, -    ) -> Size { -        self.text_pipeline.measure( -            contents, -            size, -            line_height, -            font, -            bounds, -            shaping, -        ) -    } - -    fn hit_test( -        &self, -        contents: &str, -        size: f32, -        line_height: core::text::LineHeight, -        font: Font, -        bounds: Size, -        shaping: core::text::Shaping, -        point: Point, -        nearest_only: bool, -    ) -> Option<core::text::Hit> { -        self.text_pipeline.hit_test( -            contents, -            size, -            line_height, -            font, -            bounds, -            shaping, -            point, -            nearest_only, -        ) +    fn font_system(&self) -> &graphics::text::FontSystem { +        self.text_pipeline.font_system()      }      fn load_font(&mut self, font: Cow<'static, [u8]>) { @@ -381,14 +321,17 @@ impl backend::Text for Backend {  #[cfg(feature = "image")]  impl backend::Image for Backend { -    fn dimensions(&self, handle: &core::image::Handle) -> Size<u32> { +    fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {          self.image_pipeline.dimensions(handle)      }  }  #[cfg(feature = "svg")]  impl backend::Svg for Backend { -    fn viewport_dimensions(&self, handle: &core::svg::Handle) -> Size<u32> { +    fn viewport_dimensions( +        &self, +        handle: &crate::core::svg::Handle, +    ) -> Size<u32> {          self.image_pipeline.viewport_dimensions(handle)      }  } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index b8f32db1..7a5a0f7c 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -10,7 +10,7 @@ pub use text::Text;  use crate::core;  use crate::core::alignment; -use crate::core::{Color, Font, Point, Rectangle, Size, Vector}; +use crate::core::{Color, Font, Pixels, Point, Rectangle, Size, Vector};  use crate::graphics;  use crate::graphics::color;  use crate::graphics::Viewport; @@ -56,14 +56,14 @@ impl<'a> Layer<'a> {              Layer::new(Rectangle::with_size(viewport.logical_size()));          for (i, line) in lines.iter().enumerate() { -            let text = Text { +            let text = text::Cached {                  content: line.as_ref(),                  bounds: Rectangle::new(                      Point::new(11.0, 11.0 + 25.0 * i as f32),                      Size::INFINITY,                  ),                  color: Color::new(0.9, 0.9, 0.9, 1.0), -                size: 20.0, +                size: Pixels(20.0),                  line_height: core::text::LineHeight::default(),                  font: Font::MONOSPACE,                  horizontal_alignment: alignment::Horizontal::Left, @@ -71,13 +71,13 @@ impl<'a> Layer<'a> {                  shaping: core::text::Shaping::Basic,              }; -            overlay.text.push(text); +            overlay.text.push(Text::Cached(text.clone())); -            overlay.text.push(Text { +            overlay.text.push(Text::Cached(text::Cached {                  bounds: text.bounds + Vector::new(-1.0, -1.0),                  color: Color::BLACK,                  ..text -            }); +            }));          }          overlay @@ -113,6 +113,19 @@ impl<'a> Layer<'a> {          current_layer: usize,      ) {          match primitive { +            Primitive::Paragraph { +                paragraph, +                position, +                color, +            } => { +                let layer = &mut layers[current_layer]; + +                layer.text.push(Text::Managed { +                    paragraph: paragraph.clone(), +                    position: *position + translation, +                    color: *color, +                }); +            }              Primitive::Text {                  content,                  bounds, @@ -126,7 +139,7 @@ impl<'a> Layer<'a> {              } => {                  let layer = &mut layers[current_layer]; -                layer.text.push(Text { +                layer.text.push(Text::Cached(text::Cached {                      content,                      bounds: *bounds + translation,                      size: *size, @@ -136,7 +149,7 @@ impl<'a> Layer<'a> {                      horizontal_alignment: *horizontal_alignment,                      vertical_alignment: *vertical_alignment,                      shaping: *shaping, -                }); +                }));              }              Primitive::Quad {                  bounds, diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index ba1bdca8..b61615d6 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -1,10 +1,21 @@  use crate::core::alignment;  use crate::core::text; -use crate::core::{Color, Font, Rectangle}; +use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics::text::paragraph;  /// A paragraph of text. -#[derive(Debug, Clone, Copy)] -pub struct Text<'a> { +#[derive(Debug, Clone)] +pub enum Text<'a> { +    Managed { +        paragraph: paragraph::Weak, +        position: Point, +        color: Color, +    }, +    Cached(Cached<'a>), +} + +#[derive(Debug, Clone)] +pub struct Cached<'a> {      /// The content of the [`Text`].      pub content: &'a str, @@ -15,7 +26,7 @@ pub struct Text<'a> {      pub color: Color,      /// The size of the [`Text`] in logical pixels. -    pub size: f32, +    pub size: Pixels,      /// The line height of the [`Text`].      pub line_height: text::LineHeight, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index b9f54560..2f483751 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 266a2c87..c9338fec 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -1,5 +1,5 @@  //! Configure a renderer. -use crate::core::Font; +use crate::core::{Font, Pixels};  use crate::graphics::Antialiasing;  /// The settings of a [`Backend`]. @@ -21,7 +21,7 @@ pub struct Settings {      /// The default size of text.      ///      /// By default, it will be set to `16.0`. -    pub default_text_size: f32, +    pub default_text_size: Pixels,      /// The antialiasing strategy that will be used for triangle primitives.      /// @@ -59,7 +59,7 @@ impl Default for Settings {              present_mode: wgpu::PresentMode::AutoVsync,              internal_backend: wgpu::Backends::all(),              default_font: Font::default(), -            default_text_size: 16.0, +            default_text_size: Pixels(16.0),              antialiasing: None,          }      } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 9c42be0e..bd4f3e06 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<glyphon::FontSystem>, +    font_system: FontSystem,      renderers: Vec<glyphon::TextRenderer>,      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<u32>,      ) { @@ -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<Hit> { -        let mut cache = self.cache.borrow_mut(); - -        let line_height = f32::from(line_height.to_absolute(Pixels(size))); - -        let (_, entry) = cache.allocate( -            &mut self.font_system.borrow_mut(), -            Key { -                content, -                size, -                line_height, -                font, -                bounds, -                shaping, -            }, -            Purpose::Measuring, -        ); - -        let cursor = entry.buffer.hit(point.x, point.y)?; - -        Some(Hit::CharOffset(cursor.index)) -    } -} - -fn measure(buffer: &glyphon::Buffer) -> Size { -    let (width, total_lines) = buffer -        .layout_runs() -        .fold((0.0, 0usize), |(width, total_lines), run| { -            (run.line_w.max(width), total_lines + 1) -        }); - -    Size::new(width, total_lines as f32 * buffer.metrics().line_height) -} - -fn to_family(family: font::Family) -> glyphon::Family<'static> { -    match family { -        font::Family::Name(name) => glyphon::Family::Name(name), -        font::Family::SansSerif => glyphon::Family::SansSerif, -        font::Family::Serif => glyphon::Family::Serif, -        font::Family::Cursive => glyphon::Family::Cursive, -        font::Family::Fantasy => glyphon::Family::Fantasy, -        font::Family::Monospace => glyphon::Family::Monospace, -    } -} - -fn to_weight(weight: font::Weight) -> glyphon::Weight { -    match weight { -        font::Weight::Thin => glyphon::Weight::THIN, -        font::Weight::ExtraLight => glyphon::Weight::EXTRA_LIGHT, -        font::Weight::Light => glyphon::Weight::LIGHT, -        font::Weight::Normal => glyphon::Weight::NORMAL, -        font::Weight::Medium => glyphon::Weight::MEDIUM, -        font::Weight::Semibold => glyphon::Weight::SEMIBOLD, -        font::Weight::Bold => glyphon::Weight::BOLD, -        font::Weight::ExtraBold => glyphon::Weight::EXTRA_BOLD, -        font::Weight::Black => glyphon::Weight::BLACK, -    } -} - -fn to_stretch(stretch: font::Stretch) -> glyphon::Stretch { -    match stretch { -        font::Stretch::UltraCondensed => glyphon::Stretch::UltraCondensed, -        font::Stretch::ExtraCondensed => glyphon::Stretch::ExtraCondensed, -        font::Stretch::Condensed => glyphon::Stretch::Condensed, -        font::Stretch::SemiCondensed => glyphon::Stretch::SemiCondensed, -        font::Stretch::Normal => glyphon::Stretch::Normal, -        font::Stretch::SemiExpanded => glyphon::Stretch::SemiExpanded, -        font::Stretch::Expanded => glyphon::Stretch::Expanded, -        font::Stretch::ExtraExpanded => glyphon::Stretch::ExtraExpanded, -        font::Stretch::UltraExpanded => glyphon::Stretch::UltraExpanded, -    }  } - -fn to_style(style: font::Style) -> glyphon::Style { -    match style { -        font::Style::Normal => glyphon::Style::Normal, -        font::Style::Italic => glyphon::Style::Italic, -        font::Style::Oblique => glyphon::Style::Oblique, -    } -} - -fn to_shaping(shaping: Shaping) -> glyphon::Shaping { -    match shaping { -        Shaping::Basic => glyphon::Shaping::Basic, -        Shaping::Advanced => glyphon::Shaping::Advanced, -    } -} - -struct Cache { -    entries: FxHashMap<KeyHash, Entry>, -    aliases: FxHashMap<KeyHash, KeyHash>, -    recently_measured: FxHashSet<KeyHash>, -    recently_drawn: FxHashSet<KeyHash>, -    hasher: HashBuilder, -} - -struct Entry { -    buffer: glyphon::Buffer, -    bounds: Size, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Purpose { -    Measuring, -    Drawing, -} - -#[cfg(not(target_arch = "wasm32"))] -type HashBuilder = twox_hash::RandomXxHashBuilder64; - -#[cfg(target_arch = "wasm32")] -type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; - -impl Cache { -    fn new() -> Self { -        Self { -            entries: FxHashMap::default(), -            aliases: FxHashMap::default(), -            recently_measured: FxHashSet::default(), -            recently_drawn: FxHashSet::default(), -            hasher: HashBuilder::default(), -        } -    } - -    fn get(&self, key: &KeyHash) -> Option<&Entry> { -        self.entries.get(key) -    } - -    fn allocate( -        &mut self, -        font_system: &mut glyphon::FontSystem, -        key: Key<'_>, -        purpose: Purpose, -    ) -> (KeyHash, &mut Entry) { -        let hash = key.hash(self.hasher.build_hasher()); - -        let recently_used = match purpose { -            Purpose::Measuring => &mut self.recently_measured, -            Purpose::Drawing => &mut self.recently_drawn, -        }; - -        if let Some(hash) = self.aliases.get(&hash) { -            let _ = recently_used.insert(*hash); - -            return (*hash, self.entries.get_mut(hash).unwrap()); -        } - -        if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { -            let metrics = glyphon::Metrics::new( -                key.size, -                key.line_height.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<H: Hasher>(self, mut hasher: H) -> KeyHash { -        self.content.hash(&mut hasher); -        self.size.to_bits().hash(&mut hasher); -        self.line_height.to_bits().hash(&mut hasher); -        self.font.hash(&mut hasher); -        self.bounds.width.to_bits().hash(&mut hasher); -        self.bounds.height.to_bits().hash(&mut hasher); -        self.shaping.hash(&mut hasher); - -        hasher.finish() -    } -} - -type KeyHash = u64; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 9e9c63db..a9521a15 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -216,7 +216,14 @@ impl<Theme> graphics::Compositor for Compositor<Theme> {      ) -> Result<(Self, Self::Renderer), Error> {          let (compositor, backend) = new(settings, compatible_window)?; -        Ok((compositor, Renderer::new(backend))) +        Ok(( +            compositor, +            Renderer::new( +                backend, +                settings.default_font, +                settings.default_text_size, +            ), +        ))      }      fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( diff --git a/widget/src/button.rs b/widget/src/button.rs index 18a95c9e..4915bd49 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: &Renderer, +pub fn layout(      limits: &layout::Limits,      width: Length,      height: Length,      padding: Padding, -    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +    layout_content: impl FnOnce(&layout::Limits) -> layout::Node,  ) -> layout::Node {      let limits = limits.width(width).height(height); -    let mut content = layout_content(renderer, &limits.pad(padding)); +    let mut content = layout_content(&limits.pad(padding));      let padding = padding.fit(content.size(), limits.max());      let size = limits.pad(padding).resolve(content.size()).pad(padding); diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 1a186432..390f4d92 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 e574c3cc..d7fdf339 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -6,12 +6,11 @@ use crate::core::mouse;  use crate::core::renderer;  use crate::core::text;  use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree};  use crate::core::{ -    Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, -    Widget, +    Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget,  }; -use crate::{Row, Text};  pub use iced_style::checkbox::{Appearance, StyleSheet}; @@ -45,7 +44,7 @@ where      width: Length,      size: f32,      spacing: f32, -    text_size: Option<f32>, +    text_size: Option<Pixels>,      text_line_height: text::LineHeight,      text_shaping: text::Shaping,      font: Option<Renderer::Font>, @@ -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<Pixels>) -> Self { -        self.text_size = Some(text_size.into().0); +        self.text_size = Some(text_size.into());          self      } @@ -167,6 +166,14 @@ where      Renderer: text::Renderer,      Renderer::Theme: StyleSheet + crate::text::StyleSheet,  { +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<widget::text::State<Renderer::Paragraph>>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) +    } +      fn width(&self) -> Length {          self.width      } @@ -177,26 +184,35 @@ where      fn layout(          &self, +        tree: &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::State<Renderer::Paragraph>>(); + +                widget::text::layout( +                    state, +                    renderer, +                    limits, +                    self.width, +                    Length::Shrink, +                    &self.label, +                    self.text_line_height, +                    self.text_size, +                    self.font, +                    alignment::Horizontal::Left, +                    alignment::Vertical::Top, +                    self.text_shaping, +                ) +            }, +        )      }      fn on_event( @@ -244,7 +260,7 @@ where      fn draw(          &self, -        _tree: &Tree, +        tree: &Tree,          renderer: &mut Renderer,          theme: &Renderer::Theme,          style: &renderer::Style, @@ -283,24 +299,23 @@ where                  line_height,                  shaping,              } = &self.icon; -            let size = size.unwrap_or(bounds.height * 0.7); +            let size = size.unwrap_or(Pixels(bounds.height * 0.7));              if self.is_checked { -                renderer.fill_text(text::Text { -                    content: &code_point.to_string(), -                    font: *font, -                    size, -                    line_height: *line_height, -                    bounds: Rectangle { -                        x: bounds.center_x(), -                        y: bounds.center_y(), -                        ..bounds +                renderer.fill_text( +                    text::Text { +                        content: &code_point.to_string(), +                        font: *font, +                        size, +                        line_height: *line_height, +                        bounds: bounds.size(), +                        horizontal_alignment: alignment::Horizontal::Center, +                        vertical_alignment: alignment::Vertical::Center, +                        shaping: *shaping,                      }, -                    color: custom_style.icon_color, -                    horizontal_alignment: alignment::Horizontal::Center, -                    vertical_alignment: alignment::Vertical::Center, -                    shaping: *shaping, -                }); +                    bounds.center(), +                    custom_style.icon_color, +                );              }          } @@ -311,16 +326,10 @@ where                  renderer,                  style,                  label_layout, -                &self.label, -                self.text_size, -                self.text_line_height, -                self.font, +                tree.state.downcast_ref(),                  crate::text::Appearance {                      color: custom_style.text_color,                  }, -                alignment::Horizontal::Left, -                alignment::Vertical::Center, -                self.text_shaping,              );          }      } @@ -348,7 +357,7 @@ pub struct Icon<Font> {      /// The unicode code point that will be used as the icon.      pub code_point: char,      /// Font size of the content. -    pub size: Option<f32>, +    pub size: Option<Pixels>,      /// The line height of the icon.      pub line_height: text::LineHeight,      /// The shaping strategy of the icon. diff --git a/widget/src/column.rs b/widget/src/column.rs index c16477f3..f2347cc9 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 0dc12354..d6915281 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<T>(RefCell<Inner<T>>);  #[derive(Debug, Clone)]  struct Inner<T> { -    text_input: text_input::State,      value: String,      options: Vec<T>,      option_matchers: Vec<String>, @@ -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<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {          let inner = self.0.borrow(); @@ -290,21 +244,6 @@ where      }  } -impl<T> Inner<T> { -    fn text_input_tree(&self) -> widget::Tree { -        widget::Tree { -            tag: widget::tree::Tag::of::<text_input::State>(), -            state: widget::tree::State::new(self.text_input.clone()), -            children: vec![], -        } -    } - -    fn update_text_input(&mut self, tree: widget::Tree) { -        self.text_input = -            tree.state.downcast_ref::<text_input::State>().clone(); -    } -} -  impl<T> Filtered<T>  where      T: Clone, @@ -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<widget::Tree> { +        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::<Menu<T>>(); -        let started_focused = self.state.is_focused(); +        let started_focused = { +            let text_input_state = tree.children[0] +                .state +                .downcast_ref::<text_input::State<Renderer::Paragraph>>(); + +            text_input_state.is_focused() +        };          // This is intended to check whether or not the message buffer was empty,          // since `Shell` does not expose such functionality.          let mut published_message_to_shell = false; @@ -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<Renderer::Paragraph>>(); + +            text_input_state.is_focused() +        }; + +        if is_focused {              self.state.with_inner(|state| {                  if !started_focused {                      if let Some(on_option_hovered) = &mut self.on_option_hovered @@ -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<Renderer::Paragraph>>(); + +            text_input_state.is_focused() +        }; + +        if started_focused && !is_focused && !published_message_to_shell {              if let Some(message) = self.on_close.take() {                  shell.publish(message);              }          }          // Focus changed, invalidate widget tree to force a fresh `view` -        if started_focused != self.state.is_focused() { +        if started_focused != is_focused {              shell.invalidate_widgets();          } @@ -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<Renderer::Paragraph>>(); + +            text_input_state.is_focused() +        }; + +        let selection = if is_focused || self.selection.is_empty() {              None          } else {              Some(&self.selection)          }; -        let tree = self.state.text_input_tree(); -        self.text_input -            .draw(&tree, renderer, theme, layout, cursor, selection); +        self.text_input.draw( +            &tree.children[0], +            renderer, +            theme, +            layout, +            cursor, +            selection, +        );      }      fn overlay<'b>( @@ -666,14 +641,22 @@ where          layout: Layout<'_>,          _renderer: &Renderer,      ) -> Option<overlay::Element<'b, Message, Renderer>> { -        let Menu { -            menu, -            filtered_options, -            hovered_option, -            .. -        } = tree.state.downcast_mut::<Menu<T>>(); +        let is_focused = { +            let text_input_state = tree.children[0] +                .state +                .downcast_ref::<text_input::State<Renderer::Paragraph>>(); + +            text_input_state.is_focused() +        }; + +        if is_focused { +            let Menu { +                menu, +                filtered_options, +                hovered_option, +                .. +            } = tree.state.downcast_mut::<Menu<T>>(); -        if self.state.is_focused() {              let bounds = layout.bounds();              self.state.sync_filtered_options(filtered_options); diff --git a/widget/src/container.rs b/widget/src/container.rs index 1f1df861..ee7a4965 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -5,7 +5,8 @@ use crate::core::layout;  use crate::core::mouse;  use crate::core::overlay;  use crate::core::renderer; -use crate::core::widget::{self, Operation, Tree}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::{self, Operation};  use crate::core::{      Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,      Point, Rectangle, Shell, Size, Vector, Widget, @@ -135,12 +136,20 @@ where      Renderer: crate::core::Renderer,      Renderer::Theme: StyleSheet,  { +    fn tag(&self) -> tree::Tag { +        self.content.as_widget().tag() +    } + +    fn state(&self) -> tree::State { +        self.content.as_widget().state() +    } +      fn children(&self) -> Vec<Tree> { -        vec![Tree::new(&self.content)] +        self.content.as_widget().children()      }      fn diff(&self, tree: &mut Tree) { -        tree.diff_children(std::slice::from_ref(&self.content)) +        self.content.as_widget().diff(tree);      }      fn width(&self) -> Length { @@ -153,11 +162,11 @@ where      fn layout(          &self, +        tree: &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<overlay::Element<'b, Message, Renderer>> {          self.content.as_widget_mut().overlay( -            &mut tree.children[0], +            tree,              layout.children().next().unwrap(),              renderer,          ) @@ -291,8 +298,7 @@ where  }  /// Computes the layout of a [`Container`]. -pub fn layout<Renderer>( -    renderer: &Renderer, +pub fn layout(      limits: &layout::Limits,      width: Length,      height: Length, @@ -301,7 +307,7 @@ pub fn layout<Renderer>(      padding: Padding,      horizontal_alignment: alignment::Horizontal,      vertical_alignment: alignment::Vertical, -    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +    layout_content: impl FnOnce(&layout::Limits) -> layout::Node,  ) -> layout::Node {      let limits = limits          .loose() @@ -310,7 +316,7 @@ pub fn layout<Renderer>(          .width(width)          .height(height); -    let mut content = layout_content(renderer, &limits.pad(padding).loose()); +    let mut content = layout_content(&limits.pad(padding).loose());      let padding = padding.fit(content.size(), limits.max());      let size = limits.pad(padding).resolve(content.size()); diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 289720eb..3c9c2b29 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<Message, Renderer>(      children: Vec<Element<'_, Message, Renderer>>,  ) -> 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<Item = (Key, Element<'a, Message, Renderer>)>, +) -> 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 66bf2156..3c83c87b 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 6e095667..724d121e 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 00000000..ad531e66 --- /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 00000000..19016679 --- /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<Key>, +    children: Vec<Element<'a, Message, Renderer>>, +} + +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<Item = (Key, Element<'a, Message, Renderer>)>, +    ) -> 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<Pixels>) -> Self { +        self.spacing = amount.into().0; +        self +    } + +    /// Sets the [`Padding`] of the [`Column`]. +    pub fn padding<P: Into<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<Length>) -> Self { +        self.width = width.into(); +        self +    } + +    /// Sets the height of the [`Column`]. +    pub fn height(mut self, height: impl Into<Length>) -> Self { +        self.height = height.into(); +        self +    } + +    /// Sets the maximum width of the [`Column`]. +    pub fn max_width(mut self, max_width: impl Into<Pixels>) -> 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<Element<'a, Message, Renderer>>, +    ) -> 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<Key> +where +    Key: Copy + PartialEq, +{ +    keys: Vec<Key>, +} + +impl<'a, Key, Message, Renderer> Widget<Message, Renderer> +    for Column<'a, Key, Message, Renderer> +where +    Renderer: crate::core::Renderer, +    Key: Copy + PartialEq + 'static, +{ +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<State<Key>>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(State { +            keys: self.keys.clone(), +        }) +    } + +    fn children(&self) -> Vec<Tree> { +        self.children.iter().map(Tree::new).collect() +    } + +    fn diff(&self, tree: &mut Tree) { +        let Tree { +            state, children, .. +        } = tree; + +        let state = state.downcast_mut::<State<Key>>(); + +        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<Message>, +    ) { +        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::Element<'b, Message, Renderer>> { +        overlay::from_children(&mut self.children, tree, layout, renderer) +    } +} + +impl<'a, Key, Message, Renderer> From<Column<'a, Key, Message, Renderer>> +    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 761f45ad..bf695a57 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 19df2792..fe99a7f2 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::<Rc<RefCell<Option<Tree>>>>(); +          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 b56545c8..0b819455 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<Target = Renderer>,      {          self.update(tree, layout.bounds().size(), view); -        self.layout(renderer.deref()); +        self.layout(tree, renderer.deref());          let content_layout = Layout::with_offset(              layout.position() - Point::ORIGIN, @@ -144,6 +144,7 @@ where      fn layout(          &self, +        _tree: &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 5cb0f8de..7e204171 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 490f7c48..65d44dd5 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 711a8891..b293f9fa 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -31,7 +31,7 @@ where      on_option_hovered: Option<&'a dyn Fn(T) -> Message>,      width: f32,      padding: Padding, -    text_size: Option<f32>, +    text_size: Option<Pixels>,      text_line_height: text::LineHeight,      text_shaping: text::Shaping,      font: Option<Renderer::Font>, @@ -85,7 +85,7 @@ where      /// Sets the text size of the [`Menu`].      pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { -        self.text_size = Some(text_size.into().0); +        self.text_size = Some(text_size.into());          self      } @@ -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<dyn FnMut(T) -> Message + 'a>,      on_option_hovered: Option<&'a dyn Fn(T) -> Message>,      padding: Padding, -    text_size: Option<f32>, +    text_size: Option<Pixels>,      text_line_height: text::LineHeight,      text_shaping: text::Shaping,      font: Option<Renderer::Font>, @@ -352,6 +352,7 @@ where      fn layout(          &self, +        _tree: &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 40833622..f868a648 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<Renderer, T>( +    tree: &mut Tree,      renderer: &Renderer,      limits: &layout::Limits,      node: &Node, @@ -478,19 +483,26 @@ pub fn layout<Renderer, T>(      height: Length,      spacing: f32,      contents: impl Iterator<Item = (Pane, T)>, -    layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, +    layout_content: impl Fn( +        T, +        &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 218adcd5..826ea663 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 47457337..5ae7a6a0 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 26fe5ca2..056a5e65 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<T>,      width: Length,      padding: Padding, -    text_size: Option<f32>, +    text_size: Option<Pixels>,      text_line_height: text::LineHeight,      text_shaping: text::Shaping,      font: Option<Renderer::Font>, @@ -101,7 +101,7 @@ where      /// Sets the text size of the [`PickList`].      pub fn text_size(mut self, size: impl Into<Pixels>) -> Self { -        self.text_size = Some(size.into().0); +        self.text_size = Some(size.into());          self      } @@ -157,11 +157,11 @@ where          From<<Renderer::Theme as StyleSheet>::Style>,  {      fn tag(&self) -> tree::Tag { -        tree::Tag::of::<State>() +        tree::Tag::of::<State<Renderer::Paragraph>>()      }      fn state(&self) -> tree::State { -        tree::State::new(State::new()) +        tree::State::new(State::<Renderer::Paragraph>::new())      }      fn width(&self) -> Length { @@ -174,10 +174,12 @@ where      fn layout(          &self, +        tree: &mut Tree,          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node {          layout( +            tree.state.downcast_mut::<State<Renderer::Paragraph>>(),              renderer,              limits,              self.width, @@ -210,7 +212,7 @@ where              self.on_selected.as_ref(),              self.selected.as_ref(),              &self.options, -            || tree.state.downcast_mut::<State>(), +            || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),          )      } @@ -250,7 +252,7 @@ where              self.selected.as_ref(),              &self.handle,              &self.style, -            || tree.state.downcast_ref::<State>(), +            || tree.state.downcast_ref::<State<Renderer::Paragraph>>(),          )      } @@ -260,7 +262,7 @@ where          layout: Layout<'_>,          renderer: &Renderer,      ) -> Option<overlay::Element<'b, Message, Renderer>> { -        let state = tree.state.downcast_mut::<State>(); +        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();          overlay(              layout, @@ -295,28 +297,32 @@ where      }  } -/// The local state of a [`PickList`]. +/// The state of a [`PickList`].  #[derive(Debug)] -pub struct State { +pub struct State<P: text::Paragraph> {      menu: menu::State,      keyboard_modifiers: keyboard::Modifiers,      is_open: bool,      hovered_option: Option<usize>, +    options: Vec<P>, +    placeholder: P,  } -impl State { +impl<P: text::Paragraph> State<P> {      /// Creates a new [`State`] for a [`PickList`]. -    pub fn new() -> Self { +    fn new() -> Self {          Self {              menu: menu::State::default(),              keyboard_modifiers: keyboard::Modifiers::default(),              is_open: bool::default(),              hovered_option: Option::default(), +            options: Vec::new(), +            placeholder: P::default(),          }      }  } -impl Default for State { +impl<P: text::Paragraph> Default for State<P> {      fn default() -> Self {          Self::new()      } @@ -330,7 +336,7 @@ pub enum Handle<Font> {      /// This is the default.      Arrow {          /// Font size of the content. -        size: Option<f32>, +        size: Option<Pixels>,      },      /// A custom static handle.      Static(Icon<Font>), @@ -359,7 +365,7 @@ pub struct Icon<Font> {      /// The unicode code point that will be used as the icon.      pub code_point: char,      /// Font size of the content. -    pub size: Option<f32>, +    pub size: Option<Pixels>,      /// Line height of the content.      pub line_height: text::LineHeight,      /// The shaping strategy of the icon. @@ -368,11 +374,12 @@ pub struct Icon<Font> {  /// Computes the layout of a [`PickList`].  pub fn layout<Renderer, T>( +    state: &mut State<Renderer::Paragraph>,      renderer: &Renderer,      limits: &layout::Limits,      width: Length,      padding: Padding, -    text_size: Option<f32>, +    text_size: Option<Pixels>,      text_line_height: text::LineHeight,      text_shaping: text::Shaping,      font: Option<Renderer::Font>, @@ -386,38 +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<P>,  ) -> 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<Renderer::Paragraph>,      padding: Padding, -    text_size: Option<f32>, +    text_size: Option<Pixels>,      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<f32>, +    text_size: Option<Pixels>,      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<Renderer::Font>,      style: &<Renderer::Theme as StyleSheet>::Style, -    state: impl FnOnce() -> &'a State, +    state: impl FnOnce() -> &'a State<Renderer::Paragraph>,  ) where      Renderer: text::Renderer,      Renderer::Theme: StyleSheet, @@ -665,22 +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 37c6bc72..07de72d5 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 75409091..1dc4da7f 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -60,6 +60,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {      fn layout(          &self, +        _tree: &mut Tree,          _renderer: &Renderer<Theme>,          _limits: &layout::Limits,      ) -> layout::Node { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 12bbd9c7..57acc033 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -6,12 +6,12 @@ use crate::core::mouse;  use crate::core::renderer;  use crate::core::text;  use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree};  use crate::core::{ -    Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, -    Shell, Widget, +    Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, Shell, Size, +    Widget,  }; -use crate::{Row, Text};  pub use iced_style::radio::{Appearance, StyleSheet}; @@ -80,7 +80,7 @@ where      width: Length,      size: f32,      spacing: f32, -    text_size: Option<f32>, +    text_size: Option<Pixels>,      text_line_height: text::LineHeight,      text_shaping: text::Shaping,      font: Option<Renderer::Font>, @@ -152,7 +152,7 @@ where      /// Sets the text size of the [`Radio`] button.      pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { -        self.text_size = Some(text_size.into().0); +        self.text_size = Some(text_size.into());          self      } @@ -193,6 +193,14 @@ where      Renderer: text::Renderer,      Renderer::Theme: StyleSheet + crate::text::StyleSheet,  { +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<widget::text::State<Renderer::Paragraph>>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) +    } +      fn width(&self) -> Length {          self.width      } @@ -203,25 +211,35 @@ where      fn layout(          &self, +        tree: &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::State<Renderer::Paragraph>>(); + +                widget::text::layout( +                    state, +                    renderer, +                    limits, +                    self.width, +                    Length::Shrink, +                    &self.label, +                    self.text_line_height, +                    self.text_size, +                    self.font, +                    alignment::Horizontal::Left, +                    alignment::Vertical::Top, +                    self.text_shaping, +                ) +            }, +        )      }      fn on_event( @@ -267,7 +285,7 @@ where      fn draw(          &self, -        _state: &Tree, +        tree: &Tree,          renderer: &mut Renderer,          theme: &Renderer::Theme,          style: &renderer::Style, @@ -327,16 +345,10 @@ where                  renderer,                  style,                  label_layout, -                &self.label, -                self.text_size, -                self.text_line_height, -                self.font, +                tree.state.downcast_ref(),                  crate::text::Appearance {                      color: custom_style.text_color,                  }, -                alignment::Horizontal::Left, -                alignment::Vertical::Center, -                self.text_shaping,              );          }      } diff --git a/widget/src/row.rs b/widget/src/row.rs index 99b2a0bf..71cf0509 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 d703e6ae..b5c5fa55 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 d0c77e6b..def28821 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 bd73ea79..2c4a2913 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 9a5385e8..e5a8f169 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 1ccc5d62..2d01d1ab 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 f7a90880..bfd196fd 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<Renderer::Font>,      width: Length,      padding: Padding, -    size: Option<f32>, +    size: Option<Pixels>,      line_height: text::LineHeight,      on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,      on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, @@ -178,7 +178,7 @@ where      /// Sets the text size of the [`TextInput`].      pub fn size(mut self, size: impl Into<Pixels>) -> Self { -        self.size = Some(size.into().0); +        self.size = Some(size.into());          self      } @@ -218,12 +218,8 @@ where              theme,              layout,              cursor, -            tree.state.downcast_ref::<State>(), +            tree.state.downcast_ref::<State<Renderer::Paragraph>>(),              value.unwrap_or(&self.value), -            &self.placeholder, -            self.size, -            self.line_height, -            self.font,              self.on_input.is_none(),              self.is_secure,              self.icon.as_ref(), @@ -240,15 +236,15 @@ where      Renderer::Theme: StyleSheet,  {      fn tag(&self) -> tree::Tag { -        tree::Tag::of::<State>() +        tree::Tag::of::<State<Renderer::Paragraph>>()      }      fn state(&self) -> tree::State { -        tree::State::new(State::new()) +        tree::State::new(State::<Renderer::Paragraph>::new())      }      fn diff(&self, tree: &mut Tree) { -        let state = tree.state.downcast_mut::<State>(); +        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();          // Unfocus text input if it becomes disabled          if self.on_input.is_none() { @@ -269,6 +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::<State<Renderer::Paragraph>>(), +            &self.value, +            &self.placeholder, +            self.is_secure,          )      } @@ -290,7 +292,7 @@ where          _renderer: &Renderer,          operation: &mut dyn Operation<Message>,      ) { -        let state = tree.state.downcast_mut::<State>(); +        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();          operation.focusable(state, self.id.as_ref().map(|id| &id.0));          operation.text_input(state, self.id.as_ref().map(|id| &id.0)); @@ -322,7 +324,7 @@ where              self.on_input.as_deref(),              self.on_paste.as_deref(),              &self.on_submit, -            || tree.state.downcast_mut::<State>(), +            || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),          )      } @@ -341,12 +343,8 @@ where              theme,              layout,              cursor, -            tree.state.downcast_ref::<State>(), +            tree.state.downcast_ref::<State<Renderer::Paragraph>>(),              &self.value, -            &self.placeholder, -            self.size, -            self.line_height, -            self.font,              self.on_input.is_none(),              self.is_secure,              self.icon.as_ref(), @@ -388,7 +386,7 @@ pub struct Icon<Font> {      /// The unicode code point that will be used as the icon.      pub code_point: char,      /// The font size of the content. -    pub size: Option<f32>, +    pub size: Option<Pixels>,      /// The spacing between the [`Icon`] and the text in a [`TextInput`].      pub spacing: f32,      /// The side of a [`TextInput`] where to display the [`Icon`]. @@ -465,29 +463,68 @@ pub fn layout<Renderer>(      limits: &layout::Limits,      width: Length,      padding: Padding, -    size: Option<f32>, +    size: Option<Pixels>, +    font: Option<Renderer::Font>,      line_height: text::LineHeight,      icon: Option<&Icon<Renderer::Font>>, +    state: &mut State<Renderer::Paragraph>, +    value: &Value, +    placeholder: &str, +    is_secure: bool,  ) -> layout::Node  where      Renderer: text::Renderer,  { +    let font = font.unwrap_or_else(|| renderer.default_font());      let text_size = size.unwrap_or_else(|| renderer.default_size()); +      let padding = padding.fit(Size::ZERO, limits.max());      let limits = limits          .width(width)          .pad(padding) -        .height(line_height.to_absolute(Pixels(text_size))); +        .height(line_height.to_absolute(text_size));      let text_bounds = limits.resolve(Size::ZERO); +    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<f32>, +    size: Option<Pixels>,      line_height: text::LineHeight,      font: Option<Renderer::Font>,      is_secure: bool,      on_input: Option<&dyn Fn(String) -> Message>,      on_paste: Option<&dyn Fn(String) -> Message>,      on_submit: &Option<Message>, -    state: impl FnOnce() -> &'a mut State, +    state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>,  ) -> event::Status  where      Message: Clone,      Renderer: text::Renderer,  { +    let update_cache = |state, value| { +        replace_paragraph( +            renderer, +            state, +            layout, +            value, +            font, +            size, +            line_height, +        ) +    }; +      match event {          Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))          | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -592,11 +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<Renderer>(      theme: &Renderer::Theme,      layout: Layout<'_>,      cursor: mouse::Cursor, -    state: &State, +    state: &State<Renderer::Paragraph>,      value: &Value, -    placeholder: &str, -    size: Option<f32>, -    line_height: text::LineHeight, -    font: Option<Renderer::Font>,      is_disabled: bool,      is_secure: bool,      icon: Option<&Icon<Renderer::Font>>, @@ -1023,28 +1066,17 @@ pub fn draw<Renderer>(          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<Renderer>(              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<Renderer>(                  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<Renderer>(          (None, 0.0)      }; -    let text_width = renderer.measure_width( -        if text.is_empty() { placeholder } else { &text }, -        size, -        font, -        text::Shaping::Advanced, -    ); +    let text_width = state.value.min_width();      let render = |renderer: &mut Renderer| {          if let Some((cursor, color)) = cursor { @@ -1157,27 +1175,21 @@ pub fn draw<Renderer>(              renderer.with_translation(Vector::ZERO, |_| {});          } -        renderer.fill_text(Text { -            content: if text.is_empty() { placeholder } else { &text }, -            color: if text.is_empty() { +        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<P: text::Paragraph> { +    value: P, +    placeholder: P, +    icon: P,      is_focused: Option<Focus>,      is_dragging: bool,      is_pasting: Option<Value>, @@ -1225,7 +1240,7 @@ struct Focus {      is_window_focused: bool,  } -impl State { +impl<P: text::Paragraph> State<P> {      /// Creates a new [`State`], representing an unfocused [`TextInput`].      pub fn new() -> Self {          Self::default() @@ -1234,6 +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<P: text::Paragraph> operation::Focusable for State<P> {      fn is_focused(&self) -> bool {          State::is_focused(self)      } @@ -1306,7 +1324,7 @@ impl operation::Focusable for State {      }  } -impl operation::TextInput for State { +impl<P: text::Paragraph> operation::TextInput for State<P> {      fn move_cursor_to_front(&mut self) {          State::move_cursor_to_front(self)      } @@ -1336,17 +1354,11 @@ mod platform {      }  } -fn offset<Renderer>( -    renderer: &Renderer, +fn offset<P: text::Paragraph>(      text_bounds: Rectangle, -    font: Renderer::Font, -    size: f32,      value: &Value, -    state: &State, -) -> f32 -where -    Renderer: text::Renderer, -{ +    state: &State<P>, +) -> f32 {      if state.is_focused() {          let cursor = state.cursor(); @@ -1356,12 +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: &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: &Renderer, +fn find_cursor_position<P: text::Paragraph>(      text_bounds: Rectangle, -    font: Option<Renderer::Font>, -    size: Option<f32>, -    line_height: text::LineHeight,      value: &Value, -    state: &State, +    state: &State<P>,      x: f32, -) -> Option<usize> -where -    Renderer: text::Renderer, -{ -    let font = font.unwrap_or_else(|| renderer.default_font()); -    let size = size.unwrap_or_else(|| renderer.default_size()); - -    let offset = offset(renderer, text_bounds, font, size, value, state); +) -> Option<usize> { +    let offset = offset(text_bounds, value, state);      let value = value.to_string(); -    let char_offset = renderer -        .hit_test( -            &value, -            size, -            line_height, -            font, -            Size::INFINITY, -            text::Shaping::Advanced, -            Point::new(x + offset, text_bounds.height / 2.0), -            true, -        ) +    let char_offset = state +        .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: &Renderer, +    state: &mut State<Renderer::Paragraph>, +    layout: Layout<'_>, +    value: &Value, +    font: Option<Renderer::Font>, +    text_size: Option<Pixels>, +    line_height: text::LineHeight, +) where +    Renderer: text::Renderer, +{ +    let font = font.unwrap_or_else(|| renderer.default_font()); +    let text_size = text_size.unwrap_or_else(|| renderer.default_size()); + +    let mut children_layout = layout.children(); +    let text_bounds = children_layout.next().unwrap().bounds(); + +    state.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 3793f5b0..2440317f 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -6,12 +6,12 @@ use crate::core::mouse;  use crate::core::renderer;  use crate::core::text;  use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree};  use crate::core::{ -    Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, -    Shell, Widget, +    Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, +    Widget,  }; -use crate::{Row, Text};  pub use crate::style::toggler::{Appearance, StyleSheet}; @@ -42,7 +42,7 @@ where      label: Option<String>,      width: Length,      size: f32, -    text_size: Option<f32>, +    text_size: Option<Pixels>,      text_line_height: text::LineHeight,      text_alignment: alignment::Horizontal,      text_shaping: text::Shaping, @@ -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<Pixels>) -> Self { -        self.text_size = Some(text_size.into().0); +        self.text_size = Some(text_size.into());          self      } @@ -160,6 +160,14 @@ where      Renderer: text::Renderer,      Renderer::Theme: StyleSheet + crate::text::StyleSheet,  { +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<widget::text::State<Renderer::Paragraph>>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) +    } +      fn width(&self) -> Length {          self.width      } @@ -170,32 +178,41 @@ where      fn layout(          &self, +        tree: &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::State<Renderer::Paragraph>>(); + +                    widget::text::layout( +                        state, +                        renderer, +                        limits, +                        self.width, +                        Length::Shrink, +                        label, +                        self.text_line_height, +                        self.text_size, +                        self.font, +                        self.text_alignment, +                        alignment::Vertical::Top, +                        self.text_shaping,                      ) -                    .line_height(self.text_line_height) -                    .shaping(self.text_shaping), -            ); -        } - -        row = row.push(Row::new().width(2.0 * self.size).height(self.size)); - -        row.layout(renderer, limits) +                } else { +                    layout::Node::new(Size::ZERO) +                } +            }, +        )      }      fn on_event( @@ -243,7 +260,7 @@ where      fn draw(          &self, -        _state: &Tree, +        tree: &Tree,          renderer: &mut Renderer,          theme: &Renderer::Theme,          style: &renderer::Style, @@ -259,28 +276,21 @@ where          const SPACE_RATIO: f32 = 0.05;          let mut children = layout.children(); +        let toggler_layout = children.next().unwrap(); -        if let Some(label) = &self.label { +        if self.label.is_some() {              let label_layout = children.next().unwrap();              crate::text::draw(                  renderer,                  style,                  label_layout, -                label, -                self.text_size, -                self.text_line_height, -                self.font, +                tree.state.downcast_ref(),                  Default::default(), -                self.text_alignment, -                alignment::Vertical::Center, -                self.text_shaping,              );          } -        let toggler_layout = children.next().unwrap();          let bounds = toggler_layout.bounds(); -          let is_mouse_over = cursor.is_over(layout.bounds());          let style = if is_mouse_over { diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index faa3f3e1..534d901a 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -107,11 +107,14 @@ where      Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,  {      fn children(&self) -> Vec<widget::Tree> { -        vec![widget::Tree::new(&self.content)] +        vec![ +            widget::Tree::new(&self.content), +            widget::Tree::new(&self.tooltip as &dyn Widget<Message, _>), +        ]      }      fn diff(&self, tree: &mut widget::Tree) { -        tree.diff_children(std::slice::from_ref(&self.content)) +        tree.diff_children(&[self.content.as_widget(), &self.tooltip])      }      fn state(&self) -> widget::tree::State { @@ -132,10 +135,11 @@ where      fn layout(          &self, +        tree: &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<overlay::Element<'b, Message, Renderer>> {          let state = tree.state.downcast_ref::<State>(); +        let mut children = tree.children.iter_mut(); +          let content = self.content.as_widget_mut().overlay( -            &mut tree.children[0], +            children.next().unwrap(),              layout,              renderer,          ); @@ -225,6 +231,7 @@ where                  layout.position(),                  Box::new(Overlay {                      tooltip: &self.tooltip, +                    state: children.next().unwrap(),                      cursor_position,                      content_bounds: layout.bounds(),                      snap_within_viewport: self.snap_within_viewport, @@ -295,6 +302,7 @@ where      Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,  {      tooltip: &'b Text<'a, Renderer>, +    state: &'b 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 efca302a..1efcd63b 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 { | 
