Implement explicit text caching in the widget state tree

This commit is contained in:
Héctor Ramón Jiménez 2023-08-30 04:31:21 +02:00
parent c9bd48704d
commit ed3454301e
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
79 changed files with 1910 additions and 1705 deletions

View file

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

View file

@ -7,7 +7,7 @@ pub mod flex;
pub use limits::Limits;
pub use node::Node;
use crate::{Point, Rectangle, Vector};
use crate::{Point, Rectangle, Size, Vector};
/// The bounds of a [`Node`] and its children, using absolute coordinates.
#[derive(Debug, Clone, Copy)]
@ -63,3 +63,29 @@ impl<'a> Layout<'a> {
})
}
}
/// Produces a [`Node`] with two children nodes one right next to each other.
pub fn next_to_each_other(
limits: &Limits,
spacing: f32,
left: impl FnOnce(&Limits) -> Node,
right: impl FnOnce(&Limits) -> Node,
) -> Node {
let left_node = left(limits);
let left_size = left_node.size();
let right_limits = limits.shrink(Size::new(left_size.width + spacing, 0.0));
let mut right_node = right(&right_limits);
let right_size = right_node.size();
right_node.move_to(Point::new(left_size.width + spacing, 0.0));
Node::with_children(
Size::new(
left_size.width + spacing + right_size.width,
left_size.height.max(right_size.height),
),
vec![left_node, right_node],
)
}

View file

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

View file

@ -5,26 +5,13 @@ mod null;
#[cfg(debug_assertions)]
pub use null::Null;
use crate::layout;
use crate::{Background, BorderRadius, Color, Element, Rectangle, Vector};
use crate::{Background, BorderRadius, Color, Rectangle, Vector};
/// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer: Sized {
/// The supported theme of the [`Renderer`].
type Theme;
/// Lays out the elements of a user interface.
///
/// You should override this if you need to perform any operations before or
/// after layouting. For instance, trimming the measurements cache.
fn layout<Message>(
&mut self,
element: &Element<'_, Message, Self>,
limits: &layout::Limits,
) -> layout::Node {
element.as_widget().layout(self, limits)
}
/// Draws the primitives recorded in the given closure in a new layer.
///
/// The layer will clip its contents to the provided `bounds`.

View file

@ -1,6 +1,7 @@
use crate::alignment;
use crate::renderer::{self, Renderer};
use crate::text::{self, Text};
use crate::{Background, Font, Point, Rectangle, Size, Vector};
use crate::{Background, Color, Font, Pixels, Point, Rectangle, Size, Vector};
use std::borrow::Cow;
@ -41,6 +42,7 @@ impl Renderer for Null {
impl text::Renderer for Null {
type Font = Font;
type Paragraph = ();
const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0';
@ -50,37 +52,83 @@ impl text::Renderer for Null {
Font::default()
}
fn default_size(&self) -> f32 {
16.0
fn default_size(&self) -> Pixels {
Pixels(16.0)
}
fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
fn measure(
&self,
_content: &str,
_size: f32,
_line_height: text::LineHeight,
_font: Font,
_bounds: Size,
_shaping: text::Shaping,
) -> Size {
Size::new(0.0, 20.0)
fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph {
}
fn hit_test(
fn resize_paragraph(
&self,
_contents: &str,
_size: f32,
_line_height: text::LineHeight,
_font: Self::Font,
_bounds: Size,
_shaping: text::Shaping,
_point: Point,
_nearest_only: bool,
) -> Option<text::Hit> {
_paragraph: &mut Self::Paragraph,
_new_bounds: Size,
) {
}
fn fill_paragraph(
&mut self,
_paragraph: &Self::Paragraph,
_position: Point,
_color: Color,
) {
}
fn fill_text(
&mut self,
_paragraph: Text<'_, Self::Font>,
_position: Point,
_color: Color,
) {
}
}
impl text::Paragraph for () {
type Font = Font;
fn content(&self) -> &str {
""
}
fn text_size(&self) -> Pixels {
Pixels(16.0)
}
fn font(&self) -> Self::Font {
Font::default()
}
fn line_height(&self) -> text::LineHeight {
text::LineHeight::default()
}
fn shaping(&self) -> text::Shaping {
text::Shaping::default()
}
fn horizontal_alignment(&self) -> alignment::Horizontal {
alignment::Horizontal::Left
}
fn vertical_alignment(&self) -> alignment::Vertical {
alignment::Vertical::Top
}
fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> {
None
}
fn fill_text(&mut self, _text: Text<'_, Self::Font>) {}
fn bounds(&self) -> Size {
Size::ZERO
}
fn min_bounds(&self) -> Size {
Size::ZERO
}
fn hit_test(&self, _point: Point) -> Option<text::Hit> {
None
}
}

View file

@ -1,6 +1,6 @@
//! Draw and interact with text.
use crate::alignment;
use crate::{Color, Pixels, Point, Rectangle, Size};
use crate::{Color, Pixels, Point, Size};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
@ -12,17 +12,14 @@ pub struct Text<'a, Font> {
pub content: &'a str,
/// The bounds of the paragraph.
pub bounds: Rectangle,
pub bounds: Size,
/// The size of the [`Text`] in logical pixels.
pub size: f32,
pub size: Pixels,
/// The line height of the [`Text`].
pub line_height: LineHeight,
/// The color of the [`Text`].
pub color: Color,
/// The font of the [`Text`].
pub font: Font,
@ -132,7 +129,10 @@ impl Hit {
/// A renderer capable of measuring and drawing [`Text`].
pub trait Renderer: crate::Renderer {
/// The font type used.
type Font: Copy;
type Font: Copy + PartialEq;
/// The [`Paragraph`] of this [`Renderer`].
type Paragraph: Paragraph<Font = Self::Font> + 'static;
/// The icon font of the backend.
const ICON_FONT: Self::Font;
@ -151,62 +151,107 @@ pub trait Renderer: crate::Renderer {
fn default_font(&self) -> Self::Font;
/// Returns the default size of [`Text`].
fn default_size(&self) -> f32;
/// 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>;
fn default_size(&self) -> Pixels;
/// Loads a [`Self::Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
/// Draws the given [`Text`].
fn fill_text(&mut self, text: Text<'_, Self::Font>);
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph;
/// Lays out the given [`Paragraph`] with some new boundaries.
fn resize_paragraph(
&self,
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>,
) {
if paragraph.content() != text.content
|| paragraph.text_size() != text.size
|| paragraph.line_height().to_absolute(text.size)
!= text.line_height.to_absolute(text.size)
|| paragraph.font() != text.font
|| paragraph.shaping() != text.shaping
|| paragraph.horizontal_alignment() != text.horizontal_alignment
|| paragraph.vertical_alignment() != text.vertical_alignment
{
*paragraph = self.create_paragraph(text);
} else if paragraph.bounds() != text.bounds {
self.resize_paragraph(paragraph, text.bounds);
}
}
/// 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 [`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
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -285,10 +285,13 @@ mod modal {
fn layout(
&self,
tree: &widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.base.as_widget().layout(renderer, limits)
self.base
.as_widget()
.layout(&tree.children[0], renderer, limits)
}
fn on_event(
@ -408,7 +411,11 @@ mod modal {
.width(Length::Fill)
.height(Length::Fill);
let mut child = self.content.as_widget().layout(renderer, &limits);
let mut child = self
.content
.as_widget()
.layout(self.tree, renderer, &limits);
child.align(Alignment::Center, Alignment::Center, limits.max());
let mut node = layout::Node::with_children(self.size, vec![child]);

View file

@ -326,10 +326,13 @@ mod toast {
fn layout(
&self,
tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content.as_widget().layout(renderer, limits)
self.content
.as_widget()
.layout(&tree.children[0], renderer, limits)
}
fn tag(&self) -> widget::tree::Tag {
@ -517,6 +520,7 @@ mod toast {
10.0,
Alignment::End,
self.toasts,
self.state,
)
.translate(Vector::new(position.x, position.y))
}

View file

@ -5,7 +5,7 @@ use iced::widget::{
scrollable, slider, text, text_input, toggler, vertical_space,
};
use iced::widget::{Button, Column, Container, Slider};
use iced::{Color, Element, Font, Length, Renderer, Sandbox, Settings};
use iced::{Color, Element, Font, Length, Pixels, Renderer, Sandbox, Settings};
pub fn main() -> iced::Result {
env_logger::init();
@ -571,7 +571,7 @@ impl<'a> Step {
text_input = text_input.icon(text_input::Icon {
font: Font::default(),
code_point: '🚀',
size: Some(28.0),
size: Some(Pixels(28.0)),
spacing: 10.0,
side: text_input::Side::Right,
});

View file

@ -23,6 +23,9 @@ log = "0.4"
raw-window-handle = "0.5"
thiserror = "1.0"
bitflags = "1.2"
cosmic-text = "0.9"
rustc-hash = "1.1"
unicode-segmentation = "1.6"
[dependencies.bytemuck]
version = "1.4"
@ -32,6 +35,14 @@ features = ["derive"]
version = "0.10"
path = "../core"
[dependencies.twox-hash]
version = "1.6"
default-features = false
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
version = "1.6.1"
features = ["std"]
[dependencies.image]
version = "0.24"
optional = true

View file

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

View file

@ -40,6 +40,32 @@ impl<T: Damage> Damage for Primitive<T> {
bounds.expand(1.5)
}
Self::Paragraph {
paragraph,
position,
..
} => {
let mut bounds =
Rectangle::new(*position, paragraph.min_bounds);
bounds.x = match paragraph.horizontal_alignment {
alignment::Horizontal::Left => bounds.x,
alignment::Horizontal::Center => {
bounds.x - bounds.width / 2.0
}
alignment::Horizontal::Right => bounds.x - bounds.width,
};
bounds.y = match paragraph.vertical_alignment {
alignment::Vertical::Top => bounds.y,
alignment::Vertical::Center => {
bounds.y - bounds.height / 2.0
}
alignment::Vertical::Bottom => bounds.y - bounds.height,
};
bounds.expand(1.5)
}
Self::Quad { bounds, .. }
| Self::Image { bounds, .. }
| Self::Svg { bounds, .. } => bounds.expand(1.0),

View file

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

View file

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

View file

@ -3,7 +3,8 @@ use crate::core::alignment;
use crate::core::image;
use crate::core::svg;
use crate::core::text;
use crate::core::{Background, Color, Font, Rectangle, Vector};
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
use crate::text::paragraph;
use std::sync::Arc;
@ -19,7 +20,7 @@ pub enum Primitive<T> {
/// The color of the text
color: Color,
/// The size of the text in logical pixels
size: f32,
size: Pixels,
/// The line height of the text
line_height: text::LineHeight,
/// The font of the text
@ -31,6 +32,15 @@ pub enum Primitive<T> {
/// The shaping strategy of the text.
shaping: text::Shaping,
},
/// A paragraph primitive
Paragraph {
/// The [`Paragraph`].
paragraph: paragraph::Weak,
/// The position of the [`Paragraph`].
position: Point,
/// The color of the [`Paragraph`].
color: Color,
},
/// A quad primitive
Quad {
/// The bounds of the quad

View file

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

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

@ -0,0 +1,113 @@
pub mod cache;
pub mod paragraph;
pub use cache::Cache;
pub use paragraph::Paragraph;
pub use cosmic_text;
use crate::core::font::{self, Font};
use crate::core::text::Shaping;
use crate::core::Size;
use std::sync::{self, Arc, RwLock};
#[allow(missing_debug_implementations)]
pub struct FontSystem(RwLock<cosmic_text::FontSystem>);
impl FontSystem {
pub fn new() -> Self {
FontSystem(RwLock::new(cosmic_text::FontSystem::new_with_fonts(
[cosmic_text::fontdb::Source::Binary(Arc::new(
include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
))]
.into_iter(),
)))
}
pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem {
self.0.get_mut().expect("Lock font system")
}
pub fn write(&self) -> sync::RwLockWriteGuard<'_, cosmic_text::FontSystem> {
self.0.write().expect("Write font system")
}
}
impl Default for FontSystem {
fn default() -> Self {
Self::new()
}
}
pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
let (width, total_lines) = buffer
.layout_runs()
.fold((0.0, 0usize), |(width, total_lines), run| {
(run.line_w.max(width), total_lines + 1)
});
Size::new(width, total_lines as f32 * buffer.metrics().line_height)
}
pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> {
cosmic_text::Attrs::new()
.family(to_family(font.family))
.weight(to_weight(font.weight))
.stretch(to_stretch(font.stretch))
.style(to_style(font.style))
}
fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
match family {
font::Family::Name(name) => cosmic_text::Family::Name(name),
font::Family::SansSerif => cosmic_text::Family::SansSerif,
font::Family::Serif => cosmic_text::Family::Serif,
font::Family::Cursive => cosmic_text::Family::Cursive,
font::Family::Fantasy => cosmic_text::Family::Fantasy,
font::Family::Monospace => cosmic_text::Family::Monospace,
}
}
fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
match weight {
font::Weight::Thin => cosmic_text::Weight::THIN,
font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
font::Weight::Light => cosmic_text::Weight::LIGHT,
font::Weight::Normal => cosmic_text::Weight::NORMAL,
font::Weight::Medium => cosmic_text::Weight::MEDIUM,
font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
font::Weight::Bold => cosmic_text::Weight::BOLD,
font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
font::Weight::Black => cosmic_text::Weight::BLACK,
}
}
fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
match stretch {
font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
font::Stretch::Normal => cosmic_text::Stretch::Normal,
font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
}
}
fn to_style(style: font::Style) -> cosmic_text::Style {
match style {
font::Style::Normal => cosmic_text::Style::Normal,
font::Style::Italic => cosmic_text::Style::Italic,
font::Style::Oblique => cosmic_text::Style::Oblique,
}
}
pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
match shaping {
Shaping::Basic => cosmic_text::Shaping::Basic,
Shaping::Advanced => cosmic_text::Shaping::Advanced,
}
}

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

@ -0,0 +1,120 @@
use crate::core::{Font, Size};
use crate::text;
use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::hash_map;
use std::hash::{BuildHasher, Hash, Hasher};
#[allow(missing_debug_implementations)]
#[derive(Default)]
pub struct Cache {
entries: FxHashMap<KeyHash, cosmic_text::Buffer>,
aliases: FxHashMap<KeyHash, KeyHash>,
recently_used: FxHashSet<KeyHash>,
hasher: HashBuilder,
}
#[cfg(not(target_arch = "wasm32"))]
type HashBuilder = twox_hash::RandomXxHashBuilder64;
#[cfg(target_arch = "wasm32")]
type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
impl Cache {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, key: &KeyHash) -> Option<&cosmic_text::Buffer> {
self.entries.get(key)
}
pub fn allocate(
&mut self,
font_system: &mut cosmic_text::FontSystem,
key: Key<'_>,
) -> (KeyHash, &mut cosmic_text::Buffer) {
let hash = key.hash(self.hasher.build_hasher());
if let Some(hash) = self.aliases.get(&hash) {
let _ = self.recently_used.insert(*hash);
return (*hash, self.entries.get_mut(hash).unwrap());
}
if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
let metrics = cosmic_text::Metrics::new(key.size, key.line_height);
let mut buffer = cosmic_text::Buffer::new(font_system, metrics);
buffer.set_size(
font_system,
key.bounds.width,
key.bounds.height.max(key.line_height),
);
buffer.set_text(
font_system,
key.content,
text::to_attributes(key.font),
text::to_shaping(key.shaping),
);
let bounds = text::measure(&buffer);
let _ = entry.insert(buffer);
for bounds in [
bounds,
Size {
width: key.bounds.width,
..bounds
},
] {
if key.bounds != bounds {
let _ = self.aliases.insert(
Key { bounds, ..key }.hash(self.hasher.build_hasher()),
hash,
);
}
}
}
let _ = self.recently_used.insert(hash);
(hash, self.entries.get_mut(&hash).unwrap())
}
pub fn trim(&mut self) {
self.entries
.retain(|key, _| self.recently_used.contains(key));
self.aliases
.retain(|_, value| self.recently_used.contains(value));
self.recently_used.clear();
}
}
#[derive(Debug, Clone, Copy)]
pub struct Key<'a> {
pub content: &'a str,
pub size: f32,
pub line_height: f32,
pub font: Font,
pub bounds: Size,
pub shaping: text::Shaping,
}
impl Key<'_> {
fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
self.content.hash(&mut hasher);
self.size.to_bits().hash(&mut hasher);
self.line_height.to_bits().hash(&mut hasher);
self.font.hash(&mut hasher);
self.bounds.width.to_bits().hash(&mut hasher);
self.bounds.height.to_bits().hash(&mut hasher);
self.shaping.hash(&mut hasher);
hasher.finish()
}
}
pub type KeyHash = u64;

View file

@ -0,0 +1,246 @@
use crate::core;
use crate::core::alignment;
use crate::core::text::{Hit, LineHeight, Shaping, Text};
use crate::core::{Font, Pixels, Point, Size};
use crate::text::{self, FontSystem};
use std::fmt;
use std::sync::{self, Arc};
#[derive(Clone, PartialEq, Default)]
pub struct Paragraph(Arc<Internal>);
struct Internal {
buffer: cosmic_text::Buffer,
content: String, // TODO: Reuse from `buffer` (?)
font: Font,
shaping: Shaping,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
bounds: Size,
min_bounds: Size,
}
impl Paragraph {
pub fn new() -> Self {
Self::default()
}
pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self {
let mut font_system = font_system.write();
let mut buffer = cosmic_text::Buffer::new(
&mut font_system,
cosmic_text::Metrics::new(
text.size.into(),
text.line_height.to_absolute(text.size).into(),
),
);
buffer.set_size(
&mut font_system,
text.bounds.width,
text.bounds.height,
);
buffer.set_text(
&mut font_system,
text.content,
text::to_attributes(text.font),
text::to_shaping(text.shaping),
);
let min_bounds = text::measure(&buffer);
Self(Arc::new(Internal {
buffer,
content: text.content.to_owned(),
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
bounds: text.bounds,
min_bounds,
}))
}
pub fn buffer(&self) -> &cosmic_text::Buffer {
&self.0.buffer
}
pub fn downgrade(&self) -> Weak {
Weak {
raw: Arc::downgrade(&self.0),
min_bounds: self.0.min_bounds,
horizontal_alignment: self.0.horizontal_alignment,
vertical_alignment: self.0.vertical_alignment,
}
}
pub fn resize(&mut self, new_bounds: Size, font_system: &FontSystem) {
if let Some(internal) = Arc::get_mut(&mut self.0) {
// If there is no strong reference holding on to the paragraph, we
// resize the buffer in-place
internal.buffer.set_size(
&mut font_system.write(),
new_bounds.width,
new_bounds.height,
);
internal.bounds = new_bounds;
internal.min_bounds = text::measure(&internal.buffer);
} else {
let metrics = self.0.buffer.metrics();
// If there is a strong reference somewhere, we recompute the buffer
// from scratch
*self = Self::with_text(
Text {
content: &self.0.content,
bounds: self.0.bounds,
size: Pixels(metrics.font_size),
line_height: LineHeight::Absolute(Pixels(
metrics.line_height,
)),
font: self.0.font,
horizontal_alignment: self.0.horizontal_alignment,
vertical_alignment: self.0.vertical_alignment,
shaping: self.0.shaping,
},
font_system,
);
}
}
}
impl core::text::Paragraph for Paragraph {
type Font = Font;
fn content(&self) -> &str {
&self.0.content
}
fn text_size(&self) -> Pixels {
Pixels(self.0.buffer.metrics().font_size)
}
fn line_height(&self) -> LineHeight {
LineHeight::Absolute(Pixels(self.0.buffer.metrics().line_height))
}
fn font(&self) -> Font {
self.0.font
}
fn shaping(&self) -> Shaping {
self.0.shaping
}
fn horizontal_alignment(&self) -> alignment::Horizontal {
self.0.horizontal_alignment
}
fn vertical_alignment(&self) -> alignment::Vertical {
self.0.vertical_alignment
}
fn bounds(&self) -> Size {
self.0.bounds
}
fn min_bounds(&self) -> Size {
self.0.min_bounds
}
fn hit_test(&self, point: Point) -> Option<Hit> {
let cursor = self.0.buffer.hit(point.x, point.y)?;
Some(Hit::CharOffset(cursor.index))
}
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
let run = self.0.buffer.layout_runs().nth(line)?;
// TODO: Index represents a grapheme, not a glyph
let glyph = run.glyphs.get(index).or_else(|| run.glyphs.last())?;
let advance_last = if index == run.glyphs.len() {
glyph.w
} else {
0.0
};
Some(Point::new(
glyph.x + glyph.x_offset * glyph.font_size + advance_last,
glyph.y - glyph.y_offset * glyph.font_size,
))
}
}
impl PartialEq for Internal {
fn eq(&self, other: &Self) -> bool {
self.content == other.content
&& self.font == other.font
&& self.shaping == other.shaping
&& self.horizontal_alignment == other.horizontal_alignment
&& self.vertical_alignment == other.vertical_alignment
&& self.bounds == other.bounds
&& self.min_bounds == other.min_bounds
&& self.buffer.metrics() == other.buffer.metrics()
}
}
impl Default for Internal {
fn default() -> Self {
Self {
buffer: cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
font_size: 1.0,
line_height: 1.0,
}),
content: String::new(),
font: Font::default(),
shaping: Shaping::default(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
bounds: Size::ZERO,
min_bounds: Size::ZERO,
}
}
}
impl fmt::Debug for Paragraph {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Paragraph")
.field("content", &self.0.content)
.field("font", &self.0.font)
.field("shaping", &self.0.shaping)
.field("horizontal_alignment", &self.0.horizontal_alignment)
.field("vertical_alignment", &self.0.vertical_alignment)
.field("bounds", &self.0.bounds)
.field("min_bounds", &self.0.min_bounds)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct Weak {
raw: sync::Weak<Internal>,
pub min_bounds: Size,
pub horizontal_alignment: alignment::Horizontal,
pub vertical_alignment: alignment::Vertical,
}
impl Weak {
pub fn upgrade(&self) -> Option<Paragraph> {
self.raw.upgrade().map(Paragraph)
}
}
impl PartialEq for Weak {
fn eq(&self, other: &Self) -> bool {
match (self.raw.upgrade(), other.raw.upgrade()) {
(Some(p1), Some(p2)) => p1 == p2,
_ => false,
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,12 @@
use crate::core::text;
use crate::core::Gradient;
use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector};
use crate::core::{Background, Color, Gradient, Rectangle, Vector};
use crate::graphics::backend;
use crate::graphics::text;
use crate::graphics::{Damage, Viewport};
use crate::primitive::{self, Primitive};
use crate::Settings;
use std::borrow::Cow;
pub struct Backend {
default_font: Font,
default_text_size: f32,
text_pipeline: crate::text::Pipeline,
#[cfg(feature = "image")]
@ -21,10 +17,8 @@ pub struct Backend {
}
impl Backend {
pub fn new(settings: Settings) -> Self {
pub fn new() -> Self {
Self {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
text_pipeline: crate::text::Pipeline::new(),
#[cfg(feature = "image")]
@ -364,6 +358,32 @@ impl Backend {
}
}
}
Primitive::Paragraph {
paragraph,
position,
color,
} => {
let physical_bounds =
(Rectangle::new(*position, paragraph.min_bounds)
+ translation)
* scale_factor;
if !clip_bounds.intersects(&physical_bounds) {
return;
}
let clip_mask = (!physical_bounds.is_within(&clip_bounds))
.then_some(clip_mask as &_);
self.text_pipeline.draw_paragraph(
paragraph,
*position + translation,
*color,
scale_factor,
pixels,
clip_mask,
);
}
Primitive::Text {
content,
bounds,
@ -599,6 +619,12 @@ impl Backend {
}
}
impl Default for Backend {
fn default() -> Self {
Self::new()
}
}
fn into_color(color: Color) -> tiny_skia::Color {
tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
.expect("Convert color from iced to tiny_skia")
@ -779,58 +805,8 @@ impl iced_graphics::Backend for Backend {
}
impl backend::Text for Backend {
const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = '\u{f00c}';
const ARROW_DOWN_ICON: char = '\u{e800}';
fn default_font(&self) -> Font {
self.default_font
}
fn default_size(&self) -> f32 {
self.default_text_size
}
fn measure(
&self,
contents: &str,
size: f32,
line_height: text::LineHeight,
font: Font,
bounds: Size,
shaping: text::Shaping,
) -> Size {
self.text_pipeline.measure(
contents,
size,
line_height,
font,
bounds,
shaping,
)
}
fn hit_test(
&self,
contents: &str,
size: f32,
line_height: text::LineHeight,
font: Font,
bounds: Size,
shaping: text::Shaping,
point: Point,
nearest_only: bool,
) -> Option<text::Hit> {
self.text_pipeline.hit_test(
contents,
size,
line_height,
font,
bounds,
shaping,
point,
nearest_only,
)
fn font_system(&self) -> &text::FontSystem {
self.text_pipeline.font_system()
}
fn load_font(&mut self, font: Cow<'static, [u8]>) {
@ -840,7 +816,10 @@ impl backend::Text for Backend {
#[cfg(feature = "image")]
impl backend::Image for Backend {
fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
fn dimensions(
&self,
handle: &crate::core::image::Handle,
) -> crate::core::Size<u32> {
self.raster_pipeline.dimensions(handle)
}
}
@ -850,7 +829,7 @@ impl backend::Svg for Backend {
fn viewport_dimensions(
&self,
handle: &crate::core::svg::Handle,
) -> Size<u32> {
) -> crate::core::Size<u32> {
self.vector_pipeline.viewport_dimensions(handle)
}
}

View file

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

View file

@ -1,18 +1,19 @@
use crate::core::alignment;
use crate::core::font::{self, Font};
use crate::core::text::{Hit, LineHeight, Shaping};
use crate::core::{Color, Pixels, Point, Rectangle, Size};
use crate::core::text::{LineHeight, Shaping};
use crate::core::{Color, Font, Pixels, Point, Rectangle};
use crate::graphics::text::cache::{self, Cache};
use crate::graphics::text::paragraph;
use crate::graphics::text::FontSystem;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::hash_map;
use std::hash::{BuildHasher, Hash, Hasher};
use std::sync::Arc;
#[allow(missing_debug_implementations)]
pub struct Pipeline {
font_system: RefCell<cosmic_text::FontSystem>,
font_system: FontSystem,
glyph_cache: GlyphCache,
cache: RefCell<Cache>,
}
@ -20,17 +21,16 @@ pub struct Pipeline {
impl Pipeline {
pub fn new() -> Self {
Pipeline {
font_system: RefCell::new(cosmic_text::FontSystem::new_with_fonts(
[cosmic_text::fontdb::Source::Binary(Arc::new(
include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
))]
.into_iter(),
)),
font_system: FontSystem::new(),
glyph_cache: GlyphCache::new(),
cache: RefCell::new(Cache::new()),
}
}
pub fn font_system(&self) -> &FontSystem {
&self.font_system
}
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
self.font_system.get_mut().db_mut().load_font_source(
cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
@ -39,12 +39,23 @@ impl Pipeline {
self.cache = RefCell::new(Cache::new());
}
pub fn draw_paragraph(
&mut self,
_paragraph: &paragraph::Weak,
_position: Point,
_color: Color,
_scale_factor: f32,
_pixels: &mut tiny_skia::PixmapMut<'_>,
_clip_mask: Option<&tiny_skia::Mask>,
) {
}
pub fn draw(
&mut self,
content: &str,
bounds: Rectangle,
color: Color,
size: f32,
size: Pixels,
line_height: LineHeight,
font: Font,
horizontal_alignment: alignment::Horizontal,
@ -54,22 +65,22 @@ impl Pipeline {
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
) {
let line_height = f32::from(line_height.to_absolute(Pixels(size)));
let line_height = f32::from(line_height.to_absolute(size));
let font_system = self.font_system.get_mut();
let key = Key {
let key = cache::Key {
bounds: bounds.size(),
content,
font,
size,
size: size.into(),
line_height,
shaping,
};
let (_, entry) = self.cache.get_mut().allocate(font_system, key);
let (_, buffer) = self.cache.get_mut().allocate(font_system, key);
let max_width = entry.bounds.width * scale_factor;
let total_height = entry.bounds.height * scale_factor;
let max_width = bounds.width * scale_factor;
let total_height = bounds.height * scale_factor;
let bounds = bounds * scale_factor;
@ -87,7 +98,7 @@ impl Pipeline {
let mut swash = cosmic_text::SwashCache::new();
for run in entry.buffer.layout_runs() {
for run in buffer.layout_runs() {
for glyph in run.glyphs {
let physical_glyph = glyph.physical((x, y), scale_factor);
@ -122,130 +133,6 @@ impl Pipeline {
self.cache.get_mut().trim();
self.glyph_cache.trim();
}
pub fn measure(
&self,
content: &str,
size: f32,
line_height: LineHeight,
font: Font,
bounds: Size,
shaping: Shaping,
) -> Size {
let mut measurement_cache = self.cache.borrow_mut();
let line_height = f32::from(line_height.to_absolute(Pixels(size)));
let (_, entry) = measurement_cache.allocate(
&mut self.font_system.borrow_mut(),
Key {
content,
size,
line_height,
font,
bounds,
shaping,
},
);
entry.bounds
}
pub fn hit_test(
&self,
content: &str,
size: f32,
line_height: LineHeight,
font: Font,
bounds: Size,
shaping: Shaping,
point: Point,
_nearest_only: bool,
) -> Option<Hit> {
let mut measurement_cache = self.cache.borrow_mut();
let line_height = f32::from(line_height.to_absolute(Pixels(size)));
let (_, entry) = measurement_cache.allocate(
&mut self.font_system.borrow_mut(),
Key {
content,
size,
line_height,
font,
bounds,
shaping,
},
);
let cursor = entry.buffer.hit(point.x, point.y)?;
Some(Hit::CharOffset(cursor.index))
}
}
fn measure(buffer: &cosmic_text::Buffer) -> Size {
let (width, total_lines) = buffer
.layout_runs()
.fold((0.0, 0usize), |(width, total_lines), run| {
(run.line_w.max(width), total_lines + 1)
});
Size::new(width, total_lines as f32 * buffer.metrics().line_height)
}
fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
match family {
font::Family::Name(name) => cosmic_text::Family::Name(name),
font::Family::SansSerif => cosmic_text::Family::SansSerif,
font::Family::Serif => cosmic_text::Family::Serif,
font::Family::Cursive => cosmic_text::Family::Cursive,
font::Family::Fantasy => cosmic_text::Family::Fantasy,
font::Family::Monospace => cosmic_text::Family::Monospace,
}
}
fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
match weight {
font::Weight::Thin => cosmic_text::Weight::THIN,
font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
font::Weight::Light => cosmic_text::Weight::LIGHT,
font::Weight::Normal => cosmic_text::Weight::NORMAL,
font::Weight::Medium => cosmic_text::Weight::MEDIUM,
font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
font::Weight::Bold => cosmic_text::Weight::BOLD,
font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
font::Weight::Black => cosmic_text::Weight::BLACK,
}
}
fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
match stretch {
font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
font::Stretch::Normal => cosmic_text::Stretch::Normal,
font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
}
}
fn to_style(style: font::Style) -> cosmic_text::Style {
match style {
font::Style::Normal => cosmic_text::Style::Normal,
font::Style::Italic => cosmic_text::Style::Italic,
font::Style::Oblique => cosmic_text::Style::Oblique,
}
}
fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
match shaping {
Shaping::Basic => cosmic_text::Shaping::Basic,
Shaping::Advanced => cosmic_text::Shaping::Advanced,
}
}
#[derive(Debug, Clone, Default)]
@ -358,135 +245,3 @@ impl GlyphCache {
}
}
}
struct Cache {
entries: FxHashMap<KeyHash, Entry>,
measurements: FxHashMap<KeyHash, KeyHash>,
recently_used: FxHashSet<KeyHash>,
hasher: HashBuilder,
trim_count: usize,
}
struct Entry {
buffer: cosmic_text::Buffer,
bounds: Size,
}
#[cfg(not(target_arch = "wasm32"))]
type HashBuilder = twox_hash::RandomXxHashBuilder64;
#[cfg(target_arch = "wasm32")]
type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
impl Cache {
const TRIM_INTERVAL: usize = 300;
fn new() -> Self {
Self {
entries: FxHashMap::default(),
measurements: FxHashMap::default(),
recently_used: FxHashSet::default(),
hasher: HashBuilder::default(),
trim_count: 0,
}
}
fn allocate(
&mut self,
font_system: &mut cosmic_text::FontSystem,
key: Key<'_>,
) -> (KeyHash, &mut Entry) {
let hash = key.hash(self.hasher.build_hasher());
if let Some(hash) = self.measurements.get(&hash) {
let _ = self.recently_used.insert(*hash);
return (*hash, self.entries.get_mut(hash).unwrap());
}
if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2);
let mut buffer = cosmic_text::Buffer::new(font_system, metrics);
buffer.set_size(
font_system,
key.bounds.width,
key.bounds.height.max(key.size * 1.2),
);
buffer.set_text(
font_system,
key.content,
cosmic_text::Attrs::new()
.family(to_family(key.font.family))
.weight(to_weight(key.font.weight))
.stretch(to_stretch(key.font.stretch))
.style(to_style(key.font.style)),
to_shaping(key.shaping),
);
let bounds = measure(&buffer);
let _ = entry.insert(Entry { buffer, bounds });
for bounds in [
bounds,
Size {
width: key.bounds.width,
..bounds
},
] {
if key.bounds != bounds {
let _ = self.measurements.insert(
Key { bounds, ..key }.hash(self.hasher.build_hasher()),
hash,
);
}
}
}
let _ = self.recently_used.insert(hash);
(hash, self.entries.get_mut(&hash).unwrap())
}
fn trim(&mut self) {
if self.trim_count > Self::TRIM_INTERVAL {
self.entries
.retain(|key, _| self.recently_used.contains(key));
self.measurements
.retain(|_, value| self.recently_used.contains(value));
self.recently_used.clear();
self.trim_count = 0;
} else {
self.trim_count += 1;
}
}
}
#[derive(Debug, Clone, Copy)]
struct Key<'a> {
content: &'a str,
size: f32,
line_height: f32,
font: Font,
bounds: Size,
shaping: Shaping,
}
impl Key<'_> {
fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
self.content.hash(&mut hasher);
self.size.to_bits().hash(&mut hasher);
self.line_height.to_bits().hash(&mut hasher);
self.font.hash(&mut hasher);
self.bounds.width.to_bits().hash(&mut hasher);
self.bounds.height.to_bits().hash(&mut hasher);
self.shaping.hash(&mut hasher);
hasher.finish()
}
}
type KeyHash = u64;

View file

@ -28,9 +28,16 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
settings: Self::Settings,
_compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> {
let (compositor, backend) = new(settings);
let (compositor, backend) = new();
Ok((compositor, Renderer::new(backend)))
Ok((
compositor,
Renderer::new(
backend,
settings.default_font,
settings.default_text_size,
),
))
}
fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
@ -113,12 +120,12 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
}
}
pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) {
pub fn new<Theme>() -> (Compositor<Theme>, Backend) {
(
Compositor {
_theme: PhantomData,
},
Backend::new(settings),
Backend::new(),
)
}

View file

@ -21,20 +21,11 @@ guillotiere = "0.6"
futures = "0.3"
bitflags = "1.2"
once_cell = "1.0"
rustc-hash = "1.1"
log = "0.4"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wgpu = { version = "0.16", features = ["webgl"] }
[dependencies.twox-hash]
version = "1.6"
default-features = false
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
version = "1.6.1"
features = ["std"]
[dependencies.bytemuck]
version = "1.9"
features = ["derive"]

Binary file not shown.

View file

@ -1,5 +1,5 @@
use crate::core;
use crate::core::{Color, Font, Point, Size};
use crate::core::{Color, Size};
use crate::graphics;
use crate::graphics::backend;
use crate::graphics::color;
use crate::graphics::{Transformation, Viewport};
@ -29,9 +29,6 @@ pub struct Backend {
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: image::Pipeline,
default_font: Font,
default_text_size: f32,
}
impl Backend {
@ -57,9 +54,6 @@ impl Backend {
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline,
default_font: settings.default_font,
default_text_size: settings.default_text_size,
}
}
@ -313,65 +307,11 @@ impl Backend {
impl crate::graphics::Backend for Backend {
type Primitive = primitive::Custom;
fn trim_measurements(&mut self) {
self.text_pipeline.trim_measurements();
}
}
impl backend::Text for Backend {
const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = '\u{f00c}';
const ARROW_DOWN_ICON: char = '\u{e800}';
fn default_font(&self) -> Font {
self.default_font
}
fn default_size(&self) -> f32 {
self.default_text_size
}
fn measure(
&self,
contents: &str,
size: f32,
line_height: core::text::LineHeight,
font: Font,
bounds: Size,
shaping: core::text::Shaping,
) -> Size {
self.text_pipeline.measure(
contents,
size,
line_height,
font,
bounds,
shaping,
)
}
fn hit_test(
&self,
contents: &str,
size: f32,
line_height: core::text::LineHeight,
font: Font,
bounds: Size,
shaping: core::text::Shaping,
point: Point,
nearest_only: bool,
) -> Option<core::text::Hit> {
self.text_pipeline.hit_test(
contents,
size,
line_height,
font,
bounds,
shaping,
point,
nearest_only,
)
fn font_system(&self) -> &graphics::text::FontSystem {
self.text_pipeline.font_system()
}
fn load_font(&mut self, font: Cow<'static, [u8]>) {
@ -381,14 +321,17 @@ impl backend::Text for Backend {
#[cfg(feature = "image")]
impl backend::Image for Backend {
fn dimensions(&self, handle: &core::image::Handle) -> Size<u32> {
fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
self.image_pipeline.dimensions(handle)
}
}
#[cfg(feature = "svg")]
impl backend::Svg for Backend {
fn viewport_dimensions(&self, handle: &core::svg::Handle) -> Size<u32> {
fn viewport_dimensions(
&self,
handle: &crate::core::svg::Handle,
) -> Size<u32> {
self.image_pipeline.viewport_dimensions(handle)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,20 +1,17 @@
use crate::core::alignment;
use crate::core::font::{self, Font};
use crate::core::text::{Hit, LineHeight, Shaping};
use crate::core::{Pixels, Point, Rectangle, Size};
use crate::core::{Rectangle, Size};
use crate::graphics::color;
use crate::graphics::text::cache::{self, Cache};
use crate::graphics::text::{FontSystem, Paragraph};
use crate::layer::Text;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::hash_map;
use std::hash::{BuildHasher, Hash, Hasher};
use std::sync::Arc;
#[allow(missing_debug_implementations)]
pub struct Pipeline {
font_system: RefCell<glyphon::FontSystem>,
font_system: FontSystem,
renderers: Vec<glyphon::TextRenderer>,
atlas: glyphon::TextAtlas,
prepare_layer: usize,
@ -28,12 +25,7 @@ impl Pipeline {
format: wgpu::TextureFormat,
) -> Self {
Pipeline {
font_system: RefCell::new(glyphon::FontSystem::new_with_fonts(
[glyphon::fontdb::Source::Binary(Arc::new(
include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
))]
.into_iter(),
)),
font_system: FontSystem::new(),
renderers: Vec::new(),
atlas: glyphon::TextAtlas::with_color_mode(
device,
@ -50,6 +42,10 @@ impl Pipeline {
}
}
pub fn font_system(&self) -> &FontSystem {
&self.font_system
}
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
let _ = self.font_system.get_mut().db_mut().load_font_source(
glyphon::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
@ -80,97 +76,137 @@ impl Pipeline {
let renderer = &mut self.renderers[self.prepare_layer];
let cache = self.cache.get_mut();
if self.prepare_layer == 0 {
cache.trim(Purpose::Drawing);
enum Allocation {
Paragraph(Paragraph),
Cache(cache::KeyHash),
}
let keys: Vec<_> = sections
let allocations: Vec<_> = sections
.iter()
.map(|section| {
let (key, _) = cache.allocate(
font_system,
Key {
content: section.content,
size: section.size,
line_height: f32::from(
section
.line_height
.to_absolute(Pixels(section.size)),
),
font: section.font,
bounds: Size {
width: section.bounds.width,
height: section.bounds.height,
.map(|section| match section {
Text::Managed { paragraph, .. } => {
paragraph.upgrade().map(Allocation::Paragraph)
}
Text::Cached(text) => {
let (key, _) = cache.allocate(
font_system,
cache::Key {
content: text.content,
size: text.size.into(),
line_height: f32::from(
text.line_height.to_absolute(text.size),
),
font: text.font,
bounds: Size {
width: bounds.width,
height: bounds.height,
},
shaping: text.shaping,
},
shaping: section.shaping,
},
Purpose::Drawing,
);
);
key
Some(Allocation::Cache(key))
}
})
.collect();
let bounds = bounds * scale_factor;
let layer_bounds = bounds * scale_factor;
let text_areas =
sections
.iter()
.zip(keys.iter())
.filter_map(|(section, key)| {
let entry = cache.get(key).expect("Get cached buffer");
let text_areas = sections.iter().zip(allocations.iter()).filter_map(
|(section, allocation)| {
let (
buffer,
bounds,
horizontal_alignment,
vertical_alignment,
color,
) = match section {
Text::Managed {
position, color, ..
} => {
use crate::core::text::Paragraph as _;
let x = section.bounds.x * scale_factor;
let y = section.bounds.y * scale_factor;
let Some(Allocation::Paragraph(paragraph)) = allocation
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 {
alignment::Horizontal::Left => x,
alignment::Horizontal::Center => x - max_width / 2.0,
alignment::Horizontal::Right => x - max_width,
};
let buffer = cache.get(key).expect("Get cached buffer");
let top = match section.vertical_alignment {
alignment::Vertical::Top => y,
alignment::Vertical::Center => y - total_height / 2.0,
alignment::Vertical::Bottom => y - total_height,
};
(
buffer,
text.bounds,
text.horizontal_alignment,
text.vertical_alignment,
text.color,
)
}
};
let section_bounds = Rectangle {
x: left,
y: top,
width: section.bounds.width * scale_factor,
height: section.bounds.height * scale_factor,
};
let x = bounds.x * scale_factor;
let y = bounds.y * scale_factor;
let clip_bounds = bounds.intersection(&section_bounds)?;
let max_width = bounds.width * scale_factor;
let total_height = bounds.height * scale_factor;
Some(glyphon::TextArea {
buffer: &entry.buffer,
left,
top,
scale: scale_factor,
bounds: glyphon::TextBounds {
left: clip_bounds.x as i32,
top: clip_bounds.y as i32,
right: (clip_bounds.x + clip_bounds.width) as i32,
bottom: (clip_bounds.y + clip_bounds.height) as i32,
},
default_color: {
let [r, g, b, a] =
color::pack(section.color).components();
let left = match horizontal_alignment {
alignment::Horizontal::Left => x,
alignment::Horizontal::Center => x - max_width / 2.0,
alignment::Horizontal::Right => x - max_width,
};
glyphon::Color::rgba(
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
(a * 255.0) as u8,
)
},
})
});
let top = match vertical_alignment {
alignment::Vertical::Top => y,
alignment::Vertical::Center => y - total_height / 2.0,
alignment::Vertical::Bottom => y - total_height,
};
let section_bounds = Rectangle {
x: left,
y: top,
width: max_width,
height: total_height,
};
let clip_bounds = layer_bounds.intersection(&section_bounds)?;
Some(glyphon::TextArea {
buffer,
left,
top,
scale: scale_factor,
bounds: glyphon::TextBounds {
left: clip_bounds.x as i32,
top: clip_bounds.y as i32,
right: (clip_bounds.x + clip_bounds.width) as i32,
bottom: (clip_bounds.y + clip_bounds.height) as i32,
},
default_color: {
let [r, g, b, a] = color::pack(color).components();
glyphon::Color::rgba(
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
(a * 255.0) as u8,
)
},
})
},
);
let result = renderer.prepare(
device,
@ -219,287 +255,8 @@ impl Pipeline {
pub fn end_frame(&mut self) {
self.atlas.trim();
self.cache.get_mut().trim();
self.prepare_layer = 0;
}
pub fn trim_measurements(&mut self) {
self.cache.get_mut().trim(Purpose::Measuring);
}
pub fn measure(
&self,
content: &str,
size: f32,
line_height: LineHeight,
font: Font,
bounds: Size,
shaping: Shaping,
) -> Size {
let mut cache = self.cache.borrow_mut();
let line_height = f32::from(line_height.to_absolute(Pixels(size)));
let (_, entry) = cache.allocate(
&mut self.font_system.borrow_mut(),
Key {
content,
size,
line_height,
font,
bounds,
shaping,
},
Purpose::Measuring,
);
entry.bounds
}
pub fn hit_test(
&self,
content: &str,
size: f32,
line_height: LineHeight,
font: Font,
bounds: Size,
shaping: Shaping,
point: Point,
_nearest_only: bool,
) -> Option<Hit> {
let mut cache = self.cache.borrow_mut();
let line_height = f32::from(line_height.to_absolute(Pixels(size)));
let (_, entry) = cache.allocate(
&mut self.font_system.borrow_mut(),
Key {
content,
size,
line_height,
font,
bounds,
shaping,
},
Purpose::Measuring,
);
let cursor = entry.buffer.hit(point.x, point.y)?;
Some(Hit::CharOffset(cursor.index))
}
}
fn measure(buffer: &glyphon::Buffer) -> Size {
let (width, total_lines) = buffer
.layout_runs()
.fold((0.0, 0usize), |(width, total_lines), run| {
(run.line_w.max(width), total_lines + 1)
});
Size::new(width, total_lines as f32 * buffer.metrics().line_height)
}
fn to_family(family: font::Family) -> glyphon::Family<'static> {
match family {
font::Family::Name(name) => glyphon::Family::Name(name),
font::Family::SansSerif => glyphon::Family::SansSerif,
font::Family::Serif => glyphon::Family::Serif,
font::Family::Cursive => glyphon::Family::Cursive,
font::Family::Fantasy => glyphon::Family::Fantasy,
font::Family::Monospace => glyphon::Family::Monospace,
}
}
fn to_weight(weight: font::Weight) -> glyphon::Weight {
match weight {
font::Weight::Thin => glyphon::Weight::THIN,
font::Weight::ExtraLight => glyphon::Weight::EXTRA_LIGHT,
font::Weight::Light => glyphon::Weight::LIGHT,
font::Weight::Normal => glyphon::Weight::NORMAL,
font::Weight::Medium => glyphon::Weight::MEDIUM,
font::Weight::Semibold => glyphon::Weight::SEMIBOLD,
font::Weight::Bold => glyphon::Weight::BOLD,
font::Weight::ExtraBold => glyphon::Weight::EXTRA_BOLD,
font::Weight::Black => glyphon::Weight::BLACK,
}
}
fn to_stretch(stretch: font::Stretch) -> glyphon::Stretch {
match stretch {
font::Stretch::UltraCondensed => glyphon::Stretch::UltraCondensed,
font::Stretch::ExtraCondensed => glyphon::Stretch::ExtraCondensed,
font::Stretch::Condensed => glyphon::Stretch::Condensed,
font::Stretch::SemiCondensed => glyphon::Stretch::SemiCondensed,
font::Stretch::Normal => glyphon::Stretch::Normal,
font::Stretch::SemiExpanded => glyphon::Stretch::SemiExpanded,
font::Stretch::Expanded => glyphon::Stretch::Expanded,
font::Stretch::ExtraExpanded => glyphon::Stretch::ExtraExpanded,
font::Stretch::UltraExpanded => glyphon::Stretch::UltraExpanded,
}
}
fn to_style(style: font::Style) -> glyphon::Style {
match style {
font::Style::Normal => glyphon::Style::Normal,
font::Style::Italic => glyphon::Style::Italic,
font::Style::Oblique => glyphon::Style::Oblique,
}
}
fn to_shaping(shaping: Shaping) -> glyphon::Shaping {
match shaping {
Shaping::Basic => glyphon::Shaping::Basic,
Shaping::Advanced => glyphon::Shaping::Advanced,
}
}
struct Cache {
entries: FxHashMap<KeyHash, Entry>,
aliases: FxHashMap<KeyHash, KeyHash>,
recently_measured: FxHashSet<KeyHash>,
recently_drawn: FxHashSet<KeyHash>,
hasher: HashBuilder,
}
struct Entry {
buffer: glyphon::Buffer,
bounds: Size,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Purpose {
Measuring,
Drawing,
}
#[cfg(not(target_arch = "wasm32"))]
type HashBuilder = twox_hash::RandomXxHashBuilder64;
#[cfg(target_arch = "wasm32")]
type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
impl Cache {
fn new() -> Self {
Self {
entries: FxHashMap::default(),
aliases: FxHashMap::default(),
recently_measured: FxHashSet::default(),
recently_drawn: FxHashSet::default(),
hasher: HashBuilder::default(),
}
}
fn get(&self, key: &KeyHash) -> Option<&Entry> {
self.entries.get(key)
}
fn allocate(
&mut self,
font_system: &mut glyphon::FontSystem,
key: Key<'_>,
purpose: Purpose,
) -> (KeyHash, &mut Entry) {
let hash = key.hash(self.hasher.build_hasher());
let recently_used = match purpose {
Purpose::Measuring => &mut self.recently_measured,
Purpose::Drawing => &mut self.recently_drawn,
};
if let Some(hash) = self.aliases.get(&hash) {
let _ = recently_used.insert(*hash);
return (*hash, self.entries.get_mut(hash).unwrap());
}
if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
let metrics = glyphon::Metrics::new(key.size, key.line_height);
let mut buffer = glyphon::Buffer::new(font_system, metrics);
buffer.set_size(
font_system,
key.bounds.width,
key.bounds.height.max(key.line_height),
);
buffer.set_text(
font_system,
key.content,
glyphon::Attrs::new()
.family(to_family(key.font.family))
.weight(to_weight(key.font.weight))
.stretch(to_stretch(key.font.stretch))
.style(to_style(key.font.style)),
to_shaping(key.shaping),
);
let bounds = measure(&buffer);
let _ = entry.insert(Entry { buffer, bounds });
for bounds in [
bounds,
Size {
width: key.bounds.width,
..bounds
},
] {
if key.bounds != bounds {
let _ = self.aliases.insert(
Key { bounds, ..key }.hash(self.hasher.build_hasher()),
hash,
);
}
}
}
let _ = recently_used.insert(hash);
(hash, self.entries.get_mut(&hash).unwrap())
}
fn trim(&mut self, purpose: Purpose) {
self.entries.retain(|key, _| {
self.recently_measured.contains(key)
|| self.recently_drawn.contains(key)
});
self.aliases.retain(|_, value| {
self.recently_measured.contains(value)
|| self.recently_drawn.contains(value)
});
match purpose {
Purpose::Measuring => {
self.recently_measured.clear();
}
Purpose::Drawing => {
self.recently_drawn.clear();
}
}
}
}
#[derive(Debug, Clone, Copy)]
struct Key<'a> {
content: &'a str,
size: f32,
line_height: f32,
font: Font,
bounds: Size,
shaping: Shaping,
}
impl Key<'_> {
fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
self.content.hash(&mut hasher);
self.size.to_bits().hash(&mut hasher);
self.line_height.to_bits().hash(&mut hasher);
self.font.hash(&mut hasher);
self.bounds.width.to_bits().hash(&mut hasher);
self.bounds.height.to_bits().hash(&mut hasher);
self.shaping.hash(&mut hasher);
hasher.finish()
}
}
type KeyHash = u64;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -144,11 +144,6 @@ where
self
}
/// Returns whether the [`ComboBox`] is currently focused or not.
pub fn is_focused(&self) -> bool {
self.state.is_focused()
}
/// Sets the text sixe of the [`ComboBox`].
pub fn size(mut self, size: f32) -> Self {
self.text_input = self.text_input.size(size);
@ -179,7 +174,6 @@ pub struct State<T>(RefCell<Inner<T>>);
#[derive(Debug, Clone)]
struct Inner<T> {
text_input: text_input::State,
value: String,
options: Vec<T>,
option_matchers: Vec<String>,
@ -216,7 +210,6 @@ where
);
Self(RefCell::new(Inner {
text_input: text_input::State::new(),
value,
options,
option_matchers,
@ -224,51 +217,12 @@ where
}))
}
/// Focuses the [`ComboBox`].
pub fn focused(self) -> Self {
self.focus();
self
}
/// Focuses the [`ComboBox`].
pub fn focus(&self) {
let mut inner = self.0.borrow_mut();
inner.text_input.focus();
}
/// Unfocuses the [`ComboBox`].
pub fn unfocus(&self) {
let mut inner = self.0.borrow_mut();
inner.text_input.unfocus();
}
/// Returns whether the [`ComboBox`] is currently focused or not.
pub fn is_focused(&self) -> bool {
let inner = self.0.borrow();
inner.text_input.is_focused()
}
fn value(&self) -> String {
let inner = self.0.borrow();
inner.value.clone()
}
fn text_input_tree(&self) -> widget::Tree {
let inner = self.0.borrow();
inner.text_input_tree()
}
fn update_text_input(&self, tree: widget::Tree) {
let mut inner = self.0.borrow_mut();
inner.update_text_input(tree)
}
fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
let inner = self.0.borrow();
@ -288,21 +242,6 @@ where
}
}
impl<T> Inner<T> {
fn text_input_tree(&self) -> widget::Tree {
widget::Tree {
tag: widget::tree::Tag::of::<text_input::State>(),
state: widget::tree::State::new(self.text_input.clone()),
children: vec![],
}
}
fn update_text_input(&mut self, tree: widget::Tree) {
self.text_input =
tree.state.downcast_ref::<text_input::State>().clone();
}
}
impl<T> Filtered<T>
where
T: Clone,
@ -366,10 +305,11 @@ where
fn layout(
&self,
tree: &widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.text_input.layout(renderer, limits)
self.text_input.layout(tree, renderer, limits)
}
fn tag(&self) -> widget::tree::Tag {
@ -385,6 +325,10 @@ where
})
}
fn children(&self) -> Vec<widget::Tree> {
vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _>)]
}
fn on_event(
&mut self,
tree: &mut widget::Tree,
@ -398,7 +342,13 @@ where
) -> event::Status {
let menu = tree.state.downcast_mut::<Menu<T>>();
let started_focused = self.state.is_focused();
let started_focused = {
let text_input_state = tree.children[0]
.state
.downcast_ref::<text_input::State<Renderer::Paragraph>>();
text_input_state.is_focused()
};
// This is intended to check whether or not the message buffer was empty,
// since `Shell` does not expose such functionality.
let mut published_message_to_shell = false;
@ -408,9 +358,8 @@ where
let mut local_shell = Shell::new(&mut local_messages);
// Provide it to the widget
let mut tree = self.state.text_input_tree();
let mut event_status = self.text_input.on_event(
&mut tree,
&mut tree.children[0],
event.clone(),
layout,
cursor,
@ -419,7 +368,6 @@ where
&mut local_shell,
viewport,
);
self.state.update_text_input(tree);
// Then finally react to them here
for message in local_messages {
@ -450,7 +398,15 @@ where
shell.invalidate_layout();
}
if self.state.is_focused() {
let is_focused = {
let text_input_state = tree.children[0]
.state
.downcast_ref::<text_input::State<Renderer::Paragraph>>();
text_input_state.is_focused()
};
if is_focused {
self.state.with_inner(|state| {
if !started_focused {
if let Some(on_option_hovered) = &mut self.on_option_hovered
@ -589,9 +545,8 @@ where
published_message_to_shell = true;
// Unfocus the input
let mut tree = state.text_input_tree();
let _ = self.text_input.on_event(
&mut tree,
&mut tree.children[0],
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)),
@ -602,21 +557,25 @@ where
&mut Shell::new(&mut vec![]),
viewport,
);
state.update_text_input(tree);
}
});
if started_focused
&& !self.state.is_focused()
&& !published_message_to_shell
{
let is_focused = {
let text_input_state = tree.children[0]
.state
.downcast_ref::<text_input::State<Renderer::Paragraph>>();
text_input_state.is_focused()
};
if started_focused && !is_focused && !published_message_to_shell {
if let Some(message) = self.on_close.take() {
shell.publish(message);
}
}
// Focus changed, invalidate widget tree to force a fresh `view`
if started_focused != self.state.is_focused() {
if started_focused != is_focused {
shell.invalidate_widgets();
}
@ -625,20 +584,24 @@ where
fn mouse_interaction(
&self,
_tree: &widget::Tree,
tree: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let tree = self.state.text_input_tree();
self.text_input
.mouse_interaction(&tree, layout, cursor, viewport, renderer)
self.text_input.mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn draw(
&self,
_tree: &widget::Tree,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
@ -646,16 +609,28 @@ where
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let selection = if self.state.is_focused() || self.selection.is_empty()
{
let is_focused = {
let text_input_state = tree.children[0]
.state
.downcast_ref::<text_input::State<Renderer::Paragraph>>();
text_input_state.is_focused()
};
let selection = if is_focused || self.selection.is_empty() {
None
} else {
Some(&self.selection)
};
let tree = self.state.text_input_tree();
self.text_input
.draw(&tree, renderer, theme, layout, cursor, selection);
self.text_input.draw(
&tree.children[0],
renderer,
theme,
layout,
cursor,
selection,
);
}
fn overlay<'b>(
@ -664,14 +639,22 @@ where
layout: Layout<'_>,
_renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
let Menu {
menu,
filtered_options,
hovered_option,
..
} = tree.state.downcast_mut::<Menu<T>>();
let is_focused = {
let text_input_state = tree.children[0]
.state
.downcast_ref::<text_input::State<Renderer::Paragraph>>();
text_input_state.is_focused()
};
if is_focused {
let Menu {
menu,
filtered_options,
hovered_option,
..
} = tree.state.downcast_mut::<Menu<T>>();
if self.state.is_focused() {
let bounds = layout.bounds();
self.state.sync_filtered_options(filtered_options);

View file

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

View file

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

View file

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

View file

@ -152,11 +152,12 @@ where
fn layout(
&self,
tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.with_element(|element| {
element.as_widget().layout(renderer, limits)
element.as_widget().layout(tree, renderer, limits)
})
}

View file

@ -254,11 +254,12 @@ where
fn layout(
&self,
tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.with_element(|element| {
element.as_widget().layout(renderer, limits)
element.as_widget().layout(tree, renderer, limits)
})
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@ use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse::{self, click};
use crate::core::renderer;
use crate::core::text::{self, Text};
use crate::core::text::{self, Paragraph as _, Text};
use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
@ -30,6 +30,8 @@ use crate::core::{
};
use crate::runtime::Command;
use std::cell::RefCell;
pub use iced_style::text_input::{Appearance, StyleSheet};
/// A field that can be filled with text.
@ -67,7 +69,7 @@ where
font: Option<Renderer::Font>,
width: Length,
padding: Padding,
size: Option<f32>,
size: Option<Pixels>,
line_height: text::LineHeight,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
@ -178,7 +180,7 @@ where
/// Sets the text size of the [`TextInput`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = Some(size.into().0);
self.size = Some(size.into());
self
}
@ -218,12 +220,8 @@ where
theme,
layout,
cursor,
tree.state.downcast_ref::<State>(),
tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
value.unwrap_or(&self.value),
&self.placeholder,
self.size,
self.line_height,
self.font,
self.on_input.is_none(),
self.is_secure,
self.icon.as_ref(),
@ -240,15 +238,15 @@ where
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
tree::Tag::of::<State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
tree::State::new(State::<Renderer::Paragraph>::new())
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>();
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
// Unfocus text input if it becomes disabled
if self.on_input.is_none() {
@ -269,6 +267,7 @@ where
fn layout(
&self,
tree: &Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@ -278,8 +277,13 @@ where
self.width,
self.padding,
self.size,
self.font,
self.line_height,
self.icon.as_ref(),
tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
&self.value,
&self.placeholder,
self.is_secure,
)
}
@ -290,7 +294,7 @@ where
_renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
let state = tree.state.downcast_mut::<State>();
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
operation.text_input(state, self.id.as_ref().map(|id| &id.0));
@ -322,7 +326,7 @@ where
self.on_input.as_deref(),
self.on_paste.as_deref(),
&self.on_submit,
|| tree.state.downcast_mut::<State>(),
|| tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
)
}
@ -341,12 +345,8 @@ where
theme,
layout,
cursor,
tree.state.downcast_ref::<State>(),
tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
&self.value,
&self.placeholder,
self.size,
self.line_height,
self.font,
self.on_input.is_none(),
self.is_secure,
self.icon.as_ref(),
@ -388,7 +388,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// The font size of the content.
pub size: Option<f32>,
pub size: Option<Pixels>,
/// The spacing between the [`Icon`] and the text in a [`TextInput`].
pub spacing: f32,
/// The side of a [`TextInput`] where to display the [`Icon`].
@ -465,29 +465,65 @@ pub fn layout<Renderer>(
limits: &layout::Limits,
width: Length,
padding: Padding,
size: Option<f32>,
size: Option<Pixels>,
font: Option<Renderer::Font>,
line_height: text::LineHeight,
icon: Option<&Icon<Renderer::Font>>,
state: &State<Renderer::Paragraph>,
value: &Value,
placeholder: &str,
is_secure: bool,
) -> layout::Node
where
Renderer: text::Renderer,
{
let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = size.unwrap_or_else(|| renderer.default_size());
let padding = padding.fit(Size::ZERO, limits.max());
let limits = limits
.width(width)
.pad(padding)
.height(line_height.to_absolute(Pixels(text_size)));
.height(line_height.to_absolute(text_size));
let text_bounds = limits.resolve(Size::ZERO);
if let Some(icon) = icon {
let icon_width = renderer.measure_width(
&icon.code_point.to_string(),
icon.size.unwrap_or_else(|| renderer.default_size()),
icon.font,
text::Shaping::Advanced,
let placeholder_text = Text {
font,
line_height,
content: placeholder,
bounds: Size::new(f32::INFINITY, text_bounds.height),
size: text_size,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
};
renderer.update_paragraph(
&mut state.placeholder_paragraph.borrow_mut(),
placeholder_text,
);
if is_secure {
renderer.update_paragraph(
&mut state.paragraph.borrow_mut(),
Text {
content: &value.secure().to_string(),
..placeholder_text
},
);
} else {
renderer.update_paragraph(
&mut state.paragraph.borrow_mut(),
Text {
content: &value.to_string(),
..placeholder_text
},
);
}
if let Some(icon) = icon {
let icon_width = 0.0; // TODO
let mut text_node = layout::Node::new(
text_bounds - Size::new(icon_width + icon.spacing, 0.0),
@ -537,19 +573,31 @@ pub fn update<'a, Message, Renderer>(
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
value: &mut Value,
size: Option<f32>,
size: Option<Pixels>,
line_height: text::LineHeight,
font: Option<Renderer::Font>,
is_secure: bool,
on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>,
state: impl FnOnce() -> &'a mut State,
state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>,
) -> event::Status
where
Message: Clone,
Renderer: text::Renderer,
{
let update_cache = |state, value| {
replace_paragraph(
renderer,
state,
layout,
value,
font,
size,
line_height,
)
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
@ -592,11 +640,7 @@ where
};
find_cursor_position(
renderer,
text_layout.bounds(),
font,
size,
line_height,
&value,
state,
target,
@ -621,11 +665,7 @@ where
state.cursor.select_all(value);
} else {
let position = find_cursor_position(
renderer,
text_layout.bounds(),
font,
size,
line_height,
value,
state,
target,
@ -671,11 +711,7 @@ where
};
let position = find_cursor_position(
renderer,
text_layout.bounds(),
font,
size,
line_height,
&value,
state,
target,
@ -710,6 +746,8 @@ where
focus.updated_at = Instant::now();
update_cache(state, value);
return event::Status::Captured;
}
}
@ -749,6 +787,8 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
update_cache(state, value);
}
keyboard::KeyCode::Delete => {
if platform::is_jump_modifier_pressed(modifiers)
@ -769,6 +809,8 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
update_cache(state, value);
}
keyboard::KeyCode::Left => {
if platform::is_jump_modifier_pressed(modifiers)
@ -844,6 +886,8 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
update_cache(state, value);
}
keyboard::KeyCode::V => {
if state.keyboard_modifiers.command()
@ -876,6 +920,8 @@ where
shell.publish(message);
state.is_pasting = Some(content);
update_cache(state, value);
} else {
state.is_pasting = None;
}
@ -979,12 +1025,8 @@ pub fn draw<Renderer>(
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor: mouse::Cursor,
state: &State,
state: &State<Renderer::Paragraph>,
value: &Value,
placeholder: &str,
size: Option<f32>,
line_height: text::LineHeight,
font: Option<Renderer::Font>,
is_disabled: bool,
is_secure: bool,
icon: Option<&Icon<Renderer::Font>>,
@ -1023,28 +1065,14 @@ pub fn draw<Renderer>(
appearance.background,
);
if let Some(icon) = icon {
let icon_layout = children_layout.next().unwrap();
if let Some(_icon) = icon {
let _icon_layout = children_layout.next().unwrap();
renderer.fill_text(Text {
content: &icon.code_point.to_string(),
size: icon.size.unwrap_or_else(|| renderer.default_size()),
line_height: text::LineHeight::default(),
font: icon.font,
color: appearance.icon_color,
bounds: Rectangle {
y: text_bounds.center_y(),
..icon_layout.bounds()
},
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
});
// TODO
}
let text = value.to_string();
let font = font.unwrap_or_else(|| renderer.default_font());
let size = size.unwrap_or_else(|| renderer.default_size());
let paragraph = &state.paragraph.borrow() as &Renderer::Paragraph;
let (cursor, offset) = if let Some(focus) = state
.is_focused
@ -1055,12 +1083,9 @@ pub fn draw<Renderer>(
cursor::State::Index(position) => {
let (text_value_width, offset) =
measure_cursor_and_scroll_offset(
renderer,
paragraph,
text_bounds,
value,
size,
position,
font,
);
let is_cursor_visible = ((focus.now - focus.updated_at)
@ -1096,22 +1121,16 @@ pub fn draw<Renderer>(
let (left_position, left_offset) =
measure_cursor_and_scroll_offset(
renderer,
paragraph,
text_bounds,
value,
size,
left,
font,
);
let (right_position, right_offset) =
measure_cursor_and_scroll_offset(
renderer,
paragraph,
text_bounds,
value,
size,
right,
font,
);
let width = right_position - left_position;
@ -1143,12 +1162,7 @@ pub fn draw<Renderer>(
(None, 0.0)
};
let text_width = renderer.measure_width(
if text.is_empty() { placeholder } else { &text },
size,
font,
text::Shaping::Advanced,
);
let text_width = paragraph.min_width();
let render = |renderer: &mut Renderer| {
if let Some((cursor, color)) = cursor {
@ -1157,27 +1171,23 @@ pub fn draw<Renderer>(
renderer.with_translation(Vector::ZERO, |_| {});
}
renderer.fill_text(Text {
content: if text.is_empty() { placeholder } else { &text },
color: if text.is_empty() {
let placeholder_paragraph = state.placeholder_paragraph.borrow();
renderer.fill_paragraph(
if text.is_empty() {
&placeholder_paragraph
} else {
paragraph
},
Point::new(text_bounds.x, text_bounds.center_y()),
if text.is_empty() {
theme.placeholder_color(style)
} else if is_disabled {
theme.disabled_color(style)
} else {
theme.value_color(style)
},
font,
bounds: Rectangle {
y: text_bounds.center_y(),
width: f32::INFINITY,
..text_bounds
},
size,
line_height,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
});
);
};
if text_width > text_bounds.width {
@ -1208,7 +1218,9 @@ pub fn mouse_interaction(
/// The state of a [`TextInput`].
#[derive(Debug, Default, Clone)]
pub struct State {
pub struct State<P: text::Paragraph> {
paragraph: RefCell<P>,
placeholder_paragraph: RefCell<P>,
is_focused: Option<Focus>,
is_dragging: bool,
is_pasting: Option<Value>,
@ -1225,7 +1237,7 @@ struct Focus {
is_window_focused: bool,
}
impl State {
impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`], representing an unfocused [`TextInput`].
pub fn new() -> Self {
Self::default()
@ -1234,6 +1246,8 @@ impl State {
/// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused() -> Self {
Self {
paragraph: RefCell::new(P::default()),
placeholder_paragraph: RefCell::new(P::default()),
is_focused: None,
is_dragging: false,
is_pasting: None,
@ -1292,7 +1306,7 @@ impl State {
}
}
impl operation::Focusable for State {
impl<P: text::Paragraph> operation::Focusable for State<P> {
fn is_focused(&self) -> bool {
State::is_focused(self)
}
@ -1306,7 +1320,7 @@ impl operation::Focusable for State {
}
}
impl operation::TextInput for State {
impl<P: text::Paragraph> operation::TextInput for State<P> {
fn move_cursor_to_front(&mut self) {
State::move_cursor_to_front(self)
}
@ -1336,17 +1350,11 @@ mod platform {
}
}
fn offset<Renderer>(
renderer: &Renderer,
fn offset<P: text::Paragraph>(
text_bounds: Rectangle,
font: Renderer::Font,
size: f32,
value: &Value,
state: &State,
) -> f32
where
Renderer: text::Renderer,
{
state: &State<P>,
) -> f32 {
if state.is_focused() {
let cursor = state.cursor();
@ -1356,12 +1364,9 @@ where
};
let (_, offset) = measure_cursor_and_scroll_offset(
renderer,
&state.paragraph.borrow() as &P,
text_bounds,
value,
size,
focus_position,
font,
);
offset
@ -1370,63 +1375,35 @@ where
}
}
fn measure_cursor_and_scroll_offset<Renderer>(
renderer: &Renderer,
fn measure_cursor_and_scroll_offset(
paragraph: &impl text::Paragraph,
text_bounds: Rectangle,
value: &Value,
size: f32,
cursor_index: usize,
font: Renderer::Font,
) -> (f32, f32)
where
Renderer: text::Renderer,
{
let text_before_cursor = value.until(cursor_index).to_string();
) -> (f32, f32) {
let grapheme_position = paragraph
.grapheme_position(0, cursor_index)
.unwrap_or(Point::ORIGIN);
let text_value_width = renderer.measure_width(
&text_before_cursor,
size,
font,
text::Shaping::Advanced,
);
let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0);
(text_value_width, offset)
(grapheme_position.x, offset)
}
/// Computes the position of the text cursor at the given X coordinate of
/// a [`TextInput`].
fn find_cursor_position<Renderer>(
renderer: &Renderer,
fn find_cursor_position<P: text::Paragraph>(
text_bounds: Rectangle,
font: Option<Renderer::Font>,
size: Option<f32>,
line_height: text::LineHeight,
value: &Value,
state: &State,
state: &State<P>,
x: f32,
) -> Option<usize>
where
Renderer: text::Renderer,
{
let font = font.unwrap_or_else(|| renderer.default_font());
let size = size.unwrap_or_else(|| renderer.default_size());
let offset = offset(renderer, text_bounds, font, size, value, state);
) -> Option<usize> {
let offset = offset(text_bounds, value, state);
let value = value.to_string();
let char_offset = renderer
.hit_test(
&value,
size,
line_height,
font,
Size::INFINITY,
text::Shaping::Advanced,
Point::new(x + offset, text_bounds.height / 2.0),
true,
)
let char_offset = state
.paragraph
.borrow()
.hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?;
Some(
@ -1438,4 +1415,33 @@ where
)
}
fn replace_paragraph<Renderer>(
renderer: &Renderer,
state: &mut State<Renderer::Paragraph>,
layout: Layout<'_>,
value: &Value,
font: Option<Renderer::Font>,
text_size: Option<Pixels>,
line_height: text::LineHeight,
) where
Renderer: text::Renderer,
{
let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds();
*state.paragraph.get_mut() = renderer.create_paragraph(Text {
font,
line_height,
content: &value.to_string(),
bounds: Size::new(f32::INFINITY, text_bounds.height),
size: text_size,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
});
}
const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;

View file

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

View file

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

View file

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