Merge pull request #2058 from iced-rs/explicit-text-caching

Explicit text caching
This commit is contained in:
Héctor Ramón 2023-09-10 01:14:39 +02:00 committed by GitHub
commit 1af5ff41ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 2684 additions and 1778 deletions

View file

@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### 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) - `Theme::Custom::with_fn` for custom extended palette generation. [#2067](https://github.com/iced-rs/iced/pull/2067)
### Changed ### Changed

View file

@ -306,10 +306,11 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.widget.layout(renderer, limits) self.widget.layout(tree, renderer, limits)
} }
fn operate( fn operate(
@ -491,10 +492,11 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.element.widget.layout(renderer, limits) self.element.widget.layout(tree, renderer, limits)
} }
fn operate( fn operate(

View file

@ -7,7 +7,7 @@ pub mod flex;
pub use limits::Limits; pub use limits::Limits;
pub use node::Node; 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. /// The bounds of a [`Node`] and its children, using absolute coordinates.
#[derive(Debug, Clone, Copy)] #[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],
)
}

View file

@ -19,6 +19,7 @@
use crate::Element; use crate::Element;
use crate::layout::{Limits, Node}; use crate::layout::{Limits, Node};
use crate::widget;
use crate::{Alignment, Padding, Point, Size}; use crate::{Alignment, Padding, Point, Size};
/// The main axis of a flex layout. /// The main axis of a flex layout.
@ -66,6 +67,7 @@ pub fn resolve<Message, Renderer>(
spacing: f32, spacing: f32,
align_items: Alignment, align_items: Alignment,
items: &[Element<'_, Message, Renderer>], items: &[Element<'_, Message, Renderer>],
trees: &mut [widget::Tree],
) -> Node ) -> Node
where where
Renderer: crate::Renderer, Renderer: crate::Renderer,
@ -81,7 +83,7 @@ where
let mut nodes: Vec<Node> = Vec::with_capacity(items.len()); let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
nodes.resize(items.len(), Node::default()); 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 { let fill_factor = match axis {
Axis::Horizontal => child.as_widget().width(), Axis::Horizontal => child.as_widget().width(),
Axis::Vertical => child.as_widget().height(), Axis::Vertical => child.as_widget().height(),
@ -94,7 +96,8 @@ where
let child_limits = let child_limits =
Limits::new(Size::ZERO, Size::new(max_width, max_height)); 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(); let size = layout.size();
available -= axis.main(size); available -= axis.main(size);
@ -108,7 +111,7 @@ where
let remaining = available.max(0.0); 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 { let fill_factor = match axis {
Axis::Horizontal => child.as_widget().width(), Axis::Horizontal => child.as_widget().width(),
Axis::Vertical => child.as_widget().height(), Axis::Vertical => child.as_widget().height(),
@ -133,7 +136,8 @@ where
Size::new(max_width, max_height), 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())); cross = cross.max(axis.cross(layout.size()));
nodes[i] = layout; nodes[i] = layout;

View file

@ -25,7 +25,7 @@ where
/// ///
/// [`Node`]: layout::Node /// [`Node`]: layout::Node
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,

View file

@ -54,7 +54,7 @@ where
/// Computes the layout of the [`Element`] in the given bounds. /// Computes the layout of the [`Element`] in the given bounds.
pub fn layout( pub fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
translation: Vector, translation: Vector,
@ -150,7 +150,7 @@ where
Renderer: crate::Renderer, Renderer: crate::Renderer,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,

View file

@ -61,7 +61,7 @@ where
Renderer: crate::Renderer, Renderer: crate::Renderer,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,
@ -71,7 +71,7 @@ where
layout::Node::with_children( layout::Node::with_children(
bounds, bounds,
self.children self.children
.iter() .iter_mut()
.map(|child| child.layout(renderer, bounds, translation)) .map(|child| child.layout(renderer, bounds, translation))
.collect(), .collect(),
) )

View file

@ -5,26 +5,13 @@ mod null;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub use null::Null; pub use null::Null;
use crate::layout; use crate::{Background, BorderRadius, Color, Rectangle, Vector};
use crate::{Background, BorderRadius, Color, Element, Rectangle, Vector};
/// A component that can be used by widgets to draw themselves on a screen. /// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer: Sized { pub trait Renderer: Sized {
/// The supported theme of the [`Renderer`]. /// The supported theme of the [`Renderer`].
type Theme; 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. /// Draws the primitives recorded in the given closure in a new layer.
/// ///
/// The layer will clip its contents to the provided `bounds`. /// The layer will clip its contents to the provided `bounds`.

View file

@ -1,6 +1,7 @@
use crate::alignment;
use crate::renderer::{self, Renderer}; use crate::renderer::{self, Renderer};
use crate::text::{self, Text}; 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; use std::borrow::Cow;
@ -41,6 +42,7 @@ impl Renderer for Null {
impl text::Renderer for Null { impl text::Renderer for Null {
type Font = Font; type Font = Font;
type Paragraph = ();
const ICON_FONT: Font = Font::DEFAULT; const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0'; const CHECKMARK_ICON: char = '0';
@ -50,37 +52,83 @@ impl text::Renderer for Null {
Font::default() Font::default()
} }
fn default_size(&self) -> f32 { fn default_size(&self) -> Pixels {
16.0 Pixels(16.0)
} }
fn load_font(&mut self, _font: Cow<'static, [u8]>) {} fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
fn measure( fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph {
&self,
_content: &str,
_size: f32,
_line_height: text::LineHeight,
_font: Font,
_bounds: Size,
_shaping: text::Shaping,
) -> Size {
Size::new(0.0, 20.0)
} }
fn hit_test( fn resize_paragraph(
&self, &self,
_contents: &str, _paragraph: &mut Self::Paragraph,
_size: f32, _new_bounds: Size,
_line_height: text::LineHeight, ) {
_font: Self::Font, }
_bounds: Size,
_shaping: text::Shaping, fn fill_paragraph(
_point: Point, &mut self,
_nearest_only: bool, _paragraph: &Self::Paragraph,
) -> Option<text::Hit> { _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 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
}
} }

View file

@ -1,6 +1,6 @@
//! Draw and interact with text. //! Draw and interact with text.
use crate::alignment; use crate::alignment;
use crate::{Color, Pixels, Point, Rectangle, Size}; use crate::{Color, Pixels, Point, Size};
use std::borrow::Cow; use std::borrow::Cow;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@ -12,17 +12,14 @@ pub struct Text<'a, Font> {
pub content: &'a str, pub content: &'a str,
/// The bounds of the paragraph. /// The bounds of the paragraph.
pub bounds: Rectangle, pub bounds: Size,
/// The size of the [`Text`] in logical pixels. /// The size of the [`Text`] in logical pixels.
pub size: f32, pub size: Pixels,
/// The line height of the [`Text`]. /// The line height of the [`Text`].
pub line_height: LineHeight, pub line_height: LineHeight,
/// The color of the [`Text`].
pub color: Color,
/// The font of the [`Text`]. /// The font of the [`Text`].
pub font: Font, pub font: Font,
@ -132,7 +129,10 @@ impl Hit {
/// A renderer capable of measuring and drawing [`Text`]. /// A renderer capable of measuring and drawing [`Text`].
pub trait Renderer: crate::Renderer { pub trait Renderer: crate::Renderer {
/// The font type used. /// 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. /// The icon font of the backend.
const ICON_FONT: Self::Font; const ICON_FONT: Self::Font;
@ -151,62 +151,151 @@ pub trait Renderer: crate::Renderer {
fn default_font(&self) -> Self::Font; fn default_font(&self) -> Self::Font;
/// Returns the default size of [`Text`]. /// Returns the default size of [`Text`].
fn default_size(&self) -> f32; fn default_size(&self) -> Pixels;
/// Measures the text in the given bounds and returns the minimum boundaries
/// that can fit the contents.
fn measure(
&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(
&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
}
/// Tests whether the provided point is within the boundaries of text
/// laid out with the given parameters, returning information about
/// the nearest character.
///
/// If `nearest_only` is true, the hit test does not consider whether the
/// the point is interior to any glyph bounds, returning only the character
/// with the nearest centeroid.
fn hit_test(
&self,
contents: &str,
size: f32,
line_height: LineHeight,
font: Self::Font,
bounds: Size,
shaping: Shaping,
point: Point,
nearest_only: bool,
) -> Option<Hit>;
/// Loads a [`Self::Font`] from its bytes. /// Loads a [`Self::Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>); fn load_font(&mut self, font: Cow<'static, [u8]>);
/// Draws the given [`Text`]. /// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn fill_text(&mut self, text: Text<'_, Self::Font>); fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph;
/// Lays out the given [`Paragraph`] with some new boundaries.
fn resize_paragraph(
&self,
paragraph: &mut Self::Paragraph,
new_bounds: Size,
);
/// Updates a [`Paragraph`] to match the given [`Text`], if needed.
fn update_paragraph(
&self,
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
}
}
/// 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.
///
/// The text can be reused as it is!
None,
/// A bounds difference.
///
/// This normally means a relayout is necessary, but the shape of the text can
/// be reused.
Bounds,
/// 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
}
} }

View file

@ -55,6 +55,7 @@ where
/// user interface. /// user interface.
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node; ) -> layout::Node;
@ -62,7 +63,7 @@ where
/// Draws the [`Widget`] using the associated `Renderer`. /// Draws the [`Widget`] using the associated `Renderer`.
fn draw( fn draw(
&self, &self,
state: &Tree, tree: &Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
theme: &Renderer::Theme, theme: &Renderer::Theme,
style: &renderer::Style, style: &renderer::Style,

View file

@ -3,9 +3,9 @@ use crate::alignment;
use crate::layout; use crate::layout;
use crate::mouse; use crate::mouse;
use crate::renderer; use crate::renderer;
use crate::text; use crate::text::{self, Paragraph};
use crate::widget::Tree; use crate::widget::tree::{self, Tree};
use crate::{Color, Element, Layout, Length, Pixels, Rectangle, Widget}; use crate::{Color, Element, Layout, Length, Pixels, Point, Rectangle, Widget};
use std::borrow::Cow; use std::borrow::Cow;
@ -19,7 +19,7 @@ where
Renderer::Theme: StyleSheet, Renderer::Theme: StyleSheet,
{ {
content: Cow<'a, str>, content: Cow<'a, str>,
size: Option<f32>, size: Option<Pixels>,
line_height: LineHeight, line_height: LineHeight,
width: Length, width: Length,
height: Length, height: Length,
@ -53,7 +53,7 @@ where
/// Sets the size of the [`Text`]. /// Sets the size of the [`Text`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self { pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = Some(size.into().0); self.size = Some(size.into());
self 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> impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer>
where where
Renderer: text::Renderer, Renderer: text::Renderer,
Renderer::Theme: StyleSheet, 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 { fn width(&self) -> Length {
self.width self.width
} }
@ -132,30 +144,29 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
let limits = limits.width(self.width).height(self.height); layout(
tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
let size = self.size.unwrap_or_else(|| renderer.default_size()); renderer,
limits,
let bounds = renderer.measure( self.width,
self.height,
&self.content, &self.content,
size,
self.line_height, self.line_height,
self.font.unwrap_or_else(|| renderer.default_font()), self.size,
limits.max(), self.font,
self.horizontal_alignment,
self.vertical_alignment,
self.shaping, self.shaping,
); )
let size = limits.resolve(bounds);
layout::Node::new(size)
} }
fn draw( fn draw(
&self, &self,
_state: &Tree, tree: &Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
theme: &Renderer::Theme, theme: &Renderer::Theme,
style: &renderer::Style, style: &renderer::Style,
@ -163,22 +174,63 @@ where
_cursor_position: mouse::Cursor, _cursor_position: mouse::Cursor,
_viewport: &Rectangle, _viewport: &Rectangle,
) { ) {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
draw( draw(
renderer, renderer,
style, style,
layout, layout,
&self.content, state,
self.size,
self.line_height,
self.font,
theme.appearance(self.style.clone()), 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. /// Draws text using the same logic as the [`Text`] widget.
/// ///
/// Specifically: /// Specifically:
@ -193,44 +245,31 @@ pub fn draw<Renderer>(
renderer: &mut Renderer, renderer: &mut Renderer,
style: &renderer::Style, style: &renderer::Style,
layout: Layout<'_>, layout: Layout<'_>,
content: &str, state: &State<Renderer::Paragraph>,
size: Option<f32>,
line_height: LineHeight,
font: Option<Renderer::Font>,
appearance: Appearance, appearance: Appearance,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
shaping: Shaping,
) where ) where
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
let State(ref paragraph) = state;
let bounds = layout.bounds(); let bounds = layout.bounds();
let x = match horizontal_alignment { let x = match paragraph.horizontal_alignment() {
alignment::Horizontal::Left => bounds.x, alignment::Horizontal::Left => bounds.x,
alignment::Horizontal::Center => bounds.center_x(), alignment::Horizontal::Center => bounds.center_x(),
alignment::Horizontal::Right => bounds.x + bounds.width, 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::Top => bounds.y,
alignment::Vertical::Center => bounds.center_y(), alignment::Vertical::Center => bounds.center_y(),
alignment::Vertical::Bottom => bounds.y + bounds.height, alignment::Vertical::Bottom => bounds.y + bounds.height,
}; };
let size = size.unwrap_or_else(|| renderer.default_size()); renderer.fill_paragraph(
paragraph,
renderer.fill_text(crate::Text { Point::new(x, y),
content, appearance.color.unwrap_or(style.text_color),
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,
});
} }
impl<'a, Message, Renderer> From<Text<'a, Renderer>> impl<'a, Message, Renderer> From<Text<'a, Renderer>>

View file

@ -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. /// The identifier of some widget state.
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Tag(any::TypeId); pub struct Tag(any::TypeId);

View file

@ -3,8 +3,8 @@ use iced::mouse;
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};
use iced::widget::{column, row, text, Slider}; use iced::widget::{column, row, text, Slider};
use iced::{ use iced::{
Color, Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Color, Element, Length, Pixels, Point, Rectangle, Renderer, Sandbox,
Size, Vector, Settings, Size, Vector,
}; };
use palette::{ use palette::{
self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue, self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue,
@ -168,7 +168,7 @@ impl Theme {
let mut text = canvas::Text { let mut text = canvas::Text {
horizontal_alignment: alignment::Horizontal::Center, horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Top, vertical_alignment: alignment::Vertical::Top,
size: 15.0, size: Pixels(15.0),
..canvas::Text::default() ..canvas::Text::default()
}; };

View file

@ -40,7 +40,6 @@ impl Sandbox for Example {
Message::Selected(language) => { Message::Selected(language) => {
self.selected_language = Some(language); self.selected_language = Some(language);
self.text = language.hello().to_string(); self.text = language.hello().to_string();
self.languages.unfocus();
} }
Message::OptionHovered(language) => { Message::OptionHovered(language) => {
self.text = language.hello().to_string(); self.text = language.hello().to_string();

View file

@ -36,6 +36,7 @@ mod quad {
fn layout( fn layout(
&self, &self,
_tree: &mut widget::Tree,
_renderer: &Renderer, _renderer: &Renderer,
_limits: &layout::Limits, _limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -43,6 +43,7 @@ mod circle {
fn layout( fn layout(
&self, &self,
_tree: &mut widget::Tree,
_renderer: &Renderer, _renderer: &Renderer,
_limits: &layout::Limits, _limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -591,7 +591,7 @@ mod grid {
let text = Text { let text = Text {
color: Color::WHITE, color: Color::WHITE,
size: 14.0, size: 14.0.into(),
position: Point::new(frame.width(), frame.height()), position: Point::new(frame.width(), frame.height()),
horizontal_alignment: alignment::Horizontal::Right, horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Bottom, vertical_alignment: alignment::Vertical::Bottom,

View file

@ -26,6 +26,7 @@ mod rainbow {
fn layout( fn layout(
&self, &self,
_tree: &mut widget::Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -8,7 +8,7 @@ use iced_wgpu::graphics::Viewport;
use iced_wgpu::{wgpu, Backend, Renderer, Settings}; use iced_wgpu::{wgpu, Backend, Renderer, Settings};
use iced_winit::core::mouse; use iced_winit::core::mouse;
use iced_winit::core::renderer; 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::program;
use iced_winit::runtime::Debug; use iced_winit::runtime::Debug;
use iced_winit::style::Theme; use iced_winit::style::Theme;
@ -142,12 +142,11 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize iced // Initialize iced
let mut debug = Debug::new(); let mut debug = Debug::new();
let mut renderer = Renderer::new(Backend::new( let mut renderer = Renderer::new(
&device, Backend::new(&device, &queue, Settings::default(), format),
&queue, Font::default(),
Settings::default(), Pixels(16.0),
format, );
));
let mut state = program::State::new( let mut state = program::State::new(
controls, controls,

View file

@ -254,6 +254,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &iced::Renderer<Theme>, _renderer: &iced::Renderer<Theme>,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -175,6 +175,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -287,10 +287,15 @@ mod modal {
fn layout( fn layout(
&self, &self,
tree: &mut widget::Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.base.as_widget().layout(renderer, limits) self.base.as_widget().layout(
&mut tree.children[0],
renderer,
limits,
)
} }
fn on_event( fn on_event(
@ -401,7 +406,7 @@ mod modal {
Message: Clone, Message: Clone,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
_bounds: Size, _bounds: Size,
position: Point, position: Point,
@ -410,7 +415,11 @@ mod modal {
.width(Length::Fill) .width(Length::Fill)
.height(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()); child.align(Alignment::Center, Alignment::Center, limits.max());
let mut node = layout::Node::with_children(self.size, vec![child]); let mut node = layout::Node::with_children(self.size, vec![child]);

View file

@ -328,10 +328,15 @@ mod toast {
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> 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 { fn tag(&self) -> widget::tree::Tag {
@ -502,7 +507,7 @@ mod toast {
for Overlay<'a, 'b, Message> for Overlay<'a, 'b, Message>
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,
@ -519,6 +524,7 @@ mod toast {
10.0, 10.0,
Alignment::End, Alignment::End,
self.toasts, self.toasts,
self.state,
) )
.translate(Vector::new(position.x, position.y)) .translate(Vector::new(position.x, position.y))
} }

View file

@ -9,18 +9,21 @@ publish = false
iced.workspace = true iced.workspace = true
iced.features = ["async-std", "debug"] iced.features = ["async-std", "debug"]
once_cell.workspace = true
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
once_cell.workspace = true uuid = { version = "1.0", features = ["v4", "fast-rng", "serde"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-std.workspace = true async-std.workspace = true
directories-next = "2.0" directories-next = "2.0"
tracing-subscriber = "0.3"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
iced.workspace = true iced.workspace = true
iced.features = ["debug", "webgl"] iced.features = ["debug", "webgl"]
uuid = { version = "1.0", features = ["js"] }
web-sys = { workspace = true, features = ["Window", "Storage"] } web-sys = { workspace = true, features = ["Window", "Storage"] }
wasm-timer.workspace = true wasm-timer.workspace = true

View file

@ -3,8 +3,8 @@ use iced::font::{self, Font};
use iced::keyboard; use iced::keyboard;
use iced::theme::{self, Theme}; use iced::theme::{self, Theme};
use iced::widget::{ use iced::widget::{
self, button, checkbox, column, container, row, scrollable, text, self, button, checkbox, column, container, keyed_column, row, scrollable,
text_input, Text, text, text_input, Text,
}; };
use iced::window; use iced::window;
use iced::{Application, Element}; use iced::{Application, Element};
@ -12,10 +12,14 @@ use iced::{Color, Command, Length, Settings, Subscription};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
static INPUT_ID: Lazy<text_input::Id> = Lazy::new(text_input::Id::unique); static INPUT_ID: Lazy<text_input::Id> = Lazy::new(text_input::Id::unique);
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
#[cfg(not(target_arch = "wasm32"))]
tracing_subscriber::fmt::init();
Todos::run(Settings { Todos::run(Settings {
window: window::Settings { window: window::Settings {
size: (500, 800), size: (500, 800),
@ -220,17 +224,19 @@ impl Application for Todos {
tasks.iter().filter(|task| filter.matches(task)); tasks.iter().filter(|task| filter.matches(task));
let tasks: Element<_> = if filtered_tasks.count() > 0 { let tasks: Element<_> = if filtered_tasks.count() > 0 {
column( keyed_column(
tasks tasks
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_, task)| filter.matches(task)) .filter(|(_, task)| filter.matches(task))
.map(|(i, task)| { .map(|(i, task)| {
task.view(i).map(move |message| { (
Message::TaskMessage(i, message) task.id,
}) task.view(i).map(move |message| {
}) Message::TaskMessage(i, message)
.collect(), }),
)
}),
) )
.spacing(10) .spacing(10)
.into() .into()
@ -279,6 +285,8 @@ impl Application for Todos {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct Task { struct Task {
#[serde(default = "Uuid::new_v4")]
id: Uuid,
description: String, description: String,
completed: bool, completed: bool,
@ -314,6 +322,7 @@ impl Task {
fn new(description: String) -> Self { fn new(description: String) -> Self {
Task { Task {
id: Uuid::new_v4(),
description, description,
completed: false, completed: false,
state: TaskState::Idle, state: TaskState::Idle,

View file

@ -5,7 +5,7 @@ use iced::widget::{
scrollable, slider, text, text_input, toggler, vertical_space, scrollable, slider, text, text_input, toggler, vertical_space,
}; };
use iced::widget::{Button, Column, Container, Slider}; 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 { pub fn main() -> iced::Result {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -578,7 +578,7 @@ impl<'a> Step {
text_input = text_input.icon(text_input::Icon { text_input = text_input.icon(text_input::Icon {
font: Font::default(), font: Font::default(),
code_point: '🚀', code_point: '🚀',
size: Some(28.0), size: Some(Pixels(28.0)),
spacing: 10.0, spacing: 10.0,
side: text_input::Side::Right, side: text_input::Side::Right,
}); });

View file

@ -30,6 +30,8 @@ half.workspace = true
log.workspace = true log.workspace = true
raw-window-handle.workspace = true raw-window-handle.workspace = true
thiserror.workspace = true thiserror.workspace = true
cosmic-text.workspace = true
rustc-hash.workspace = true
lyon_path.workspace = true lyon_path.workspace = true
lyon_path.optional = true lyon_path.optional = true
@ -39,3 +41,9 @@ image.optional = true
kamadak-exif.workspace = true kamadak-exif.workspace = true
kamadak-exif.optional = true kamadak-exif.optional = true
twox-hash.workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
twox-hash.workspace = true
twox-hash.features = ["std"]

View file

@ -1,8 +1,8 @@
//! Write a graphics backend. //! Write a graphics backend.
use iced_core::image; use crate::core::image;
use iced_core::svg; use crate::core::svg;
use iced_core::text; use crate::core::Size;
use iced_core::{Font, Point, Size}; use crate::text;
use std::borrow::Cow; use std::borrow::Cow;
@ -12,70 +12,15 @@ use std::borrow::Cow;
pub trait Backend { pub trait Backend {
/// The custom kind of primitives this [`Backend`] supports. /// The custom kind of primitives this [`Backend`] supports.
type Primitive; 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. /// A graphics backend that supports text rendering.
pub trait Text { pub trait Text {
/// The icon font of the backend. /// Loads a font from its bytes.
const ICON_FONT: Font;
/// The `char` representing a ✔ icon in the [`ICON_FONT`].
///
/// [`ICON_FONT`]: Self::ICON_FONT
const CHECKMARK_ICON: char;
/// The `char` representing a ▼ icon in the built-in [`ICON_FONT`].
///
/// [`ICON_FONT`]: Self::ICON_FONT
const ARROW_DOWN_ICON: char;
/// Returns the default [`Font`].
fn default_font(&self) -> Font;
/// Returns the default size of text.
fn default_size(&self) -> f32;
/// Measures the text contents with the given size and font,
/// returning the size of a laid out paragraph that fits in the provided
/// bounds.
fn measure(
&self,
contents: &str,
size: f32,
line_height: text::LineHeight,
font: Font,
bounds: Size,
shaping: text::Shaping,
) -> Size;
/// Tests whether the provided point is within the boundaries of [`Text`]
/// laid out with the given parameters, returning information about
/// the nearest character.
///
/// If nearest_only is true, the hit test does not consider whether the
/// the point is interior to any glyph bounds, returning only the character
/// with the nearest centeroid.
fn hit_test(
&self,
contents: &str,
size: f32,
line_height: text::LineHeight,
font: Font,
bounds: Size,
shaping: text::Shaping,
point: Point,
nearest_only: bool,
) -> Option<text::Hit>;
/// Loads a [`Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>); 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. /// A graphics backend that supports image rendering.

View file

@ -40,6 +40,32 @@ impl<T: Damage> Damage for Primitive<T> {
bounds.expand(1.5) 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::Quad { bounds, .. }
| Self::Image { bounds, .. } | Self::Image { bounds, .. }
| Self::Svg { bounds, .. } => bounds.expand(1.0), | Self::Svg { bounds, .. } => bounds.expand(1.0),

View file

@ -1,6 +1,6 @@
use crate::core::alignment; use crate::core::alignment;
use crate::core::text::{LineHeight, Shaping}; 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 /// A bunch of text that can be drawn to a canvas
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -19,7 +19,7 @@ pub struct Text {
/// The color of the text /// The color of the text
pub color: Color, pub color: Color,
/// The size of the text /// The size of the text
pub size: f32, pub size: Pixels,
/// The line height of the text. /// The line height of the text.
pub line_height: LineHeight, pub line_height: LineHeight,
/// The font of the text /// The font of the text
@ -38,7 +38,7 @@ impl Default for Text {
content: String::new(), content: String::new(),
position: Point::ORIGIN, position: Point::ORIGIN,
color: Color::BLACK, color: Color::BLACK,
size: 16.0, size: Pixels(16.0),
line_height: LineHeight::Relative(1.2), line_height: LineHeight::Relative(1.2),
font: Font::default(), font: Font::default(),
horizontal_alignment: alignment::Horizontal::Left, horizontal_alignment: alignment::Horizontal::Left,

View file

@ -10,7 +10,7 @@
#![forbid(rust_2018_idioms)] #![forbid(rust_2018_idioms)]
#![deny( #![deny(
missing_debug_implementations, missing_debug_implementations,
missing_docs, //missing_docs,
unsafe_code, unsafe_code,
unused_results, unused_results,
clippy::extra_unused_lifetimes, clippy::extra_unused_lifetimes,
@ -35,6 +35,7 @@ pub mod damage;
pub mod gradient; pub mod gradient;
pub mod mesh; pub mod mesh;
pub mod renderer; pub mod renderer;
pub mod text;
#[cfg(feature = "geometry")] #[cfg(feature = "geometry")]
pub mod geometry; pub mod geometry;

View file

@ -3,7 +3,8 @@ use crate::core::alignment;
use crate::core::image; use crate::core::image;
use crate::core::svg; use crate::core::svg;
use crate::core::text; 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; use std::sync::Arc;
@ -19,7 +20,7 @@ pub enum Primitive<T> {
/// The color of the text /// The color of the text
color: Color, color: Color,
/// The size of the text in logical pixels /// The size of the text in logical pixels
size: f32, size: Pixels,
/// The line height of the text /// The line height of the text
line_height: text::LineHeight, line_height: text::LineHeight,
/// The font of the text /// The font of the text
@ -31,6 +32,15 @@ pub enum Primitive<T> {
/// The shaping strategy of the text. /// The shaping strategy of the text.
shaping: text::Shaping, 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 /// A quad primitive
Quad { Quad {
/// The bounds of the quad /// The bounds of the quad

View file

@ -1,15 +1,15 @@
//! Create a renderer from a [`Backend`]. //! Create a renderer from a [`Backend`].
use crate::backend::{self, Backend}; use crate::backend::{self, Backend};
use crate::Primitive; use crate::core;
use crate::core::image;
use iced_core::image; use crate::core::renderer;
use iced_core::layout; use crate::core::svg;
use iced_core::renderer; use crate::core::text::Text;
use iced_core::svg; use crate::core::{
use iced_core::text::{self, Text}; Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
use iced_core::{
Background, Color, Element, Font, Point, Rectangle, Size, Vector,
}; };
use crate::text;
use crate::Primitive;
use std::borrow::Cow; use std::borrow::Cow;
use std::marker::PhantomData; use std::marker::PhantomData;
@ -18,15 +18,23 @@ use std::marker::PhantomData;
#[derive(Debug)] #[derive(Debug)]
pub struct Renderer<B: Backend, Theme> { pub struct Renderer<B: Backend, Theme> {
backend: B, backend: B,
default_font: Font,
default_text_size: Pixels,
primitives: Vec<Primitive<B::Primitive>>, primitives: Vec<Primitive<B::Primitive>>,
theme: PhantomData<Theme>, theme: PhantomData<Theme>,
} }
impl<B: Backend, T> Renderer<B, T> { impl<B: Backend, T> Renderer<B, T> {
/// Creates a new [`Renderer`] from the given [`Backend`]. /// 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 { Self {
backend, backend,
default_font,
default_text_size,
primitives: Vec::new(), primitives: Vec::new(),
theme: PhantomData, theme: PhantomData,
} }
@ -88,16 +96,6 @@ impl<B: Backend, T> Renderer<B, T> {
impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> { impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> {
type Theme = 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)) { fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) {
let current = self.start_layer(); 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 where
B: Backend + backend::Text, B: Backend + backend::Text,
{ {
type Font = Font; type Font = Font;
type Paragraph = text::Paragraph;
const ICON_FONT: Font = B::ICON_FONT; const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = B::CHECKMARK_ICON; const CHECKMARK_ICON: char = '\u{f00c}';
const ARROW_DOWN_ICON: char = B::ARROW_DOWN_ICON; const ARROW_DOWN_ICON: char = '\u{e800}';
fn default_font(&self) -> Self::Font { fn default_font(&self) -> Self::Font {
self.backend().default_font() self.default_font
} }
fn default_size(&self) -> f32 { fn default_size(&self) -> Pixels {
self.backend().default_size() self.default_text_size
}
fn measure(
&self,
content: &str,
size: f32,
line_height: text::LineHeight,
font: Font,
bounds: Size,
shaping: text::Shaping,
) -> Size {
self.backend().measure(
content,
size,
line_height,
font,
bounds,
shaping,
)
}
fn hit_test(
&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,
)
} }
fn load_font(&mut self, bytes: Cow<'static, [u8]>) { fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
self.backend.load_font(bytes); self.backend.load_font(bytes);
} }
fn fill_text(&mut self, text: Text<'_, Self::Font>) { fn create_paragraph(&self, text: Text<'_, Self::Font>) -> text::Paragraph {
text::Paragraph::with_text(text, self.backend.font_system())
}
fn update_paragraph(
&self,
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 resize_paragraph(
&self,
paragraph: &mut Self::Paragraph,
new_bounds: Size,
) {
paragraph.resize(new_bounds, self.backend.font_system());
}
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>,
position: Point,
color: Color,
) {
self.primitives.push(Primitive::Text { self.primitives.push(Primitive::Text {
content: text.content.to_string(), content: text.content.to_string(),
bounds: text.bounds, bounds: Rectangle::new(position, text.bounds),
size: text.size, size: text.size,
line_height: text.line_height, line_height: text.line_height,
color: text.color, color,
font: text.font, font: text.font,
horizontal_alignment: text.horizontal_alignment, horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment, vertical_alignment: text.vertical_alignment,

137
graphics/src/text.rs Normal file
View file

@ -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,
}
}

132
graphics/src/text/cache.rs Normal file
View file

@ -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,
}

View file

@ -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", &paragraph.content)
.field("font", &paragraph.font)
.field("shaping", &paragraph.shaping)
.field("horizontal_alignment", &paragraph.horizontal_alignment)
.field("vertical_alignment", &paragraph.vertical_alignment)
.field("bounds", &paragraph.bounds)
.field("min_bounds", &paragraph.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,
}
}
}

View file

@ -224,16 +224,15 @@ impl Candidate {
match self { match self {
Self::TinySkia => { Self::TinySkia => {
let (compositor, backend) = let (compositor, backend) =
iced_tiny_skia::window::compositor::new( iced_tiny_skia::window::compositor::new();
iced_tiny_skia::Settings {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
},
);
Ok(( Ok((
Compositor::TinySkia(compositor), 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")] #[cfg(feature = "wgpu")]
@ -250,7 +249,11 @@ impl Candidate {
Ok(( Ok((
Compositor::Wgpu(compositor), 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"))] #[cfg(not(feature = "wgpu"))]

View file

@ -29,7 +29,10 @@ pub use geometry::Geometry;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text::{self, Text}; 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 crate::graphics::Mesh;
use std::borrow::Cow; use std::borrow::Cow;
@ -155,6 +158,7 @@ impl<T> core::Renderer for Renderer<T> {
impl<T> text::Renderer for Renderer<T> { impl<T> text::Renderer for Renderer<T> {
type Font = Font; type Font = Font;
type Paragraph = Paragraph;
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT; const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON; 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()) delegate!(self, renderer, renderer.default_font())
} }
fn default_size(&self) -> f32 { fn default_size(&self) -> Pixels {
delegate!(self, renderer, renderer.default_size()) delegate!(self, renderer, renderer.default_size())
} }
fn measure( fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph {
&self, delegate!(self, renderer, renderer.create_paragraph(text))
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 hit_test( fn resize_paragraph(
&self, &self,
content: &str, paragraph: &mut Self::Paragraph,
size: f32, new_bounds: Size,
line_height: text::LineHeight, ) {
font: Font,
bounds: Size,
shaping: text::Shaping,
point: Point,
nearest_only: bool,
) -> Option<text::Hit> {
delegate!( delegate!(
self, self,
renderer, renderer,
renderer.hit_test( renderer.resize_paragraph(paragraph, new_bounds)
content, );
size,
line_height,
font,
bounds,
shaping,
point,
nearest_only
)
)
} }
fn load_font(&mut self, bytes: Cow<'static, [u8]>) { fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
delegate!(self, renderer, renderer.load_font(bytes)); delegate!(self, renderer, renderer.load_font(bytes));
} }
fn fill_text(&mut self, text: Text<'_, Self::Font>) { fn fill_paragraph(
delegate!(self, renderer, renderer.fill_text(text)); &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));
} }
} }

View file

@ -1,4 +1,4 @@
use crate::core::Font; use crate::core::{Font, Pixels};
use crate::graphics::Antialiasing; use crate::graphics::Antialiasing;
/// The settings of a Backend. /// The settings of a Backend.
@ -10,7 +10,7 @@ pub struct Settings {
/// The default size of text. /// The default size of text.
/// ///
/// By default, it will be set to `16.0`. /// 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. /// The antialiasing strategy that will be used for triangle primitives.
/// ///
@ -22,7 +22,7 @@ impl Default for Settings {
fn default() -> Settings { fn default() -> Settings {
Settings { Settings {
default_font: Font::default(), default_font: Font::default(),
default_text_size: 16.0, default_text_size: Pixels(16.0),
antialiasing: None, antialiasing: None,
} }
} }

View file

@ -95,8 +95,11 @@ where
let Cache { mut state } = cache; let Cache { mut state } = cache;
state.diff(root.as_widget()); state.diff(root.as_widget());
let base = let base = root.as_widget().layout(
renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)); &mut state,
renderer,
&layout::Limits::new(Size::ZERO, bounds),
);
UserInterface { UserInterface {
root, root,
@ -226,8 +229,9 @@ where
if shell.is_layout_invalid() { if shell.is_layout_invalid() {
let _ = ManuallyDrop::into_inner(manual_overlay); let _ = ManuallyDrop::into_inner(manual_overlay);
self.base = renderer.layout( self.base = self.root.as_widget().layout(
&self.root, &mut self.state,
renderer,
&layout::Limits::new(Size::ZERO, self.bounds), &layout::Limits::new(Size::ZERO, self.bounds),
); );
@ -325,8 +329,9 @@ where
} }
shell.revalidate_layout(|| { shell.revalidate_layout(|| {
self.base = renderer.layout( self.base = self.root.as_widget().layout(
&self.root, &mut self.state,
renderer,
&layout::Limits::new(Size::ZERO, self.bounds), &layout::Limits::new(Size::ZERO, self.bounds),
); );

View file

@ -1,6 +1,6 @@
//! Configure your application. //! Configure your application.
use crate::window; use crate::window;
use crate::Font; use crate::{Font, Pixels};
/// The settings of an application. /// The settings of an application.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -29,7 +29,7 @@ pub struct Settings<Flags> {
/// The text size that will be used by default. /// The text size that will be used by default.
/// ///
/// The default value is `16.0`. /// 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 /// If set to true, the renderer will try to perform antialiasing for some
/// primitives. /// primitives.
@ -80,7 +80,7 @@ where
window: Default::default(), window: Default::default(),
flags: Default::default(), flags: Default::default(),
default_font: Default::default(), default_font: Default::default(),
default_text_size: 16.0, default_text_size: Pixels(16.0),
antialiasing: false, antialiasing: false,
exit_on_close_request: true, exit_on_close_request: true,
} }

View file

@ -1,16 +1,12 @@
use crate::core::text; use crate::core::{Background, Color, Gradient, Rectangle, Vector};
use crate::core::Gradient;
use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector};
use crate::graphics::backend; use crate::graphics::backend;
use crate::graphics::text;
use crate::graphics::{Damage, Viewport}; use crate::graphics::{Damage, Viewport};
use crate::primitive::{self, Primitive}; use crate::primitive::{self, Primitive};
use crate::Settings;
use std::borrow::Cow; use std::borrow::Cow;
pub struct Backend { pub struct Backend {
default_font: Font,
default_text_size: f32,
text_pipeline: crate::text::Pipeline, text_pipeline: crate::text::Pipeline,
#[cfg(feature = "image")] #[cfg(feature = "image")]
@ -21,10 +17,8 @@ pub struct Backend {
} }
impl Backend { impl Backend {
pub fn new(settings: Settings) -> Self { pub fn new() -> Self {
Self { Self {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
text_pipeline: crate::text::Pipeline::new(), text_pipeline: crate::text::Pipeline::new(),
#[cfg(feature = "image")] #[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 { Primitive::Text {
content, content,
bounds, bounds,
@ -385,7 +405,7 @@ impl Backend {
let clip_mask = (!physical_bounds.is_within(&clip_bounds)) let clip_mask = (!physical_bounds.is_within(&clip_bounds))
.then_some(clip_mask as &_); .then_some(clip_mask as &_);
self.text_pipeline.draw( self.text_pipeline.draw_cached(
content, content,
*bounds + translation, *bounds + translation,
*color, *color,
@ -599,6 +619,12 @@ impl Backend {
} }
} }
impl Default for Backend {
fn default() -> Self {
Self::new()
}
}
fn into_color(color: Color) -> tiny_skia::Color { fn into_color(color: Color) -> tiny_skia::Color {
tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
.expect("Convert color from iced to tiny_skia") .expect("Convert color from iced to tiny_skia")
@ -779,58 +805,8 @@ impl iced_graphics::Backend for Backend {
} }
impl backend::Text for Backend { impl backend::Text for Backend {
const ICON_FONT: Font = Font::with_name("Iced-Icons"); fn font_system(&self) -> &text::FontSystem {
const CHECKMARK_ICON: char = '\u{f00c}'; self.text_pipeline.font_system()
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 load_font(&mut self, font: Cow<'static, [u8]>) { fn load_font(&mut self, font: Cow<'static, [u8]>) {
@ -840,7 +816,10 @@ impl backend::Text for Backend {
#[cfg(feature = "image")] #[cfg(feature = "image")]
impl backend::Image for Backend { 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) self.raster_pipeline.dimensions(handle)
} }
} }
@ -850,7 +829,7 @@ impl backend::Svg for Backend {
fn viewport_dimensions( fn viewport_dimensions(
&self, &self,
handle: &crate::core::svg::Handle, handle: &crate::core::svg::Handle,
) -> Size<u32> { ) -> crate::core::Size<u32> {
self.vector_pipeline.viewport_dimensions(handle) self.vector_pipeline.viewport_dimensions(handle)
} }
} }

View file

@ -1,4 +1,4 @@
use crate::core::Font; use crate::core::{Font, Pixels};
/// The settings of a [`Backend`]. /// The settings of a [`Backend`].
/// ///
@ -11,14 +11,14 @@ pub struct Settings {
/// The default size of text. /// The default size of text.
/// ///
/// By default, it will be set to `16.0`. /// By default, it will be set to `16.0`.
pub default_text_size: f32, pub default_text_size: Pixels,
} }
impl Default for Settings { impl Default for Settings {
fn default() -> Settings { fn default() -> Settings {
Settings { Settings {
default_font: Font::default(), default_font: Font::default(),
default_text_size: 16.0, default_text_size: Pixels(16.0),
} }
} }
} }

View file

@ -1,18 +1,18 @@
use crate::core::alignment; use crate::core::alignment;
use crate::core::font::{self, Font}; use crate::core::text::{LineHeight, Shaping};
use crate::core::text::{Hit, LineHeight, Shaping}; use crate::core::{Color, Font, Pixels, Point, Rectangle};
use crate::core::{Color, Pixels, Point, Rectangle, Size}; use crate::graphics::text::cache::{self, Cache};
use crate::graphics::text::paragraph;
use crate::graphics::text::FontSystem;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::hash_map; use std::collections::hash_map;
use std::hash::{BuildHasher, Hash, Hasher};
use std::sync::Arc;
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Pipeline { pub struct Pipeline {
font_system: RefCell<cosmic_text::FontSystem>, font_system: FontSystem,
glyph_cache: GlyphCache, glyph_cache: GlyphCache,
cache: RefCell<Cache>, cache: RefCell<Cache>,
} }
@ -20,31 +20,57 @@ pub struct Pipeline {
impl Pipeline { impl Pipeline {
pub fn new() -> Self { pub fn new() -> Self {
Pipeline { Pipeline {
font_system: RefCell::new(cosmic_text::FontSystem::new_with_fonts( font_system: FontSystem::new(),
[cosmic_text::fontdb::Source::Binary(Arc::new(
include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
))]
.into_iter(),
)),
glyph_cache: GlyphCache::new(), glyph_cache: GlyphCache::new(),
cache: RefCell::new(Cache::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]>) { pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
let _ = self.font_system.get_mut().db_mut().load_font_source( self.font_system.load_font(bytes);
cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
);
self.cache = RefCell::new(Cache::new()); self.cache = RefCell::new(Cache::new());
} }
pub fn draw( pub fn draw_paragraph(
&mut self,
paragraph: &paragraph::Weak,
position: Point,
color: Color,
scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
) {
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, &mut self,
content: &str, content: &str,
bounds: Rectangle, bounds: Rectangle,
color: Color, color: Color,
size: f32, size: Pixels,
line_height: LineHeight, line_height: LineHeight,
font: Font, font: Font,
horizontal_alignment: alignment::Horizontal, horizontal_alignment: alignment::Horizontal,
@ -54,197 +80,103 @@ impl Pipeline {
pixels: &mut tiny_skia::PixmapMut<'_>, pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>, 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 font_system = self.font_system.get_mut();
let key = Key { let key = cache::Key {
bounds: bounds.size(), bounds: bounds.size(),
content, content,
font, font,
size, size: size.into(),
line_height, line_height,
shaping, shaping,
}; };
let (_, entry) = self.cache.get_mut().allocate(font_system, key); let (_, entry) = self.cache.get_mut().allocate(font_system, key);
let max_width = entry.bounds.width * scale_factor; let width = entry.min_bounds.width;
let total_height = entry.bounds.height * scale_factor; let height = entry.min_bounds.height;
let bounds = bounds * scale_factor; draw(
font_system,
let x = match horizontal_alignment { &mut self.glyph_cache,
alignment::Horizontal::Left => bounds.x, &entry.buffer,
alignment::Horizontal::Center => bounds.x - max_width / 2.0, Rectangle {
alignment::Horizontal::Right => bounds.x - max_width, width,
}; height,
..bounds
let y = match vertical_alignment { },
alignment::Vertical::Top => bounds.y, color,
alignment::Vertical::Center => bounds.y - total_height / 2.0, horizontal_alignment,
alignment::Vertical::Bottom => bounds.y - total_height, vertical_alignment,
}; scale_factor,
pixels,
let mut swash = cosmic_text::SwashCache::new(); clip_mask,
);
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,
);
}
}
}
} }
pub fn trim_cache(&mut self) { pub fn trim_cache(&mut self) {
self.cache.get_mut().trim(); self.cache.get_mut().trim();
self.glyph_cache.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 { fn draw(
let (width, total_lines) = buffer font_system: &mut cosmic_text::FontSystem,
.layout_runs() glyph_cache: &mut GlyphCache,
.fold((0.0, 0usize), |(width, total_lines), run| { buffer: &cosmic_text::Buffer,
(run.line_w.max(width), total_lines + 1) 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;
Size::new(width, total_lines as f32 * buffer.metrics().line_height) 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,
};
fn to_family(family: font::Family) -> cosmic_text::Family<'static> { let y = match vertical_alignment {
match family { alignment::Vertical::Top => bounds.y,
font::Family::Name(name) => cosmic_text::Family::Name(name), alignment::Vertical::Center => bounds.y - bounds.height / 2.0,
font::Family::SansSerif => cosmic_text::Family::SansSerif, alignment::Vertical::Bottom => bounds.y - bounds.height,
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 { let mut swash = cosmic_text::SwashCache::new();
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 { for run in buffer.layout_runs() {
match stretch { for glyph in run.glyphs {
font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed, let physical_glyph = glyph.physical((x, y), scale_factor);
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 { if let Some((buffer, placement)) = glyph_cache.allocate(
match style { physical_glyph.cache_key,
font::Style::Normal => cosmic_text::Style::Normal, color,
font::Style::Italic => cosmic_text::Style::Italic, font_system,
font::Style::Oblique => cosmic_text::Style::Oblique, &mut swash,
} ) {
} let pixmap = tiny_skia::PixmapRef::from_bytes(
buffer,
placement.width,
placement.height,
)
.expect("Create glyph pixel map");
fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { pixels.draw_pixmap(
match shaping { physical_glyph.x + placement.left,
Shaping::Basic => cosmic_text::Shaping::Basic, physical_glyph.y - placement.top
Shaping::Advanced => cosmic_text::Shaping::Advanced, + (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;

View file

@ -28,9 +28,16 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
settings: Self::Settings, settings: Self::Settings,
_compatible_window: Option<&W>, _compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> { ) -> 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>( 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 { Compositor {
_theme: PhantomData, _theme: PhantomData,
}, },
Backend::new(settings), Backend::new(),
) )
} }

View file

@ -33,8 +33,6 @@ guillotiere.workspace = true
log.workspace = true log.workspace = true
once_cell.workspace = true once_cell.workspace = true
raw-window-handle.workspace = true raw-window-handle.workspace = true
rustc-hash.workspace = true
twox-hash.workspace = true
wgpu.workspace = true wgpu.workspace = true
lyon.workspace = true lyon.workspace = true
@ -45,7 +43,3 @@ resvg.optional = true
tracing.workspace = true tracing.workspace = true
tracing.optional = true tracing.optional = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
twox-hash.workspace = true
twox-hash.features = ["std"]

Binary file not shown.

View file

@ -1,5 +1,5 @@
use crate::core; use crate::core::{Color, Size};
use crate::core::{Color, Font, Point, Size}; use crate::graphics;
use crate::graphics::backend; use crate::graphics::backend;
use crate::graphics::color; use crate::graphics::color;
use crate::graphics::{Transformation, Viewport}; use crate::graphics::{Transformation, Viewport};
@ -29,9 +29,6 @@ pub struct Backend {
#[cfg(any(feature = "image", feature = "svg"))] #[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: image::Pipeline, image_pipeline: image::Pipeline,
default_font: Font,
default_text_size: f32,
} }
impl Backend { impl Backend {
@ -57,9 +54,6 @@ impl Backend {
#[cfg(any(feature = "image", feature = "svg"))] #[cfg(any(feature = "image", feature = "svg"))]
image_pipeline, 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 { impl crate::graphics::Backend for Backend {
type Primitive = primitive::Custom; type Primitive = primitive::Custom;
fn trim_measurements(&mut self) {
self.text_pipeline.trim_measurements();
}
} }
impl backend::Text for Backend { impl backend::Text for Backend {
const ICON_FONT: Font = Font::with_name("Iced-Icons"); fn font_system(&self) -> &graphics::text::FontSystem {
const CHECKMARK_ICON: char = '\u{f00c}'; self.text_pipeline.font_system()
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 load_font(&mut self, font: Cow<'static, [u8]>) { fn load_font(&mut self, font: Cow<'static, [u8]>) {
@ -381,14 +321,17 @@ impl backend::Text for Backend {
#[cfg(feature = "image")] #[cfg(feature = "image")]
impl backend::Image for Backend { 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) self.image_pipeline.dimensions(handle)
} }
} }
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
impl backend::Svg for Backend { 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) self.image_pipeline.viewport_dimensions(handle)
} }
} }

View file

@ -10,7 +10,7 @@ pub use text::Text;
use crate::core; use crate::core;
use crate::core::alignment; 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;
use crate::graphics::color; use crate::graphics::color;
use crate::graphics::Viewport; use crate::graphics::Viewport;
@ -56,14 +56,14 @@ impl<'a> Layer<'a> {
Layer::new(Rectangle::with_size(viewport.logical_size())); Layer::new(Rectangle::with_size(viewport.logical_size()));
for (i, line) in lines.iter().enumerate() { for (i, line) in lines.iter().enumerate() {
let text = Text { let text = text::Cached {
content: line.as_ref(), content: line.as_ref(),
bounds: Rectangle::new( bounds: Rectangle::new(
Point::new(11.0, 11.0 + 25.0 * i as f32), Point::new(11.0, 11.0 + 25.0 * i as f32),
Size::INFINITY, Size::INFINITY,
), ),
color: Color::new(0.9, 0.9, 0.9, 1.0), color: Color::new(0.9, 0.9, 0.9, 1.0),
size: 20.0, size: Pixels(20.0),
line_height: core::text::LineHeight::default(), line_height: core::text::LineHeight::default(),
font: Font::MONOSPACE, font: Font::MONOSPACE,
horizontal_alignment: alignment::Horizontal::Left, horizontal_alignment: alignment::Horizontal::Left,
@ -71,13 +71,13 @@ impl<'a> Layer<'a> {
shaping: core::text::Shaping::Basic, 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), bounds: text.bounds + Vector::new(-1.0, -1.0),
color: Color::BLACK, color: Color::BLACK,
..text ..text
}); }));
} }
overlay overlay
@ -113,6 +113,19 @@ impl<'a> Layer<'a> {
current_layer: usize, current_layer: usize,
) { ) {
match primitive { 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 { Primitive::Text {
content, content,
bounds, bounds,
@ -126,7 +139,7 @@ impl<'a> Layer<'a> {
} => { } => {
let layer = &mut layers[current_layer]; let layer = &mut layers[current_layer];
layer.text.push(Text { layer.text.push(Text::Cached(text::Cached {
content, content,
bounds: *bounds + translation, bounds: *bounds + translation,
size: *size, size: *size,
@ -136,7 +149,7 @@ impl<'a> Layer<'a> {
horizontal_alignment: *horizontal_alignment, horizontal_alignment: *horizontal_alignment,
vertical_alignment: *vertical_alignment, vertical_alignment: *vertical_alignment,
shaping: *shaping, shaping: *shaping,
}); }));
} }
Primitive::Quad { Primitive::Quad {
bounds, bounds,

View file

@ -1,10 +1,21 @@
use crate::core::alignment; use crate::core::alignment;
use crate::core::text; 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. /// A paragraph of text.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub struct Text<'a> { 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`]. /// The content of the [`Text`].
pub content: &'a str, pub content: &'a str,
@ -15,7 +26,7 @@ pub struct Text<'a> {
pub color: Color, pub color: Color,
/// The size of the [`Text`] in logical pixels. /// The size of the [`Text`] in logical pixels.
pub size: f32, pub size: Pixels,
/// The line height of the [`Text`]. /// The line height of the [`Text`].
pub line_height: text::LineHeight, pub line_height: text::LineHeight,

View file

@ -23,7 +23,7 @@
#![forbid(rust_2018_idioms)] #![forbid(rust_2018_idioms)]
#![deny( #![deny(
missing_debug_implementations, missing_debug_implementations,
missing_docs, //missing_docs,
unsafe_code, unsafe_code,
unused_results, unused_results,
clippy::extra_unused_lifetimes, clippy::extra_unused_lifetimes,

View file

@ -1,5 +1,5 @@
//! Configure a renderer. //! Configure a renderer.
use crate::core::Font; use crate::core::{Font, Pixels};
use crate::graphics::Antialiasing; use crate::graphics::Antialiasing;
/// The settings of a [`Backend`]. /// The settings of a [`Backend`].
@ -21,7 +21,7 @@ pub struct Settings {
/// The default size of text. /// The default size of text.
/// ///
/// By default, it will be set to `16.0`. /// 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. /// The antialiasing strategy that will be used for triangle primitives.
/// ///
@ -59,7 +59,7 @@ impl Default for Settings {
present_mode: wgpu::PresentMode::AutoVsync, present_mode: wgpu::PresentMode::AutoVsync,
internal_backend: wgpu::Backends::all(), internal_backend: wgpu::Backends::all(),
default_font: Font::default(), default_font: Font::default(),
default_text_size: 16.0, default_text_size: Pixels(16.0),
antialiasing: None, antialiasing: None,
} }
} }

View file

@ -1,20 +1,16 @@
use crate::core::alignment; use crate::core::alignment;
use crate::core::font::{self, Font}; use crate::core::{Rectangle, Size};
use crate::core::text::{Hit, LineHeight, Shaping};
use crate::core::{Pixels, Point, Rectangle, Size};
use crate::graphics::color; use crate::graphics::color;
use crate::graphics::text::cache::{self, Cache};
use crate::graphics::text::{FontSystem, Paragraph};
use crate::layer::Text; use crate::layer::Text;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::hash_map;
use std::hash::{BuildHasher, Hash, Hasher};
use std::sync::Arc;
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Pipeline { pub struct Pipeline {
font_system: RefCell<glyphon::FontSystem>, font_system: FontSystem,
renderers: Vec<glyphon::TextRenderer>, renderers: Vec<glyphon::TextRenderer>,
atlas: glyphon::TextAtlas, atlas: glyphon::TextAtlas,
prepare_layer: usize, prepare_layer: usize,
@ -28,12 +24,7 @@ impl Pipeline {
format: wgpu::TextureFormat, format: wgpu::TextureFormat,
) -> Self { ) -> Self {
Pipeline { Pipeline {
font_system: RefCell::new(glyphon::FontSystem::new_with_fonts( font_system: FontSystem::new(),
[glyphon::fontdb::Source::Binary(Arc::new(
include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
))]
.into_iter(),
)),
renderers: Vec::new(), renderers: Vec::new(),
atlas: glyphon::TextAtlas::with_color_mode( atlas: glyphon::TextAtlas::with_color_mode(
device, 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]>) { pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
let _ = self.font_system.get_mut().db_mut().load_font_source( self.font_system.load_font(bytes);
glyphon::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
);
self.cache = RefCell::new(Cache::new()); self.cache = RefCell::new(Cache::new());
} }
@ -63,7 +56,7 @@ impl Pipeline {
device: &wgpu::Device, device: &wgpu::Device,
queue: &wgpu::Queue, queue: &wgpu::Queue,
sections: &[Text<'_>], sections: &[Text<'_>],
bounds: Rectangle, layer_bounds: Rectangle,
scale_factor: f32, scale_factor: f32,
target_size: Size<u32>, target_size: Size<u32>,
) { ) {
@ -80,97 +73,139 @@ impl Pipeline {
let renderer = &mut self.renderers[self.prepare_layer]; let renderer = &mut self.renderers[self.prepare_layer];
let cache = self.cache.get_mut(); let cache = self.cache.get_mut();
if self.prepare_layer == 0 { enum Allocation {
cache.trim(Purpose::Drawing); Paragraph(Paragraph),
Cache(cache::KeyHash),
} }
let keys: Vec<_> = sections let allocations: Vec<_> = sections
.iter() .iter()
.map(|section| { .map(|section| match section {
let (key, _) = cache.allocate( Text::Managed { paragraph, .. } => {
font_system, paragraph.upgrade().map(Allocation::Paragraph)
Key { }
content: section.content, Text::Cached(text) => {
size: section.size, let (key, _) = cache.allocate(
line_height: f32::from( font_system,
section cache::Key {
.line_height content: text.content,
.to_absolute(Pixels(section.size)), size: text.size.into(),
), line_height: f32::from(
font: section.font, text.line_height.to_absolute(text.size),
bounds: Size { ),
width: section.bounds.width, font: text.font,
height: section.bounds.height, bounds: Size {
width: text.bounds.width,
height: text.bounds.height,
},
shaping: text.shaping,
}, },
shaping: section.shaping, );
},
Purpose::Drawing,
);
key Some(Allocation::Cache(key))
}
}) })
.collect(); .collect();
let bounds = bounds * scale_factor; let layer_bounds = layer_bounds * scale_factor;
let text_areas = let text_areas = sections.iter().zip(allocations.iter()).filter_map(
sections |(section, allocation)| {
.iter() let (
.zip(keys.iter()) buffer,
.filter_map(|(section, key)| { bounds,
let entry = cache.get(key).expect("Get cached buffer"); horizontal_alignment,
vertical_alignment,
color,
) = match section {
Text::Managed {
position, color, ..
} => {
use crate::core::text::Paragraph as _;
let x = section.bounds.x * scale_factor; let Some(Allocation::Paragraph(paragraph)) = allocation
let y = section.bounds.y * scale_factor; else {
return None;
};
let max_width = entry.bounds.width * scale_factor; (
let total_height = entry.bounds.height * scale_factor; 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 left = match section.horizontal_alignment { let entry = cache.get(key).expect("Get cached buffer");
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, &entry.buffer,
alignment::Vertical::Center => y - total_height / 2.0, Rectangle::new(
alignment::Vertical::Bottom => y - total_height, text.bounds.position(),
}; entry.min_bounds,
),
text.horizontal_alignment,
text.vertical_alignment,
text.color,
)
}
};
let section_bounds = Rectangle { let bounds = bounds * scale_factor;
x: left,
y: top,
width: section.bounds.width * scale_factor,
height: section.bounds.height * scale_factor,
};
let clip_bounds = bounds.intersection(&section_bounds)?; 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,
};
Some(glyphon::TextArea { let top = match vertical_alignment {
buffer: &entry.buffer, alignment::Vertical::Top => bounds.y,
left, alignment::Vertical::Center => {
top, bounds.y - bounds.height / 2.0
scale: scale_factor, }
bounds: glyphon::TextBounds { alignment::Vertical::Bottom => bounds.y - bounds.height,
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( let section_bounds = Rectangle {
(r * 255.0) as u8, x: left,
(g * 255.0) as u8, y: top,
(b * 255.0) as u8, ..bounds
(a * 255.0) as u8, };
)
}, let clip_bounds = layer_bounds.intersection(&section_bounds)?;
})
}); Some(glyphon::TextArea {
buffer,
left,
top,
scale: scale_factor,
bounds: glyphon::TextBounds {
left: clip_bounds.x as i32,
top: clip_bounds.y as i32,
right: (clip_bounds.x + clip_bounds.width) as i32,
bottom: (clip_bounds.y + clip_bounds.height) as i32,
},
default_color: {
let [r, g, b, a] = color::pack(color).components();
glyphon::Color::rgba(
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
(a * 255.0) as u8,
)
},
})
},
);
let result = renderer.prepare( let result = renderer.prepare(
device, device,
@ -219,290 +254,8 @@ impl Pipeline {
pub fn end_frame(&mut self) { pub fn end_frame(&mut self) {
self.atlas.trim(); self.atlas.trim();
self.cache.get_mut().trim();
self.prepare_layer = 0; 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;

View file

@ -216,7 +216,14 @@ impl<Theme> graphics::Compositor for Compositor<Theme> {
) -> Result<(Self, Self::Renderer), Error> { ) -> Result<(Self, Self::Renderer), Error> {
let (compositor, backend) = new(settings, compatible_window)?; 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>( fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(

View file

@ -159,19 +159,17 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout( layout(limits, self.width, self.height, self.padding, |limits| {
renderer, self.content.as_widget().layout(
limits, &mut tree.children[0],
self.width, renderer,
self.height, limits,
self.padding, )
|renderer, limits| { })
self.content.as_widget().layout(renderer, limits)
},
)
} }
fn operate( fn operate(
@ -426,17 +424,16 @@ where
} }
/// Computes the layout of a [`Button`]. /// Computes the layout of a [`Button`].
pub fn layout<Renderer>( pub fn layout(
renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
width: Length, width: Length,
height: Length, height: Length,
padding: Padding, padding: Padding,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node { ) -> layout::Node {
let limits = limits.width(width).height(height); 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 padding = padding.fit(content.size(), limits.max());
let size = limits.pad(padding).resolve(content.size()).pad(padding); let size = limits.pad(padding).resolve(content.size()).pad(padding);

View file

@ -129,6 +129,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -6,12 +6,11 @@ use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text; use crate::core::text;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::Tree; use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget,
Widget,
}; };
use crate::{Row, Text};
pub use iced_style::checkbox::{Appearance, StyleSheet}; pub use iced_style::checkbox::{Appearance, StyleSheet};
@ -45,7 +44,7 @@ where
width: Length, width: Length,
size: f32, size: f32,
spacing: f32, spacing: f32,
text_size: Option<f32>, text_size: Option<Pixels>,
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
@ -62,7 +61,7 @@ where
const DEFAULT_SIZE: f32 = 20.0; const DEFAULT_SIZE: f32 = 20.0;
/// The default spacing of a [`Checkbox`]. /// The default spacing of a [`Checkbox`].
const DEFAULT_SPACING: f32 = 15.0; const DEFAULT_SPACING: f32 = 10.0;
/// Creates a new [`Checkbox`]. /// Creates a new [`Checkbox`].
/// ///
@ -118,7 +117,7 @@ where
/// Sets the text size of the [`Checkbox`]. /// Sets the text size of the [`Checkbox`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { 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 self
} }
@ -167,6 +166,14 @@ where
Renderer: text::Renderer, Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet, 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 { fn width(&self) -> Length {
self.width self.width
} }
@ -177,26 +184,35 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
Row::<(), Renderer>::new() layout::next_to_each_other(
.width(self.width) &limits.width(self.width),
.spacing(self.spacing) self.spacing,
.align_items(Alignment::Center) |_| layout::Node::new(Size::new(self.size, self.size)),
.push(Row::new().width(self.size).height(self.size)) |limits| {
.push( let state = tree
Text::new(&self.label) .state
.font(self.font.unwrap_or_else(|| renderer.default_font())) .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
.width(self.width)
.size( widget::text::layout(
self.text_size state,
.unwrap_or_else(|| renderer.default_size()), renderer,
) limits,
.line_height(self.text_line_height) self.width,
.shaping(self.text_shaping), Length::Shrink,
) &self.label,
.layout(renderer, limits) self.text_line_height,
self.text_size,
self.font,
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
)
},
)
} }
fn on_event( fn on_event(
@ -244,7 +260,7 @@ where
fn draw( fn draw(
&self, &self,
_tree: &Tree, tree: &Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
theme: &Renderer::Theme, theme: &Renderer::Theme,
style: &renderer::Style, style: &renderer::Style,
@ -283,24 +299,23 @@ where
line_height, line_height,
shaping, shaping,
} = &self.icon; } = &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 { if self.is_checked {
renderer.fill_text(text::Text { renderer.fill_text(
content: &code_point.to_string(), text::Text {
font: *font, content: &code_point.to_string(),
size, font: *font,
line_height: *line_height, size,
bounds: Rectangle { line_height: *line_height,
x: bounds.center_x(), bounds: bounds.size(),
y: bounds.center_y(), horizontal_alignment: alignment::Horizontal::Center,
..bounds vertical_alignment: alignment::Vertical::Center,
shaping: *shaping,
}, },
color: custom_style.icon_color, bounds.center(),
horizontal_alignment: alignment::Horizontal::Center, custom_style.icon_color,
vertical_alignment: alignment::Vertical::Center, );
shaping: *shaping,
});
} }
} }
@ -311,16 +326,10 @@ where
renderer, renderer,
style, style,
label_layout, label_layout,
&self.label, tree.state.downcast_ref(),
self.text_size,
self.text_line_height,
self.font,
crate::text::Appearance { crate::text::Appearance {
color: custom_style.text_color, 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. /// The unicode code point that will be used as the icon.
pub code_point: char, pub code_point: char,
/// Font size of the content. /// Font size of the content.
pub size: Option<f32>, pub size: Option<Pixels>,
/// The line height of the icon. /// The line height of the icon.
pub line_height: text::LineHeight, pub line_height: text::LineHeight,
/// The shaping strategy of the icon. /// The shaping strategy of the icon.

View file

@ -122,6 +122,7 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
@ -138,6 +139,7 @@ where
self.spacing, self.spacing,
self.align_items, self.align_items,
&self.children, &self.children,
&mut tree.children,
) )
} }

View file

@ -146,11 +146,6 @@ where
self 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`]. /// Sets the text sixe of the [`ComboBox`].
pub fn size(mut self, size: f32) -> Self { pub fn size(mut self, size: f32) -> Self {
self.text_input = self.text_input.size(size); self.text_input = self.text_input.size(size);
@ -181,7 +176,6 @@ pub struct State<T>(RefCell<Inner<T>>);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Inner<T> { struct Inner<T> {
text_input: text_input::State,
value: String, value: String,
options: Vec<T>, options: Vec<T>,
option_matchers: Vec<String>, option_matchers: Vec<String>,
@ -218,7 +212,6 @@ where
); );
Self(RefCell::new(Inner { Self(RefCell::new(Inner {
text_input: text_input::State::new(),
value, value,
options, options,
option_matchers, 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 { fn value(&self) -> String {
let inner = self.0.borrow(); let inner = self.0.borrow();
inner.value.clone() 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 { fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
let inner = self.0.borrow(); 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> impl<T> Filtered<T>
where where
T: Clone, T: Clone,
@ -368,10 +307,11 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut widget::Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.text_input.layout(renderer, limits) self.text_input.layout(tree, renderer, limits)
} }
fn tag(&self) -> widget::tree::Tag { 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( fn on_event(
&mut self, &mut self,
tree: &mut widget::Tree, tree: &mut widget::Tree,
@ -400,7 +344,13 @@ where
) -> event::Status { ) -> event::Status {
let menu = tree.state.downcast_mut::<Menu<T>>(); 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, // This is intended to check whether or not the message buffer was empty,
// since `Shell` does not expose such functionality. // since `Shell` does not expose such functionality.
let mut published_message_to_shell = false; let mut published_message_to_shell = false;
@ -410,9 +360,8 @@ where
let mut local_shell = Shell::new(&mut local_messages); let mut local_shell = Shell::new(&mut local_messages);
// Provide it to the widget // Provide it to the widget
let mut tree = self.state.text_input_tree();
let mut event_status = self.text_input.on_event( let mut event_status = self.text_input.on_event(
&mut tree, &mut tree.children[0],
event.clone(), event.clone(),
layout, layout,
cursor, cursor,
@ -421,7 +370,6 @@ where
&mut local_shell, &mut local_shell,
viewport, viewport,
); );
self.state.update_text_input(tree);
// Then finally react to them here // Then finally react to them here
for message in local_messages { for message in local_messages {
@ -452,7 +400,15 @@ where
shell.invalidate_layout(); 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| { self.state.with_inner(|state| {
if !started_focused { if !started_focused {
if let Some(on_option_hovered) = &mut self.on_option_hovered if let Some(on_option_hovered) = &mut self.on_option_hovered
@ -591,9 +547,8 @@ where
published_message_to_shell = true; published_message_to_shell = true;
// Unfocus the input // Unfocus the input
let mut tree = state.text_input_tree();
let _ = self.text_input.on_event( let _ = self.text_input.on_event(
&mut tree, &mut tree.children[0],
Event::Mouse(mouse::Event::ButtonPressed( Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left, mouse::Button::Left,
)), )),
@ -604,21 +559,25 @@ where
&mut Shell::new(&mut vec![]), &mut Shell::new(&mut vec![]),
viewport, viewport,
); );
state.update_text_input(tree);
} }
}); });
if started_focused let is_focused = {
&& !self.state.is_focused() let text_input_state = tree.children[0]
&& !published_message_to_shell .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() { if let Some(message) = self.on_close.take() {
shell.publish(message); shell.publish(message);
} }
} }
// Focus changed, invalidate widget tree to force a fresh `view` // 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(); shell.invalidate_widgets();
} }
@ -627,20 +586,24 @@ where
fn mouse_interaction( fn mouse_interaction(
&self, &self,
_tree: &widget::Tree, tree: &widget::Tree,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
viewport: &Rectangle, viewport: &Rectangle,
renderer: &Renderer, renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
let tree = self.state.text_input_tree(); self.text_input.mouse_interaction(
self.text_input &tree.children[0],
.mouse_interaction(&tree, layout, cursor, viewport, renderer) layout,
cursor,
viewport,
renderer,
)
} }
fn draw( fn draw(
&self, &self,
_tree: &widget::Tree, tree: &widget::Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
theme: &Renderer::Theme, theme: &Renderer::Theme,
_style: &renderer::Style, _style: &renderer::Style,
@ -648,16 +611,28 @@ where
cursor: mouse::Cursor, cursor: mouse::Cursor,
_viewport: &Rectangle, _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 None
} else { } else {
Some(&self.selection) Some(&self.selection)
}; };
let tree = self.state.text_input_tree(); self.text_input.draw(
self.text_input &tree.children[0],
.draw(&tree, renderer, theme, layout, cursor, selection); renderer,
theme,
layout,
cursor,
selection,
);
} }
fn overlay<'b>( fn overlay<'b>(
@ -666,14 +641,22 @@ where
layout: Layout<'_>, layout: Layout<'_>,
_renderer: &Renderer, _renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> { ) -> Option<overlay::Element<'b, Message, Renderer>> {
let Menu { let is_focused = {
menu, let text_input_state = tree.children[0]
filtered_options, .state
hovered_option, .downcast_ref::<text_input::State<Renderer::Paragraph>>();
..
} = tree.state.downcast_mut::<Menu<T>>(); 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(); let bounds = layout.bounds();
self.state.sync_filtered_options(filtered_options); self.state.sync_filtered_options(filtered_options);

View file

@ -5,7 +5,8 @@ use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::overlay; use crate::core::overlay;
use crate::core::renderer; use crate::core::renderer;
use crate::core::widget::{self, Operation, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Operation};
use crate::core::{ use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
Point, Rectangle, Shell, Size, Vector, Widget, Point, Rectangle, Shell, Size, Vector, Widget,
@ -135,12 +136,20 @@ where
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet, 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> { fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)] self.content.as_widget().children()
} }
fn diff(&self, tree: &mut Tree) { 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 { fn width(&self) -> Length {
@ -153,11 +162,11 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout( layout(
renderer,
limits, limits,
self.width, self.width,
self.height, self.height,
@ -166,9 +175,7 @@ where
self.padding, self.padding,
self.horizontal_alignment, self.horizontal_alignment,
self.vertical_alignment, self.vertical_alignment,
|renderer, limits| { |limits| self.content.as_widget().layout(tree, renderer, limits),
self.content.as_widget().layout(renderer, limits)
},
) )
} }
@ -184,7 +191,7 @@ where
layout.bounds(), layout.bounds(),
&mut |operation| { &mut |operation| {
self.content.as_widget().operate( self.content.as_widget().operate(
&mut tree.children[0], tree,
layout.children().next().unwrap(), layout.children().next().unwrap(),
renderer, renderer,
operation, operation,
@ -205,7 +212,7 @@ where
viewport: &Rectangle, viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
self.content.as_widget_mut().on_event( self.content.as_widget_mut().on_event(
&mut tree.children[0], tree,
event, event,
layout.children().next().unwrap(), layout.children().next().unwrap(),
cursor, cursor,
@ -225,7 +232,7 @@ where
renderer: &Renderer, renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
self.content.as_widget().mouse_interaction( self.content.as_widget().mouse_interaction(
&tree.children[0], tree,
layout.children().next().unwrap(), layout.children().next().unwrap(),
cursor, cursor,
viewport, viewport,
@ -248,7 +255,7 @@ where
draw_background(renderer, &style, layout.bounds()); draw_background(renderer, &style, layout.bounds());
self.content.as_widget().draw( self.content.as_widget().draw(
&tree.children[0], tree,
renderer, renderer,
theme, theme,
&renderer::Style { &renderer::Style {
@ -269,7 +276,7 @@ where
renderer: &Renderer, renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> { ) -> Option<overlay::Element<'b, Message, Renderer>> {
self.content.as_widget_mut().overlay( self.content.as_widget_mut().overlay(
&mut tree.children[0], tree,
layout.children().next().unwrap(), layout.children().next().unwrap(),
renderer, renderer,
) )
@ -291,8 +298,7 @@ where
} }
/// Computes the layout of a [`Container`]. /// Computes the layout of a [`Container`].
pub fn layout<Renderer>( pub fn layout(
renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
width: Length, width: Length,
height: Length, height: Length,
@ -301,7 +307,7 @@ pub fn layout<Renderer>(
padding: Padding, padding: Padding,
horizontal_alignment: alignment::Horizontal, horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical, vertical_alignment: alignment::Vertical,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node { ) -> layout::Node {
let limits = limits let limits = limits
.loose() .loose()
@ -310,7 +316,7 @@ pub fn layout<Renderer>(
.width(width) .width(width)
.height(height); .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 padding = padding.fit(content.size(), limits.max());
let size = limits.pad(padding).resolve(content.size()); let size = limits.pad(padding).resolve(content.size());

View file

@ -6,6 +6,7 @@ use crate::container::{self, Container};
use crate::core; use crate::core;
use crate::core::widget::operation; use crate::core::widget::operation;
use crate::core::{Element, Length, Pixels}; use crate::core::{Element, Length, Pixels};
use crate::keyed;
use crate::overlay; use crate::overlay;
use crate::pick_list::{self, PickList}; use crate::pick_list::{self, PickList};
use crate::progress_bar::{self, ProgressBar}; use crate::progress_bar::{self, ProgressBar};
@ -63,14 +64,22 @@ where
} }
/// Creates a new [`Column`] with the given children. /// Creates a new [`Column`] with the given children.
///
/// [`Column`]: crate::Column
pub fn column<Message, Renderer>( pub fn column<Message, Renderer>(
children: Vec<Element<'_, Message, Renderer>>, children: Vec<Element<'_, Message, Renderer>>,
) -> Column<'_, Message, Renderer> { ) -> Column<'_, Message, Renderer> {
Column::with_children(children) 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. /// Creates a new [`Row`] with the given children.
/// ///
/// [`Row`]: crate::Row /// [`Row`]: crate::Row

View file

@ -167,6 +167,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -105,6 +105,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

53
widget/src/keyed.rs Normal file
View file

@ -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)),+])
);
}

320
widget/src/keyed/column.rs Normal file
View file

@ -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)
}
}

View file

@ -152,11 +152,14 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.with_element(|element| { 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, Renderer: core::Renderer,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,

View file

@ -254,11 +254,18 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| { 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, S: 'static + Default,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,

View file

@ -60,13 +60,13 @@ impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where where
Renderer: core::Renderer, Renderer: core::Renderer,
{ {
fn layout(&mut self, renderer: &Renderer) { fn layout(&mut self, tree: &mut Tree, renderer: &Renderer) {
if self.layout.is_none() { if self.layout.is_none() {
self.layout = self.layout = Some(self.element.as_widget().layout(
Some(self.element.as_widget().layout( tree,
renderer, renderer,
&layout::Limits::new(Size::ZERO, self.size), &layout::Limits::new(Size::ZERO, self.size),
)); ));
} }
} }
@ -104,7 +104,7 @@ where
R: Deref<Target = Renderer>, R: Deref<Target = Renderer>,
{ {
self.update(tree, layout.bounds().size(), view); self.update(tree, layout.bounds().size(), view);
self.layout(renderer.deref()); self.layout(tree, renderer.deref());
let content_layout = Layout::with_offset( let content_layout = Layout::with_offset(
layout.position() - Point::ORIGIN, layout.position() - Point::ORIGIN,
@ -144,6 +144,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
@ -285,7 +286,7 @@ where
overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>, overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>,
tree| { tree| {
content.update(tree, layout.bounds().size(), &self.view); content.update(tree, layout.bounds().size(), &self.view);
content.layout(renderer); content.layout(tree, renderer);
let Content { let Content {
element, element,
@ -362,7 +363,7 @@ where
Renderer: core::Renderer, Renderer: core::Renderer,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,

View file

@ -30,6 +30,7 @@ pub mod button;
pub mod checkbox; pub mod checkbox;
pub mod combo_box; pub mod combo_box;
pub mod container; pub mod container;
pub mod keyed;
pub mod overlay; pub mod overlay;
pub mod pane_grid; pub mod pane_grid;
pub mod pick_list; pub mod pick_list;

View file

@ -120,10 +120,11 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.content.as_widget().layout(renderer, limits) self.content.as_widget().layout(tree, renderer, limits)
} }
fn operate( fn operate(

View file

@ -31,7 +31,7 @@ where
on_option_hovered: Option<&'a dyn Fn(T) -> Message>, on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
width: f32, width: f32,
padding: Padding, padding: Padding,
text_size: Option<f32>, text_size: Option<Pixels>,
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
@ -85,7 +85,7 @@ where
/// Sets the text size of the [`Menu`]. /// Sets the text size of the [`Menu`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { 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 self
} }
@ -232,7 +232,7 @@ where
Renderer::Theme: StyleSheet + container::StyleSheet, Renderer::Theme: StyleSheet + container::StyleSheet,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,
@ -253,7 +253,7 @@ where
) )
.width(self.width); .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 { node.move_to(if space_below > space_above {
position + Vector::new(0.0, self.target_height) position + Vector::new(0.0, self.target_height)
@ -328,7 +328,7 @@ where
on_selected: Box<dyn FnMut(T) -> Message + 'a>, on_selected: Box<dyn FnMut(T) -> Message + 'a>,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>, on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
padding: Padding, padding: Padding,
text_size: Option<f32>, text_size: Option<Pixels>,
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
@ -352,6 +352,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
@ -361,8 +362,7 @@ where
let text_size = let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size()); self.text_size.unwrap_or_else(|| renderer.default_size());
let text_line_height = let text_line_height = self.text_line_height.to_absolute(text_size);
self.text_line_height.to_absolute(Pixels(text_size));
let size = { let size = {
let intrinsic = Size::new( let intrinsic = Size::new(
@ -407,9 +407,9 @@ where
.text_size .text_size
.unwrap_or_else(|| renderer.default_size()); .unwrap_or_else(|| renderer.default_size());
let option_height = f32::from( let option_height =
self.text_line_height.to_absolute(Pixels(text_size)), f32::from(self.text_line_height.to_absolute(text_size))
) + self.padding.vertical(); + self.padding.vertical();
let new_hovered_option = let new_hovered_option =
(cursor_position.y / option_height) as usize; (cursor_position.y / option_height) as usize;
@ -436,9 +436,9 @@ where
.text_size .text_size
.unwrap_or_else(|| renderer.default_size()); .unwrap_or_else(|| renderer.default_size());
let option_height = f32::from( let option_height =
self.text_line_height.to_absolute(Pixels(text_size)), f32::from(self.text_line_height.to_absolute(text_size))
) + self.padding.vertical(); + self.padding.vertical();
*self.hovered_option = *self.hovered_option =
Some((cursor_position.y / option_height) as usize); Some((cursor_position.y / option_height) as usize);
@ -490,7 +490,7 @@ where
let text_size = let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size()); self.text_size.unwrap_or_else(|| renderer.default_size());
let option_height = 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(); + self.padding.vertical();
let offset = viewport.y - bounds.y; let offset = viewport.y - bounds.y;
@ -526,26 +526,24 @@ where
); );
} }
renderer.fill_text(Text { renderer.fill_text(
content: &option.to_string(), Text {
bounds: Rectangle { content: &option.to_string(),
x: bounds.x + self.padding.left, bounds: Size::new(f32::INFINITY, bounds.height),
y: bounds.center_y(), size: text_size,
width: f32::INFINITY, line_height: self.text_line_height,
..bounds 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, Point::new(bounds.x + self.padding.left, bounds.center_y()),
line_height: self.text_line_height, if is_selected {
font: self.font.unwrap_or_else(|| renderer.default_font()),
color: if is_selected {
appearance.selected_text_color appearance.selected_text_color
} else { } else {
appearance.text_color appearance.text_color
}, },
horizontal_alignment: alignment::Horizontal::Left, );
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
});
} }
} }
} }

View file

@ -275,10 +275,12 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout( layout(
tree,
renderer, renderer,
limits, limits,
self.contents.layout(), self.contents.layout(),
@ -286,7 +288,9 @@ where
self.height, self.height,
self.spacing, self.spacing,
self.contents.iter(), 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`]. /// Calculates the [`Layout`] of a [`PaneGrid`].
pub fn layout<Renderer, T>( pub fn layout<Renderer, T>(
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
node: &Node, node: &Node,
@ -478,19 +483,26 @@ pub fn layout<Renderer, T>(
height: Length, height: Length,
spacing: f32, spacing: f32,
contents: impl Iterator<Item = (Pane, T)>, 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 { ) -> layout::Node {
let limits = limits.width(width).height(height); let limits = limits.width(width).height(height);
let size = limits.resolve(Size::ZERO); let size = limits.resolve(Size::ZERO);
let regions = node.pane_regions(spacing, size); let regions = node.pane_regions(spacing, size);
let children = contents let children = contents
.filter_map(|(pane, content)| { .zip(tree.children.iter_mut())
.filter_map(|((pane, content), tree)| {
let region = regions.get(&pane)?; let region = regions.get(&pane)?;
let size = Size::new(region.width, region.height); let size = Size::new(region.width, region.height);
let mut node = layout_content( let mut node = layout_content(
content, content,
tree,
renderer, renderer,
&layout::Limits::new(size, size), &layout::Limits::new(size, size),
); );

View file

@ -150,18 +150,23 @@ where
pub(crate) fn layout( pub(crate) fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
if let Some(title_bar) = &self.title_bar { if let Some(title_bar) = &self.title_bar {
let max_size = limits.max(); let max_size = limits.max();
let title_bar_layout = title_bar let title_bar_layout = title_bar.layout(
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); &mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
let title_bar_size = title_bar_layout.size(); let title_bar_size = title_bar_layout.size();
let mut body_layout = self.body.as_widget().layout( let mut body_layout = self.body.as_widget().layout(
&mut tree.children[0],
renderer, renderer,
&layout::Limits::new( &layout::Limits::new(
Size::ZERO, Size::ZERO,
@ -179,7 +184,11 @@ where
vec![title_bar_layout, body_layout], vec![title_bar_layout, body_layout],
) )
} else { } else {
self.body.as_widget().layout(renderer, limits) self.body.as_widget().layout(
&mut tree.children[0],
renderer,
limits,
)
} }
} }

View file

@ -213,23 +213,27 @@ where
pub(crate) fn layout( pub(crate) fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
let limits = limits.pad(self.padding); let limits = limits.pad(self.padding);
let max_size = limits.max(); let max_size = limits.max();
let title_layout = self let title_layout = self.content.as_widget().layout(
.content &mut tree.children[0],
.as_widget() renderer,
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); &layout::Limits::new(Size::ZERO, max_size),
);
let title_size = title_layout.size(); let title_size = title_layout.size();
let mut node = if let Some(controls) = &self.controls { let mut node = if let Some(controls) = &self.controls {
let mut controls_layout = controls let mut controls_layout = controls.as_widget().layout(
.as_widget() &mut tree.children[1],
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
let controls_size = controls_layout.size(); let controls_size = controls_layout.size();
let space_before_controls = max_size.width - controls_size.width; let space_before_controls = max_size.width - controls_size.width;

View file

@ -7,12 +7,12 @@ use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::overlay; use crate::core::overlay;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text::{self, Text}; use crate::core::text::{self, Paragraph as _, Text};
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle,
Size, Widget, Shell, Size, Widget,
}; };
use crate::overlay::menu::{self, Menu}; use crate::overlay::menu::{self, Menu};
use crate::scrollable; use crate::scrollable;
@ -35,7 +35,7 @@ where
selected: Option<T>, selected: Option<T>,
width: Length, width: Length,
padding: Padding, padding: Padding,
text_size: Option<f32>, text_size: Option<Pixels>,
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
@ -101,7 +101,7 @@ where
/// Sets the text size of the [`PickList`]. /// Sets the text size of the [`PickList`].
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self { 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 self
} }
@ -157,11 +157,11 @@ where
From<<Renderer::Theme as StyleSheet>::Style>, From<<Renderer::Theme as StyleSheet>::Style>,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>() tree::Tag::of::<State<Renderer::Paragraph>>()
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State::new()) tree::State::new(State::<Renderer::Paragraph>::new())
} }
fn width(&self) -> Length { fn width(&self) -> Length {
@ -174,10 +174,12 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout( layout(
tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
renderer, renderer,
limits, limits,
self.width, self.width,
@ -210,7 +212,7 @@ where
self.on_selected.as_ref(), self.on_selected.as_ref(),
self.selected.as_ref(), self.selected.as_ref(),
&self.options, &self.options,
|| tree.state.downcast_mut::<State>(), || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
) )
} }
@ -250,7 +252,7 @@ where
self.selected.as_ref(), self.selected.as_ref(),
&self.handle, &self.handle,
&self.style, &self.style,
|| tree.state.downcast_ref::<State>(), || tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
) )
} }
@ -260,7 +262,7 @@ where
layout: Layout<'_>, layout: Layout<'_>,
renderer: &Renderer, renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> { ) -> Option<overlay::Element<'b, Message, Renderer>> {
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
overlay( overlay(
layout, layout,
@ -295,28 +297,32 @@ where
} }
} }
/// The local state of a [`PickList`]. /// The state of a [`PickList`].
#[derive(Debug)] #[derive(Debug)]
pub struct State { pub struct State<P: text::Paragraph> {
menu: menu::State, menu: menu::State,
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
is_open: bool, is_open: bool,
hovered_option: Option<usize>, hovered_option: Option<usize>,
options: Vec<P>,
placeholder: P,
} }
impl State { impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`] for a [`PickList`]. /// Creates a new [`State`] for a [`PickList`].
pub fn new() -> Self { fn new() -> Self {
Self { Self {
menu: menu::State::default(), menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(), keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(), is_open: bool::default(),
hovered_option: Option::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 { fn default() -> Self {
Self::new() Self::new()
} }
@ -330,7 +336,7 @@ pub enum Handle<Font> {
/// This is the default. /// This is the default.
Arrow { Arrow {
/// Font size of the content. /// Font size of the content.
size: Option<f32>, size: Option<Pixels>,
}, },
/// A custom static handle. /// A custom static handle.
Static(Icon<Font>), Static(Icon<Font>),
@ -359,7 +365,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon. /// The unicode code point that will be used as the icon.
pub code_point: char, pub code_point: char,
/// Font size of the content. /// Font size of the content.
pub size: Option<f32>, pub size: Option<Pixels>,
/// Line height of the content. /// Line height of the content.
pub line_height: text::LineHeight, pub line_height: text::LineHeight,
/// The shaping strategy of the icon. /// The shaping strategy of the icon.
@ -368,11 +374,12 @@ pub struct Icon<Font> {
/// Computes the layout of a [`PickList`]. /// Computes the layout of a [`PickList`].
pub fn layout<Renderer, T>( pub fn layout<Renderer, T>(
state: &mut State<Renderer::Paragraph>,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
width: Length, width: Length,
padding: Padding, padding: Padding,
text_size: Option<f32>, text_size: Option<Pixels>,
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
@ -386,38 +393,67 @@ where
use std::f32; use std::f32;
let limits = limits.width(width).height(Length::Shrink).pad(padding); 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 text_size = text_size.unwrap_or_else(|| renderer.default_size());
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,
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
let label = option.to_string();
renderer.update_paragraph(
paragraph,
Text {
content: &label,
..option_text
},
);
}
if let Some(placeholder) = placeholder {
renderer.update_paragraph(
&mut state.placeholder,
Text {
content: placeholder,
..option_text
},
);
}
let max_width = match width { let max_width = match width {
Length::Shrink => { Length::Shrink => {
let measure = |label: &str| -> f32 { let labels_width =
let width = renderer.measure_width( state.options.iter().fold(0.0, |width, paragraph| {
label, f32::max(width, paragraph.min_width())
text_size, });
font.unwrap_or_else(|| renderer.default_font()),
text_shaping,
);
width.round() labels_width.max(
}; placeholder
.map(|_| state.placeholder.min_width())
let labels = options.iter().map(ToString::to_string); .unwrap_or(0.0),
)
let labels_width = labels
.map(|label| measure(&label))
.fold(100.0, |candidate, current| current.max(candidate));
let placeholder_width = placeholder.map(measure).unwrap_or(100.0);
labels_width.max(placeholder_width)
} }
_ => 0.0, _ => 0.0,
}; };
let size = { let size = {
let intrinsic = Size::new( let intrinsic = Size::new(
max_width + text_size + padding.left, max_width + text_size.0 + padding.left,
f32::from(text_line_height.to_absolute(Pixels(text_size))), f32::from(text_line_height.to_absolute(text_size)),
); );
limits.resolve(intrinsic).pad(padding) limits.resolve(intrinsic).pad(padding)
@ -428,7 +464,7 @@ where
/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] /// Processes an [`Event`] and updates the [`State`] of a [`PickList`]
/// accordingly. /// accordingly.
pub fn update<'a, T, Message>( pub fn update<'a, T, P, Message>(
event: Event, event: Event,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
@ -436,10 +472,11 @@ pub fn update<'a, T, Message>(
on_selected: &dyn Fn(T) -> Message, on_selected: &dyn Fn(T) -> Message,
selected: Option<&T>, selected: Option<&T>,
options: &[T], options: &[T],
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State<P>,
) -> event::Status ) -> event::Status
where where
T: PartialEq + Clone + 'a, T: PartialEq + Clone + 'a,
P: text::Paragraph + 'a,
{ {
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
@ -534,9 +571,9 @@ pub fn mouse_interaction(
/// Returns the current overlay of a [`PickList`]. /// Returns the current overlay of a [`PickList`].
pub fn overlay<'a, T, Message, Renderer>( pub fn overlay<'a, T, Message, Renderer>(
layout: Layout<'_>, layout: Layout<'_>,
state: &'a mut State, state: &'a mut State<Renderer::Paragraph>,
padding: Padding, padding: Padding,
text_size: Option<f32>, text_size: Option<Pixels>,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Renderer::Font, font: Renderer::Font,
options: &'a [T], options: &'a [T],
@ -591,7 +628,7 @@ pub fn draw<'a, T, Renderer>(
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
padding: Padding, padding: Padding,
text_size: Option<f32>, text_size: Option<Pixels>,
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Renderer::Font, font: Renderer::Font,
@ -599,7 +636,7 @@ pub fn draw<'a, T, Renderer>(
selected: Option<&T>, selected: Option<&T>,
handle: &Handle<Renderer::Font>, handle: &Handle<Renderer::Font>,
style: &<Renderer::Theme as StyleSheet>::Style, style: &<Renderer::Theme as StyleSheet>::Style,
state: impl FnOnce() -> &'a State, state: impl FnOnce() -> &'a State<Renderer::Paragraph>,
) where ) where
Renderer: text::Renderer, Renderer: text::Renderer,
Renderer::Theme: StyleSheet, Renderer::Theme: StyleSheet,
@ -665,22 +702,26 @@ pub fn draw<'a, T, Renderer>(
if let Some((font, code_point, size, line_height, shaping)) = handle { if let Some((font, code_point, size, line_height, shaping)) = handle {
let size = size.unwrap_or_else(|| renderer.default_size()); let size = size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(Text { renderer.fill_text(
content: &code_point.to_string(), Text {
size, content: &code_point.to_string(),
line_height, size,
font, line_height,
color: style.handle_color, font,
bounds: Rectangle { bounds: Size::new(
x: bounds.x + bounds.width - padding.horizontal(), bounds.width,
y: bounds.center_y(), f32::from(line_height.to_absolute(size)),
height: f32::from(line_height.to_absolute(Pixels(size))), ),
..bounds horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
shaping,
}, },
horizontal_alignment: alignment::Horizontal::Right, Point::new(
vertical_alignment: alignment::Vertical::Center, bounds.x + bounds.width - padding.horizontal(),
shaping, bounds.center_y(),
}); ),
style.handle_color,
);
} }
let label = selected.map(ToString::to_string); 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) { if let Some(label) = label.as_deref().or(placeholder) {
let text_size = text_size.unwrap_or_else(|| renderer.default_size()); let text_size = text_size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(Text { renderer.fill_text(
content: label, Text {
size: text_size, content: label,
line_height: text_line_height, size: text_size,
font, line_height: text_line_height,
color: if is_selected { 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 style.text_color
} else { } else {
style.placeholder_color 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,
});
} }
} }

View file

@ -95,6 +95,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -60,6 +60,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer<Theme>, _renderer: &Renderer<Theme>,
_limits: &layout::Limits, _limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -6,12 +6,12 @@ use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text; use crate::core::text;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::Tree; use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, Shell, Size,
Shell, Widget, Widget,
}; };
use crate::{Row, Text};
pub use iced_style::radio::{Appearance, StyleSheet}; pub use iced_style::radio::{Appearance, StyleSheet};
@ -80,7 +80,7 @@ where
width: Length, width: Length,
size: f32, size: f32,
spacing: f32, spacing: f32,
text_size: Option<f32>, text_size: Option<Pixels>,
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
@ -152,7 +152,7 @@ where
/// Sets the text size of the [`Radio`] button. /// Sets the text size of the [`Radio`] button.
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { 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 self
} }
@ -193,6 +193,14 @@ where
Renderer: text::Renderer, Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet, 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 { fn width(&self) -> Length {
self.width self.width
} }
@ -203,25 +211,35 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
Row::<(), Renderer>::new() layout::next_to_each_other(
.width(self.width) &limits.width(self.width),
.spacing(self.spacing) self.spacing,
.align_items(Alignment::Center) |_| layout::Node::new(Size::new(self.size, self.size)),
.push(Row::new().width(self.size).height(self.size)) |limits| {
.push( let state = tree
Text::new(&self.label) .state
.width(self.width) .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
.size(
self.text_size widget::text::layout(
.unwrap_or_else(|| renderer.default_size()), state,
) renderer,
.line_height(self.text_line_height) limits,
.shaping(self.text_shaping), self.width,
) Length::Shrink,
.layout(renderer, limits) &self.label,
self.text_line_height,
self.text_size,
self.font,
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
)
},
)
} }
fn on_event( fn on_event(
@ -267,7 +285,7 @@ where
fn draw( fn draw(
&self, &self,
_state: &Tree, tree: &Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
theme: &Renderer::Theme, theme: &Renderer::Theme,
style: &renderer::Style, style: &renderer::Style,
@ -327,16 +345,10 @@ where
renderer, renderer,
style, style,
label_layout, label_layout,
&self.label, tree.state.downcast_ref(),
self.text_size,
self.text_line_height,
self.font,
crate::text::Appearance { crate::text::Appearance {
color: custom_style.text_color, color: custom_style.text_color,
}, },
alignment::Horizontal::Left,
alignment::Vertical::Center,
self.text_shaping,
); );
} }
} }

View file

@ -114,6 +114,7 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
@ -127,6 +128,7 @@ where
self.spacing, self.spacing,
self.align_items, self.align_items,
&self.children, &self.children,
&mut tree.children,
) )
} }

View file

@ -72,6 +72,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -230,6 +230,7 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
@ -240,7 +241,11 @@ where
self.height, self.height,
&self.direction, &self.direction,
|renderer, limits| { |renderer, limits| {
self.content.as_widget().layout(renderer, limits) self.content.as_widget().layout(
&mut tree.children[0],
renderer,
limits,
)
}, },
) )
} }

View file

@ -169,6 +169,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -55,6 +55,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -106,6 +106,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -17,7 +17,7 @@ use crate::core::keyboard;
use crate::core::layout; use crate::core::layout;
use crate::core::mouse::{self, click}; use crate::core::mouse::{self, click};
use crate::core::renderer; use crate::core::renderer;
use crate::core::text::{self, Text}; use crate::core::text::{self, Paragraph as _, Text};
use crate::core::time::{Duration, Instant}; use crate::core::time::{Duration, Instant};
use crate::core::touch; use crate::core::touch;
use crate::core::widget; use crate::core::widget;
@ -67,7 +67,7 @@ where
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
width: Length, width: Length,
padding: Padding, padding: Padding,
size: Option<f32>, size: Option<Pixels>,
line_height: text::LineHeight, line_height: text::LineHeight,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: 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`]. /// Sets the text size of the [`TextInput`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self { pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = Some(size.into().0); self.size = Some(size.into());
self self
} }
@ -218,12 +218,8 @@ where
theme, theme,
layout, layout,
cursor, cursor,
tree.state.downcast_ref::<State>(), tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
value.unwrap_or(&self.value), value.unwrap_or(&self.value),
&self.placeholder,
self.size,
self.line_height,
self.font,
self.on_input.is_none(), self.on_input.is_none(),
self.is_secure, self.is_secure,
self.icon.as_ref(), self.icon.as_ref(),
@ -240,15 +236,15 @@ where
Renderer::Theme: StyleSheet, Renderer::Theme: StyleSheet,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>() tree::Tag::of::<State<Renderer::Paragraph>>()
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State::new()) tree::State::new(State::<Renderer::Paragraph>::new())
} }
fn diff(&self, tree: &mut Tree) { 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 // Unfocus text input if it becomes disabled
if self.on_input.is_none() { if self.on_input.is_none() {
@ -269,6 +265,7 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
@ -278,8 +275,13 @@ where
self.width, self.width,
self.padding, self.padding,
self.size, self.size,
self.font,
self.line_height, self.line_height,
self.icon.as_ref(), 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, _renderer: &Renderer,
operation: &mut dyn Operation<Message>, 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.focusable(state, self.id.as_ref().map(|id| &id.0));
operation.text_input(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_input.as_deref(),
self.on_paste.as_deref(), self.on_paste.as_deref(),
&self.on_submit, &self.on_submit,
|| tree.state.downcast_mut::<State>(), || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
) )
} }
@ -341,12 +343,8 @@ where
theme, theme,
layout, layout,
cursor, cursor,
tree.state.downcast_ref::<State>(), tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
&self.value, &self.value,
&self.placeholder,
self.size,
self.line_height,
self.font,
self.on_input.is_none(), self.on_input.is_none(),
self.is_secure, self.is_secure,
self.icon.as_ref(), self.icon.as_ref(),
@ -388,7 +386,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon. /// The unicode code point that will be used as the icon.
pub code_point: char, pub code_point: char,
/// The font size of the content. /// 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`]. /// The spacing between the [`Icon`] and the text in a [`TextInput`].
pub spacing: f32, pub spacing: f32,
/// The side of a [`TextInput`] where to display the [`Icon`]. /// The side of a [`TextInput`] where to display the [`Icon`].
@ -465,29 +463,68 @@ pub fn layout<Renderer>(
limits: &layout::Limits, limits: &layout::Limits,
width: Length, width: Length,
padding: Padding, padding: Padding,
size: Option<f32>, size: Option<Pixels>,
font: Option<Renderer::Font>,
line_height: text::LineHeight, line_height: text::LineHeight,
icon: Option<&Icon<Renderer::Font>>, icon: Option<&Icon<Renderer::Font>>,
state: &mut State<Renderer::Paragraph>,
value: &Value,
placeholder: &str,
is_secure: bool,
) -> layout::Node ) -> layout::Node
where where
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = size.unwrap_or_else(|| renderer.default_size()); let text_size = size.unwrap_or_else(|| renderer.default_size());
let padding = padding.fit(Size::ZERO, limits.max()); let padding = padding.fit(Size::ZERO, limits.max());
let limits = limits let limits = limits
.width(width) .width(width)
.pad(padding) .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 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 { if let Some(icon) = icon {
let icon_width = renderer.measure_width( let icon_text = Text {
&icon.code_point.to_string(), line_height,
icon.size.unwrap_or_else(|| renderer.default_size()), content: &icon.code_point.to_string(),
icon.font, font: icon.font,
text::Shaping::Advanced, 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( let mut text_node = layout::Node::new(
text_bounds - Size::new(icon_width + icon.spacing, 0.0), text_bounds - Size::new(icon_width + icon.spacing, 0.0),
@ -537,19 +574,31 @@ pub fn update<'a, Message, Renderer>(
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
value: &mut Value, value: &mut Value,
size: Option<f32>, size: Option<Pixels>,
line_height: text::LineHeight, line_height: text::LineHeight,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
is_secure: bool, is_secure: bool,
on_input: Option<&dyn Fn(String) -> Message>, on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>, on_submit: &Option<Message>,
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>,
) -> event::Status ) -> event::Status
where where
Message: Clone, Message: Clone,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
let update_cache = |state, value| {
replace_paragraph(
renderer,
state,
layout,
value,
font,
size,
line_height,
)
};
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
@ -592,11 +641,7 @@ where
}; };
find_cursor_position( find_cursor_position(
renderer,
text_layout.bounds(), text_layout.bounds(),
font,
size,
line_height,
&value, &value,
state, state,
target, target,
@ -621,11 +666,7 @@ where
state.cursor.select_all(value); state.cursor.select_all(value);
} else { } else {
let position = find_cursor_position( let position = find_cursor_position(
renderer,
text_layout.bounds(), text_layout.bounds(),
font,
size,
line_height,
value, value,
state, state,
target, target,
@ -671,11 +712,7 @@ where
}; };
let position = find_cursor_position( let position = find_cursor_position(
renderer,
text_layout.bounds(), text_layout.bounds(),
font,
size,
line_height,
&value, &value,
state, state,
target, target,
@ -710,6 +747,8 @@ where
focus.updated_at = Instant::now(); focus.updated_at = Instant::now();
update_cache(state, value);
return event::Status::Captured; return event::Status::Captured;
} }
} }
@ -749,6 +788,8 @@ where
let message = (on_input)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
update_cache(state, value);
} }
keyboard::KeyCode::Delete => { keyboard::KeyCode::Delete => {
if platform::is_jump_modifier_pressed(modifiers) if platform::is_jump_modifier_pressed(modifiers)
@ -769,6 +810,8 @@ where
let message = (on_input)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
update_cache(state, value);
} }
keyboard::KeyCode::Left => { keyboard::KeyCode::Left => {
if platform::is_jump_modifier_pressed(modifiers) if platform::is_jump_modifier_pressed(modifiers)
@ -844,6 +887,8 @@ where
let message = (on_input)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
update_cache(state, value);
} }
keyboard::KeyCode::V => { keyboard::KeyCode::V => {
if state.keyboard_modifiers.command() if state.keyboard_modifiers.command()
@ -876,6 +921,8 @@ where
shell.publish(message); shell.publish(message);
state.is_pasting = Some(content); state.is_pasting = Some(content);
update_cache(state, value);
} else { } else {
state.is_pasting = None; state.is_pasting = None;
} }
@ -979,12 +1026,8 @@ pub fn draw<Renderer>(
theme: &Renderer::Theme, theme: &Renderer::Theme,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
state: &State, state: &State<Renderer::Paragraph>,
value: &Value, value: &Value,
placeholder: &str,
size: Option<f32>,
line_height: text::LineHeight,
font: Option<Renderer::Font>,
is_disabled: bool, is_disabled: bool,
is_secure: bool, is_secure: bool,
icon: Option<&Icon<Renderer::Font>>, icon: Option<&Icon<Renderer::Font>>,
@ -1023,28 +1066,17 @@ pub fn draw<Renderer>(
appearance.background, appearance.background,
); );
if let Some(icon) = icon { if icon.is_some() {
let icon_layout = children_layout.next().unwrap(); let icon_layout = children_layout.next().unwrap();
renderer.fill_text(Text { renderer.fill_paragraph(
content: &icon.code_point.to_string(), &state.icon,
size: icon.size.unwrap_or_else(|| renderer.default_size()), icon_layout.bounds().center(),
line_height: text::LineHeight::default(), appearance.icon_color,
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,
});
} }
let text = value.to_string(); 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 let (cursor, offset) = if let Some(focus) = state
.is_focused .is_focused
@ -1055,12 +1087,9 @@ pub fn draw<Renderer>(
cursor::State::Index(position) => { cursor::State::Index(position) => {
let (text_value_width, offset) = let (text_value_width, offset) =
measure_cursor_and_scroll_offset( measure_cursor_and_scroll_offset(
renderer, &state.value,
text_bounds, text_bounds,
value,
size,
position, position,
font,
); );
let is_cursor_visible = ((focus.now - focus.updated_at) let is_cursor_visible = ((focus.now - focus.updated_at)
@ -1096,22 +1125,16 @@ pub fn draw<Renderer>(
let (left_position, left_offset) = let (left_position, left_offset) =
measure_cursor_and_scroll_offset( measure_cursor_and_scroll_offset(
renderer, &state.value,
text_bounds, text_bounds,
value,
size,
left, left,
font,
); );
let (right_position, right_offset) = let (right_position, right_offset) =
measure_cursor_and_scroll_offset( measure_cursor_and_scroll_offset(
renderer, &state.value,
text_bounds, text_bounds,
value,
size,
right, right,
font,
); );
let width = right_position - left_position; let width = right_position - left_position;
@ -1143,12 +1166,7 @@ pub fn draw<Renderer>(
(None, 0.0) (None, 0.0)
}; };
let text_width = renderer.measure_width( let text_width = state.value.min_width();
if text.is_empty() { placeholder } else { &text },
size,
font,
text::Shaping::Advanced,
);
let render = |renderer: &mut Renderer| { let render = |renderer: &mut Renderer| {
if let Some((cursor, color)) = cursor { if let Some((cursor, color)) = cursor {
@ -1157,27 +1175,21 @@ pub fn draw<Renderer>(
renderer.with_translation(Vector::ZERO, |_| {}); renderer.with_translation(Vector::ZERO, |_| {});
} }
renderer.fill_text(Text { renderer.fill_paragraph(
content: if text.is_empty() { placeholder } else { &text }, if text.is_empty() {
color: 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) theme.placeholder_color(style)
} else if is_disabled { } else if is_disabled {
theme.disabled_color(style) theme.disabled_color(style)
} else { } else {
theme.value_color(style) 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 { if text_width > text_bounds.width {
@ -1208,7 +1220,10 @@ pub fn mouse_interaction(
/// The state of a [`TextInput`]. /// The state of a [`TextInput`].
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct State { pub struct State<P: text::Paragraph> {
value: P,
placeholder: P,
icon: P,
is_focused: Option<Focus>, is_focused: Option<Focus>,
is_dragging: bool, is_dragging: bool,
is_pasting: Option<Value>, is_pasting: Option<Value>,
@ -1225,7 +1240,7 @@ struct Focus {
is_window_focused: bool, is_window_focused: bool,
} }
impl State { impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`], representing an unfocused [`TextInput`]. /// Creates a new [`State`], representing an unfocused [`TextInput`].
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
@ -1234,6 +1249,9 @@ impl State {
/// Creates a new [`State`], representing a focused [`TextInput`]. /// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused() -> Self { pub fn focused() -> Self {
Self { Self {
value: P::default(),
placeholder: P::default(),
icon: P::default(),
is_focused: None, is_focused: None,
is_dragging: false, is_dragging: false,
is_pasting: None, 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 { fn is_focused(&self) -> bool {
State::is_focused(self) 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) { fn move_cursor_to_front(&mut self) {
State::move_cursor_to_front(self) State::move_cursor_to_front(self)
} }
@ -1336,17 +1354,11 @@ mod platform {
} }
} }
fn offset<Renderer>( fn offset<P: text::Paragraph>(
renderer: &Renderer,
text_bounds: Rectangle, text_bounds: Rectangle,
font: Renderer::Font,
size: f32,
value: &Value, value: &Value,
state: &State, state: &State<P>,
) -> f32 ) -> f32 {
where
Renderer: text::Renderer,
{
if state.is_focused() { if state.is_focused() {
let cursor = state.cursor(); let cursor = state.cursor();
@ -1356,12 +1368,9 @@ where
}; };
let (_, offset) = measure_cursor_and_scroll_offset( let (_, offset) = measure_cursor_and_scroll_offset(
renderer, &state.value,
text_bounds, text_bounds,
value,
size,
focus_position, focus_position,
font,
); );
offset offset
@ -1370,63 +1379,34 @@ where
} }
} }
fn measure_cursor_and_scroll_offset<Renderer>( fn measure_cursor_and_scroll_offset(
renderer: &Renderer, paragraph: &impl text::Paragraph,
text_bounds: Rectangle, text_bounds: Rectangle,
value: &Value,
size: f32,
cursor_index: usize, cursor_index: usize,
font: Renderer::Font, ) -> (f32, f32) {
) -> (f32, f32) let grapheme_position = paragraph
where .grapheme_position(0, cursor_index)
Renderer: text::Renderer, .unwrap_or(Point::ORIGIN);
{
let text_before_cursor = value.until(cursor_index).to_string();
let text_value_width = renderer.measure_width( let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
&text_before_cursor,
size,
font,
text::Shaping::Advanced,
);
let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); (grapheme_position.x, offset)
(text_value_width, offset)
} }
/// Computes the position of the text cursor at the given X coordinate of /// Computes the position of the text cursor at the given X coordinate of
/// a [`TextInput`]. /// a [`TextInput`].
fn find_cursor_position<Renderer>( fn find_cursor_position<P: text::Paragraph>(
renderer: &Renderer,
text_bounds: Rectangle, text_bounds: Rectangle,
font: Option<Renderer::Font>,
size: Option<f32>,
line_height: text::LineHeight,
value: &Value, value: &Value,
state: &State, state: &State<P>,
x: f32, x: f32,
) -> Option<usize> ) -> Option<usize> {
where let offset = offset(text_bounds, value, state);
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);
let value = value.to_string(); let value = value.to_string();
let char_offset = renderer let char_offset = state
.hit_test( .value
&value, .hit_test(Point::new(x + offset, text_bounds.height / 2.0))
size,
line_height,
font,
Size::INFINITY,
text::Shaping::Advanced,
Point::new(x + offset, text_bounds.height / 2.0),
true,
)
.map(text::Hit::cursor)?; .map(text::Hit::cursor)?;
Some( 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; const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;

View file

@ -6,12 +6,12 @@ use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text; use crate::core::text;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::Tree; use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size,
Shell, Widget, Widget,
}; };
use crate::{Row, Text};
pub use crate::style::toggler::{Appearance, StyleSheet}; pub use crate::style::toggler::{Appearance, StyleSheet};
@ -42,7 +42,7 @@ where
label: Option<String>, label: Option<String>,
width: Length, width: Length,
size: f32, size: f32,
text_size: Option<f32>, text_size: Option<Pixels>,
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_alignment: alignment::Horizontal, text_alignment: alignment::Horizontal,
text_shaping: text::Shaping, text_shaping: text::Shaping,
@ -85,7 +85,7 @@ where
text_line_height: text::LineHeight::default(), text_line_height: text::LineHeight::default(),
text_alignment: alignment::Horizontal::Left, text_alignment: alignment::Horizontal::Left,
text_shaping: text::Shaping::Basic, text_shaping: text::Shaping::Basic,
spacing: 0.0, spacing: Self::DEFAULT_SIZE / 2.0,
font: None, font: None,
style: Default::default(), style: Default::default(),
} }
@ -105,7 +105,7 @@ where
/// Sets the text size o the [`Toggler`]. /// Sets the text size o the [`Toggler`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { 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 self
} }
@ -160,6 +160,14 @@ where
Renderer: text::Renderer, Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet, 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 { fn width(&self) -> Length {
self.width self.width
} }
@ -170,32 +178,41 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
let mut row = Row::<(), Renderer>::new() let limits = limits.width(self.width);
.width(self.width)
.spacing(self.spacing)
.align_items(Alignment::Center);
if let Some(label) = &self.label { layout::next_to_each_other(
row = row.push( &limits,
Text::new(label) self.spacing,
.horizontal_alignment(self.text_alignment) |_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
.font(self.font.unwrap_or_else(|| renderer.default_font())) |limits| {
.width(self.width) if let Some(label) = self.label.as_deref() {
.size( let state = tree
self.text_size .state
.unwrap_or_else(|| renderer.default_size()), .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) } else {
.shaping(self.text_shaping), layout::Node::new(Size::ZERO)
); }
} },
)
row = row.push(Row::new().width(2.0 * self.size).height(self.size));
row.layout(renderer, limits)
} }
fn on_event( fn on_event(
@ -243,7 +260,7 @@ where
fn draw( fn draw(
&self, &self,
_state: &Tree, tree: &Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
theme: &Renderer::Theme, theme: &Renderer::Theme,
style: &renderer::Style, style: &renderer::Style,
@ -259,28 +276,21 @@ where
const SPACE_RATIO: f32 = 0.05; const SPACE_RATIO: f32 = 0.05;
let mut children = layout.children(); 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(); let label_layout = children.next().unwrap();
crate::text::draw( crate::text::draw(
renderer, renderer,
style, style,
label_layout, label_layout,
label, tree.state.downcast_ref(),
self.text_size,
self.text_line_height,
self.font,
Default::default(), Default::default(),
self.text_alignment,
alignment::Vertical::Center,
self.text_shaping,
); );
} }
let toggler_layout = children.next().unwrap();
let bounds = toggler_layout.bounds(); let bounds = toggler_layout.bounds();
let is_mouse_over = cursor.is_over(layout.bounds()); let is_mouse_over = cursor.is_over(layout.bounds());
let style = if is_mouse_over { let style = if is_mouse_over {

View file

@ -107,11 +107,14 @@ where
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet, Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{ {
fn children(&self) -> Vec<widget::Tree> { 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) { 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 { fn state(&self) -> widget::tree::State {
@ -132,10 +135,11 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut widget::Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.content.as_widget().layout(renderer, limits) self.content.as_widget().layout(tree, renderer, limits)
} }
fn on_event( fn on_event(
@ -214,8 +218,10 @@ where
) -> Option<overlay::Element<'b, Message, Renderer>> { ) -> Option<overlay::Element<'b, Message, Renderer>> {
let state = tree.state.downcast_ref::<State>(); let state = tree.state.downcast_ref::<State>();
let mut children = tree.children.iter_mut();
let content = self.content.as_widget_mut().overlay( let content = self.content.as_widget_mut().overlay(
&mut tree.children[0], children.next().unwrap(),
layout, layout,
renderer, renderer,
); );
@ -225,6 +231,7 @@ where
layout.position(), layout.position(),
Box::new(Overlay { Box::new(Overlay {
tooltip: &self.tooltip, tooltip: &self.tooltip,
state: children.next().unwrap(),
cursor_position, cursor_position,
content_bounds: layout.bounds(), content_bounds: layout.bounds(),
snap_within_viewport: self.snap_within_viewport, snap_within_viewport: self.snap_within_viewport,
@ -295,6 +302,7 @@ where
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
{ {
tooltip: &'b Text<'a, Renderer>, tooltip: &'b Text<'a, Renderer>,
state: &'b mut widget::Tree,
cursor_position: Point, cursor_position: Point,
content_bounds: Rectangle, content_bounds: Rectangle,
snap_within_viewport: bool, snap_within_viewport: bool,
@ -311,7 +319,7 @@ where
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,
@ -320,6 +328,7 @@ where
let text_layout = Widget::<(), Renderer>::layout( let text_layout = Widget::<(), Renderer>::layout(
self.tooltip, self.tooltip,
self.state,
renderer, renderer,
&layout::Limits::new( &layout::Limits::new(
Size::ZERO, Size::ZERO,

View file

@ -166,6 +166,7 @@ where
fn layout( fn layout(
&self, &self,
_tree: &mut Tree,
_renderer: &Renderer, _renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {