diff options
author | 2023-08-30 04:31:21 +0200 | |
---|---|---|
committer | 2023-08-30 04:31:21 +0200 | |
commit | ed3454301e663a7cb7d73cd56b57b188f4d14a2f (patch) | |
tree | 8118d1305c2eba3a1b45d04634cd0e8d050fc0fa /widget | |
parent | c9bd48704dd9679c033dd0b8588e2744a3df44a0 (diff) | |
download | iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.tar.gz iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.tar.bz2 iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.zip |
Implement explicit text caching in the widget state tree
Diffstat (limited to 'widget')
30 files changed, 643 insertions, 530 deletions
diff --git a/widget/src/button.rs b/widget/src/button.rs index 5727c631..1788b6c4 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -159,19 +159,15 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.padding, - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) + layout(limits, self.width, self.height, self.padding, |limits| { + self.content + .as_widget() + .layout(&tree.children[0], renderer, limits) + }) } fn operate( @@ -426,17 +422,16 @@ where } /// Computes the layout of a [`Button`]. -pub fn layout<Renderer>( - renderer: &Renderer, +pub fn layout( limits: &layout::Limits, width: Length, height: Length, padding: Padding, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, + layout_content: impl FnOnce(&layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits.width(width).height(height); - let mut content = layout_content(renderer, &limits.pad(padding)); + let mut content = layout_content(&limits.pad(padding)); let padding = padding.fit(content.size(), limits.max()); let size = limits.pad(padding).resolve(content.size()).pad(padding); diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 1a186432..d749355b 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -129,6 +129,7 @@ where fn layout( &self, + _tree: &Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 310a67ed..a66ce3ff 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -6,12 +6,11 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text; use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, - Widget, + Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, }; -use crate::{Row, Text}; pub use iced_style::checkbox::{Appearance, StyleSheet}; @@ -45,7 +44,7 @@ where width: Length, size: f32, spacing: f32, - text_size: Option<f32>, + text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, @@ -118,7 +117,7 @@ where /// Sets the text size of the [`Checkbox`]. pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { - self.text_size = Some(text_size.into().0); + self.text_size = Some(text_size.into()); self } @@ -167,6 +166,14 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + crate::text::StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<widget::text::State<Renderer::Paragraph>>() + } + + fn state(&self) -> tree::State { + tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) + } + fn width(&self) -> Length { self.width } @@ -177,26 +184,35 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - Row::<(), Renderer>::new() - .width(self.width) - .spacing(self.spacing) - .align_items(Alignment::Center) - .push(Row::new().width(self.size).height(self.size)) - .push( - Text::new(&self.label) - .font(self.font.unwrap_or_else(|| renderer.default_font())) - .width(self.width) - .size( - self.text_size - .unwrap_or_else(|| renderer.default_size()), - ) - .line_height(self.text_line_height) - .shaping(self.text_shaping), - ) - .layout(renderer, limits) + layout::next_to_each_other( + &limits.width(self.width), + self.spacing, + |_| layout::Node::new(Size::new(self.size, self.size)), + |limits| { + let state = tree + .state + .downcast_ref::<widget::text::State<Renderer::Paragraph>>(); + + widget::text::layout( + state, + renderer, + limits, + self.width, + Length::Shrink, + &self.label, + self.text_line_height, + self.text_size, + self.font, + alignment::Horizontal::Left, + alignment::Vertical::Top, + self.text_shaping, + ) + }, + ) } fn on_event( @@ -244,7 +260,7 @@ where fn draw( &self, - _tree: &Tree, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -283,24 +299,23 @@ where line_height, shaping, } = &self.icon; - let size = size.unwrap_or(bounds.height * 0.7); + let size = size.unwrap_or(Pixels(bounds.height * 0.7)); if self.is_checked { - renderer.fill_text(text::Text { - content: &code_point.to_string(), - font: *font, - size, - line_height: *line_height, - bounds: Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds + renderer.fill_text( + text::Text { + content: &code_point.to_string(), + font: *font, + size, + line_height: *line_height, + bounds: bounds.size(), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: *shaping, }, - color: custom_style.icon_color, - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - shaping: *shaping, - }); + bounds.center(), + custom_style.icon_color, + ); } } @@ -311,16 +326,10 @@ where renderer, style, label_layout, - &self.label, - self.text_size, - self.text_line_height, - self.font, + tree.state.downcast_ref(), crate::text::Appearance { color: custom_style.text_color, }, - alignment::Horizontal::Left, - alignment::Vertical::Center, - self.text_shaping, ); } } @@ -348,7 +357,7 @@ pub struct Icon<Font> { /// The unicode code point that will be used as the icon. pub code_point: char, /// Font size of the content. - pub size: Option<f32>, + pub size: Option<Pixels>, /// The line height of the icon. pub line_height: text::LineHeight, /// The shaping strategy of the icon. diff --git a/widget/src/column.rs b/widget/src/column.rs index c16477f3..107c3475 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -122,6 +122,7 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -138,6 +139,7 @@ where self.spacing, self.align_items, &self.children, + &tree.children, ) } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 690ef27c..8c20ae8e 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -144,11 +144,6 @@ where self } - /// Returns whether the [`ComboBox`] is currently focused or not. - pub fn is_focused(&self) -> bool { - self.state.is_focused() - } - /// Sets the text sixe of the [`ComboBox`]. pub fn size(mut self, size: f32) -> Self { self.text_input = self.text_input.size(size); @@ -179,7 +174,6 @@ pub struct State<T>(RefCell<Inner<T>>); #[derive(Debug, Clone)] struct Inner<T> { - text_input: text_input::State, value: String, options: Vec<T>, option_matchers: Vec<String>, @@ -216,7 +210,6 @@ where ); Self(RefCell::new(Inner { - text_input: text_input::State::new(), value, options, option_matchers, @@ -224,51 +217,12 @@ where })) } - /// Focuses the [`ComboBox`]. - pub fn focused(self) -> Self { - self.focus(); - self - } - - /// Focuses the [`ComboBox`]. - pub fn focus(&self) { - let mut inner = self.0.borrow_mut(); - - inner.text_input.focus(); - } - - /// Unfocuses the [`ComboBox`]. - pub fn unfocus(&self) { - let mut inner = self.0.borrow_mut(); - - inner.text_input.unfocus(); - } - - /// Returns whether the [`ComboBox`] is currently focused or not. - pub fn is_focused(&self) -> bool { - let inner = self.0.borrow(); - - inner.text_input.is_focused() - } - fn value(&self) -> String { let inner = self.0.borrow(); inner.value.clone() } - fn text_input_tree(&self) -> widget::Tree { - let inner = self.0.borrow(); - - inner.text_input_tree() - } - - fn update_text_input(&self, tree: widget::Tree) { - let mut inner = self.0.borrow_mut(); - - inner.update_text_input(tree) - } - fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O { let inner = self.0.borrow(); @@ -288,21 +242,6 @@ where } } -impl<T> Inner<T> { - fn text_input_tree(&self) -> widget::Tree { - widget::Tree { - tag: widget::tree::Tag::of::<text_input::State>(), - state: widget::tree::State::new(self.text_input.clone()), - children: vec![], - } - } - - fn update_text_input(&mut self, tree: widget::Tree) { - self.text_input = - tree.state.downcast_ref::<text_input::State>().clone(); - } -} - impl<T> Filtered<T> where T: Clone, @@ -366,10 +305,11 @@ where fn layout( &self, + tree: &widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.text_input.layout(renderer, limits) + self.text_input.layout(tree, renderer, limits) } fn tag(&self) -> widget::tree::Tag { @@ -385,6 +325,10 @@ where }) } + fn children(&self) -> Vec<widget::Tree> { + vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _>)] + } + fn on_event( &mut self, tree: &mut widget::Tree, @@ -398,7 +342,13 @@ where ) -> event::Status { let menu = tree.state.downcast_mut::<Menu<T>>(); - let started_focused = self.state.is_focused(); + let started_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::<text_input::State<Renderer::Paragraph>>(); + + text_input_state.is_focused() + }; // This is intended to check whether or not the message buffer was empty, // since `Shell` does not expose such functionality. let mut published_message_to_shell = false; @@ -408,9 +358,8 @@ where let mut local_shell = Shell::new(&mut local_messages); // Provide it to the widget - let mut tree = self.state.text_input_tree(); let mut event_status = self.text_input.on_event( - &mut tree, + &mut tree.children[0], event.clone(), layout, cursor, @@ -419,7 +368,6 @@ where &mut local_shell, viewport, ); - self.state.update_text_input(tree); // Then finally react to them here for message in local_messages { @@ -450,7 +398,15 @@ where shell.invalidate_layout(); } - if self.state.is_focused() { + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::<text_input::State<Renderer::Paragraph>>(); + + text_input_state.is_focused() + }; + + if is_focused { self.state.with_inner(|state| { if !started_focused { if let Some(on_option_hovered) = &mut self.on_option_hovered @@ -589,9 +545,8 @@ where published_message_to_shell = true; // Unfocus the input - let mut tree = state.text_input_tree(); let _ = self.text_input.on_event( - &mut tree, + &mut tree.children[0], Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, )), @@ -602,21 +557,25 @@ where &mut Shell::new(&mut vec![]), viewport, ); - state.update_text_input(tree); } }); - if started_focused - && !self.state.is_focused() - && !published_message_to_shell - { + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::<text_input::State<Renderer::Paragraph>>(); + + text_input_state.is_focused() + }; + + if started_focused && !is_focused && !published_message_to_shell { if let Some(message) = self.on_close.take() { shell.publish(message); } } // Focus changed, invalidate widget tree to force a fresh `view` - if started_focused != self.state.is_focused() { + if started_focused != is_focused { shell.invalidate_widgets(); } @@ -625,20 +584,24 @@ where fn mouse_interaction( &self, - _tree: &widget::Tree, + tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let tree = self.state.text_input_tree(); - self.text_input - .mouse_interaction(&tree, layout, cursor, viewport, renderer) + self.text_input.mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) } fn draw( &self, - _tree: &widget::Tree, + tree: &widget::Tree, renderer: &mut Renderer, theme: &Renderer::Theme, _style: &renderer::Style, @@ -646,16 +609,28 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let selection = if self.state.is_focused() || self.selection.is_empty() - { + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::<text_input::State<Renderer::Paragraph>>(); + + text_input_state.is_focused() + }; + + let selection = if is_focused || self.selection.is_empty() { None } else { Some(&self.selection) }; - let tree = self.state.text_input_tree(); - self.text_input - .draw(&tree, renderer, theme, layout, cursor, selection); + self.text_input.draw( + &tree.children[0], + renderer, + theme, + layout, + cursor, + selection, + ); } fn overlay<'b>( @@ -664,14 +639,22 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - let Menu { - menu, - filtered_options, - hovered_option, - .. - } = tree.state.downcast_mut::<Menu<T>>(); + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::<text_input::State<Renderer::Paragraph>>(); + + text_input_state.is_focused() + }; + + if is_focused { + let Menu { + menu, + filtered_options, + hovered_option, + .. + } = tree.state.downcast_mut::<Menu<T>>(); - if self.state.is_focused() { let bounds = layout.bounds(); self.state.sync_filtered_options(filtered_options); diff --git a/widget/src/container.rs b/widget/src/container.rs index 1f1df861..c16c1117 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -5,7 +5,8 @@ use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::{self, Operation, Tree}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::{self, Operation}; use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Vector, Widget, @@ -135,12 +136,20 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + fn children(&self) -> Vec<Tree> { - vec![Tree::new(&self.content)] + self.content.as_widget().children() } fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + self.content.as_widget().diff(tree); } fn width(&self) -> Length { @@ -153,11 +162,11 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { layout( - renderer, limits, self.width, self.height, @@ -166,9 +175,7 @@ where self.padding, self.horizontal_alignment, self.vertical_alignment, - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, + |limits| self.content.as_widget().layout(tree, renderer, limits), ) } @@ -184,7 +191,7 @@ where layout.bounds(), &mut |operation| { self.content.as_widget().operate( - &mut tree.children[0], + tree, layout.children().next().unwrap(), renderer, operation, @@ -205,7 +212,7 @@ where viewport: &Rectangle, ) -> event::Status { self.content.as_widget_mut().on_event( - &mut tree.children[0], + tree, event, layout.children().next().unwrap(), cursor, @@ -225,7 +232,7 @@ where renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( - &tree.children[0], + tree, layout.children().next().unwrap(), cursor, viewport, @@ -248,7 +255,7 @@ where draw_background(renderer, &style, layout.bounds()); self.content.as_widget().draw( - &tree.children[0], + tree, renderer, theme, &renderer::Style { @@ -269,7 +276,7 @@ where renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { self.content.as_widget_mut().overlay( - &mut tree.children[0], + tree, layout.children().next().unwrap(), renderer, ) @@ -291,8 +298,7 @@ where } /// Computes the layout of a [`Container`]. -pub fn layout<Renderer>( - renderer: &Renderer, +pub fn layout( limits: &layout::Limits, width: Length, height: Length, @@ -301,7 +307,7 @@ pub fn layout<Renderer>( padding: Padding, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, + layout_content: impl FnOnce(&layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits .loose() @@ -310,7 +316,7 @@ pub fn layout<Renderer>( .width(width) .height(height); - let mut content = layout_content(renderer, &limits.pad(padding).loose()); + let mut content = layout_content(&limits.pad(padding).loose()); let padding = padding.fit(content.size(), limits.max()); let size = limits.pad(padding).resolve(content.size()); diff --git a/widget/src/image.rs b/widget/src/image.rs index 66bf2156..f73ee5d7 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -167,6 +167,7 @@ where fn layout( &self, + _tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 6e095667..1f52bf2f 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -105,6 +105,7 @@ where fn layout( &self, + _tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 761f45ad..412254f5 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -152,11 +152,12 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.with_element(|element| { - element.as_widget().layout(renderer, limits) + element.as_widget().layout(tree, renderer, limits) }) } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 19df2792..9b3b13b2 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -254,11 +254,12 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.with_element(|element| { - element.as_widget().layout(renderer, limits) + element.as_widget().layout(tree, renderer, limits) }) } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index b56545c8..5ab8ed1a 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -60,13 +60,13 @@ impl<'a, Message, Renderer> Content<'a, Message, Renderer> where Renderer: core::Renderer, { - fn layout(&mut self, renderer: &Renderer) { + fn layout(&mut self, tree: &Tree, renderer: &Renderer) { if self.layout.is_none() { - self.layout = - Some(self.element.as_widget().layout( - renderer, - &layout::Limits::new(Size::ZERO, self.size), - )); + self.layout = Some(self.element.as_widget().layout( + tree, + renderer, + &layout::Limits::new(Size::ZERO, self.size), + )); } } @@ -104,7 +104,7 @@ where R: Deref<Target = Renderer>, { self.update(tree, layout.bounds().size(), view); - self.layout(renderer.deref()); + self.layout(tree, renderer.deref()); let content_layout = Layout::with_offset( layout.position() - Point::ORIGIN, @@ -144,6 +144,7 @@ where fn layout( &self, + _tree: &Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -285,7 +286,7 @@ where overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>, tree| { content.update(tree, layout.bounds().size(), &self.view); - content.layout(renderer); + content.layout(tree, renderer); let Content { element, diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 490f7c48..95b45b02 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -120,10 +120,11 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.content.as_widget().layout(renderer, limits) + self.content.as_widget().layout(tree, renderer, limits) } fn operate( diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index f7bdeef6..71703e71 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -31,7 +31,7 @@ where on_option_hovered: Option<&'a dyn Fn(T) -> Message>, width: f32, padding: Padding, - text_size: Option<f32>, + text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, @@ -85,7 +85,7 @@ where /// Sets the text size of the [`Menu`]. pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { - self.text_size = Some(text_size.into().0); + self.text_size = Some(text_size.into()); self } @@ -253,7 +253,7 @@ where ) .width(self.width); - let mut node = self.container.layout(renderer, &limits); + let mut node = self.container.layout(self.state, renderer, &limits); node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) @@ -328,7 +328,7 @@ where on_selected: Box<dyn FnMut(T) -> Message + 'a>, on_option_hovered: Option<&'a dyn Fn(T) -> Message>, padding: Padding, - text_size: Option<f32>, + text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, @@ -352,6 +352,7 @@ where fn layout( &self, + _tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -361,8 +362,7 @@ where let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); - let text_line_height = - self.text_line_height.to_absolute(Pixels(text_size)); + let text_line_height = self.text_line_height.to_absolute(text_size); let size = { let intrinsic = Size::new( @@ -407,9 +407,9 @@ where .text_size .unwrap_or_else(|| renderer.default_size()); - let option_height = f32::from( - self.text_line_height.to_absolute(Pixels(text_size)), - ) + self.padding.vertical(); + let option_height = + f32::from(self.text_line_height.to_absolute(text_size)) + + self.padding.vertical(); let new_hovered_option = (cursor_position.y / option_height) as usize; @@ -436,9 +436,9 @@ where .text_size .unwrap_or_else(|| renderer.default_size()); - let option_height = f32::from( - self.text_line_height.to_absolute(Pixels(text_size)), - ) + self.padding.vertical(); + let option_height = + f32::from(self.text_line_height.to_absolute(text_size)) + + self.padding.vertical(); *self.hovered_option = Some((cursor_position.y / option_height) as usize); @@ -490,7 +490,7 @@ where let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); let option_height = - f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + f32::from(self.text_line_height.to_absolute(text_size)) + self.padding.vertical(); let offset = viewport.y - bounds.y; @@ -526,26 +526,24 @@ where ); } - renderer.fill_text(Text { - content: &option.to_string(), - bounds: Rectangle { - x: bounds.x + self.padding.left, - y: bounds.center_y(), - width: f32::INFINITY, - ..bounds + renderer.fill_text( + Text { + content: &option.to_string(), + bounds: Size::new(f32::INFINITY, bounds.height), + size: text_size, + line_height: self.text_line_height, + font: self.font.unwrap_or_else(|| renderer.default_font()), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: self.text_shaping, }, - size: text_size, - line_height: self.text_line_height, - font: self.font.unwrap_or_else(|| renderer.default_font()), - color: if is_selected { + Point::new(bounds.x + self.padding.left, bounds.center_y()), + if is_selected { appearance.selected_text_color } else { appearance.text_color }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: self.text_shaping, - }); + ); } } } diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index d8c98858..366d9a66 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -275,10 +275,12 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { layout( + tree, renderer, limits, self.contents.layout(), @@ -286,7 +288,9 @@ where self.height, self.spacing, self.contents.iter(), - |content, renderer, limits| content.layout(renderer, limits), + |content, tree, renderer, limits| { + content.layout(tree, renderer, limits) + }, ) } @@ -471,6 +475,7 @@ where /// Calculates the [`Layout`] of a [`PaneGrid`]. pub fn layout<Renderer, T>( + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, node: &Node, @@ -478,19 +483,21 @@ pub fn layout<Renderer, T>( height: Length, spacing: f32, contents: impl Iterator<Item = (Pane, T)>, - layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, + layout_content: impl Fn(T, &Tree, &Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits.width(width).height(height); let size = limits.resolve(Size::ZERO); let regions = node.pane_regions(spacing, size); let children = contents - .filter_map(|(pane, content)| { + .zip(tree.children.iter()) + .filter_map(|((pane, content), tree)| { let region = regions.get(&pane)?; let size = Size::new(region.width, region.height); let mut node = layout_content( content, + tree, renderer, &layout::Limits::new(size, size), ); diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index e890e41a..8a74b4b9 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -150,18 +150,23 @@ where pub(crate) fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { if let Some(title_bar) = &self.title_bar { let max_size = limits.max(); - let title_bar_layout = title_bar - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + let title_bar_layout = title_bar.layout( + &tree.children[1], + renderer, + &layout::Limits::new(Size::ZERO, max_size), + ); let title_bar_size = title_bar_layout.size(); let mut body_layout = self.body.as_widget().layout( + &tree.children[0], renderer, &layout::Limits::new( Size::ZERO, @@ -179,7 +184,9 @@ where vec![title_bar_layout, body_layout], ) } else { - self.body.as_widget().layout(renderer, limits) + self.body + .as_widget() + .layout(&tree.children[0], renderer, limits) } } diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index cac24e68..c0fb9936 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -213,23 +213,27 @@ where pub(crate) fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let limits = limits.pad(self.padding); let max_size = limits.max(); - let title_layout = self - .content - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + let title_layout = self.content.as_widget().layout( + &tree.children[0], + renderer, + &layout::Limits::new(Size::ZERO, max_size), + ); let title_size = title_layout.size(); let mut node = if let Some(controls) = &self.controls { - let mut controls_layout = controls - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + let mut controls_layout = controls.as_widget().layout( + &tree.children[1], + renderer, + &layout::Limits::new(Size::ZERO, max_size), + ); let controls_size = controls_layout.size(); let space_before_controls = max_size.width - controls_size.width; diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 0a1e2a99..719aa066 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -7,17 +7,18 @@ use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::text::{self, Text}; +use crate::core::text::{self, Paragraph as _, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, - Size, Widget, + Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, + Shell, Size, Widget, }; use crate::overlay::menu::{self, Menu}; use crate::scrollable; use std::borrow::Cow; +use std::cell::RefCell; pub use crate::style::pick_list::{Appearance, StyleSheet}; @@ -35,7 +36,7 @@ where selected: Option<T>, width: Length, padding: Padding, - text_size: Option<f32>, + text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, @@ -101,7 +102,7 @@ where /// Sets the text size of the [`PickList`]. pub fn text_size(mut self, size: impl Into<Pixels>) -> Self { - self.text_size = Some(size.into().0); + self.text_size = Some(size.into()); self } @@ -157,11 +158,11 @@ where From<<Renderer::Theme as StyleSheet>::Style>, { fn tag(&self) -> tree::Tag { - tree::Tag::of::<State>() + tree::Tag::of::<State<Renderer::Paragraph>>() } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::<Renderer::Paragraph>::new()) } fn width(&self) -> Length { @@ -174,10 +175,12 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { layout( + tree.state.downcast_ref::<State<Renderer::Paragraph>>(), renderer, limits, self.width, @@ -210,7 +213,7 @@ where self.on_selected.as_ref(), self.selected.as_ref(), &self.options, - || tree.state.downcast_mut::<State>(), + || tree.state.downcast_mut::<State<Renderer::Paragraph>>(), ) } @@ -250,7 +253,7 @@ where self.selected.as_ref(), &self.handle, &self.style, - || tree.state.downcast_ref::<State>(), + || tree.state.downcast_ref::<State<Renderer::Paragraph>>(), ) } @@ -260,7 +263,7 @@ where layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - let state = tree.state.downcast_mut::<State>(); + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); overlay( layout, @@ -295,28 +298,32 @@ where } } -/// The local state of a [`PickList`]. +/// The state of a [`PickList`]. #[derive(Debug)] -pub struct State { +pub struct State<P: text::Paragraph> { menu: menu::State, keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, + option_paragraphs: RefCell<Vec<P>>, + placeholder_paragraph: RefCell<P>, } -impl State { +impl<P: text::Paragraph> State<P> { /// Creates a new [`State`] for a [`PickList`]. - pub fn new() -> Self { + fn new() -> Self { Self { menu: menu::State::default(), keyboard_modifiers: keyboard::Modifiers::default(), is_open: bool::default(), hovered_option: Option::default(), + option_paragraphs: RefCell::new(Vec::new()), + placeholder_paragraph: RefCell::new(Default::default()), } } } -impl Default for State { +impl<P: text::Paragraph> Default for State<P> { fn default() -> Self { Self::new() } @@ -330,7 +337,7 @@ pub enum Handle<Font> { /// This is the default. Arrow { /// Font size of the content. - size: Option<f32>, + size: Option<Pixels>, }, /// A custom static handle. Static(Icon<Font>), @@ -359,7 +366,7 @@ pub struct Icon<Font> { /// The unicode code point that will be used as the icon. pub code_point: char, /// Font size of the content. - pub size: Option<f32>, + pub size: Option<Pixels>, /// Line height of the content. pub line_height: text::LineHeight, /// The shaping strategy of the icon. @@ -368,11 +375,12 @@ pub struct Icon<Font> { /// Computes the layout of a [`PickList`]. pub fn layout<Renderer, T>( + state: &State<Renderer::Paragraph>, renderer: &Renderer, limits: &layout::Limits, width: Length, padding: Padding, - text_size: Option<f32>, + text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, @@ -386,38 +394,70 @@ where use std::f32; let limits = limits.width(width).height(Length::Shrink).pad(padding); + let font = font.unwrap_or_else(|| renderer.default_font()); let text_size = text_size.unwrap_or_else(|| renderer.default_size()); - let max_width = match width { - Length::Shrink => { - let measure = |label: &str| -> f32 { - let width = renderer.measure_width( - label, - text_size, - font.unwrap_or_else(|| renderer.default_font()), - text_shaping, - ); - - width.round() - }; + let mut paragraphs = state.option_paragraphs.borrow_mut(); + + paragraphs.resize_with(options.len(), Default::default); + + let option_text = Text { + content: "", + bounds: Size::new( + f32::INFINITY, + text_line_height.to_absolute(text_size).into(), + ), + size: text_size, + line_height: text_line_height, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text_shaping, + }; - let labels = options.iter().map(ToString::to_string); + for (option, paragraph) in options.iter().zip(paragraphs.iter_mut()) { + let label = option.to_string(); - let labels_width = labels - .map(|label| measure(&label)) - .fold(100.0, |candidate, current| current.max(candidate)); + renderer.update_paragraph( + paragraph, + Text { + content: &label, + ..option_text + }, + ); + } - let placeholder_width = placeholder.map(measure).unwrap_or(100.0); + if let Some(placeholder) = placeholder { + let mut paragraph = state.placeholder_paragraph.borrow_mut(); + renderer.update_paragraph( + &mut paragraph, + Text { + content: placeholder, + ..option_text + }, + ); + } - labels_width.max(placeholder_width) + let max_width = match width { + Length::Shrink => { + let labels_width = + paragraphs.iter().fold(0.0, |width, paragraph| { + f32::max(width, paragraph.min_width()) + }); + + labels_width.max( + placeholder + .map(|_| state.placeholder_paragraph.borrow().min_width()) + .unwrap_or(0.0), + ) } _ => 0.0, }; let size = { let intrinsic = Size::new( - max_width + text_size + padding.left, - f32::from(text_line_height.to_absolute(Pixels(text_size))), + max_width + text_size.0 + padding.left, + f32::from(text_line_height.to_absolute(text_size)), ); limits.resolve(intrinsic).pad(padding) @@ -428,7 +468,7 @@ where /// Processes an [`Event`] and updates the [`State`] of a [`PickList`] /// accordingly. -pub fn update<'a, T, Message>( +pub fn update<'a, T, P, Message>( event: Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -436,10 +476,11 @@ pub fn update<'a, T, Message>( on_selected: &dyn Fn(T) -> Message, selected: Option<&T>, options: &[T], - state: impl FnOnce() -> &'a mut State, + state: impl FnOnce() -> &'a mut State<P>, ) -> event::Status where T: PartialEq + Clone + 'a, + P: text::Paragraph + 'a, { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) @@ -534,9 +575,9 @@ pub fn mouse_interaction( /// Returns the current overlay of a [`PickList`]. pub fn overlay<'a, T, Message, Renderer>( layout: Layout<'_>, - state: &'a mut State, + state: &'a mut State<Renderer::Paragraph>, padding: Padding, - text_size: Option<f32>, + text_size: Option<Pixels>, text_shaping: text::Shaping, font: Renderer::Font, options: &'a [T], @@ -591,7 +632,7 @@ pub fn draw<'a, T, Renderer>( layout: Layout<'_>, cursor: mouse::Cursor, padding: Padding, - text_size: Option<f32>, + text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Renderer::Font, @@ -599,7 +640,7 @@ pub fn draw<'a, T, Renderer>( selected: Option<&T>, handle: &Handle<Renderer::Font>, style: &<Renderer::Theme as StyleSheet>::Style, - state: impl FnOnce() -> &'a State, + state: impl FnOnce() -> &'a State<Renderer::Paragraph>, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -665,22 +706,26 @@ pub fn draw<'a, T, Renderer>( if let Some((font, code_point, size, line_height, shaping)) = handle { let size = size.unwrap_or_else(|| renderer.default_size()); - renderer.fill_text(Text { - content: &code_point.to_string(), - size, - line_height, - font, - color: style.handle_color, - bounds: Rectangle { - x: bounds.x + bounds.width - padding.horizontal(), - y: bounds.center_y(), - height: f32::from(line_height.to_absolute(Pixels(size))), - ..bounds + renderer.fill_text( + Text { + content: &code_point.to_string(), + size, + line_height, + font, + bounds: Size::new( + bounds.width, + f32::from(line_height.to_absolute(size)), + ), + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + shaping, }, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - shaping, - }); + Point::new( + bounds.x + bounds.width - padding.horizontal(), + bounds.center_y(), + ), + style.handle_color, + ); } let label = selected.map(ToString::to_string); @@ -688,27 +733,26 @@ pub fn draw<'a, T, Renderer>( if let Some(label) = label.as_deref().or(placeholder) { let text_size = text_size.unwrap_or_else(|| renderer.default_size()); - renderer.fill_text(Text { - content: label, - size: text_size, - line_height: text_line_height, - font, - color: if is_selected { + renderer.fill_text( + Text { + content: label, + size: text_size, + line_height: text_line_height, + font, + bounds: Size::new( + bounds.width - padding.horizontal(), + f32::from(text_line_height.to_absolute(text_size)), + ), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text_shaping, + }, + Point::new(bounds.x + padding.left, bounds.center_y()), + if is_selected { style.text_color } else { style.placeholder_color }, - bounds: Rectangle { - x: bounds.x + padding.left, - y: bounds.center_y(), - width: bounds.width - padding.horizontal(), - height: f32::from( - text_line_height.to_absolute(Pixels(text_size)), - ), - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: text_shaping, - }); + ); } } diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 37c6bc72..8b1490af 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -95,6 +95,7 @@ where fn layout( &self, + _tree: &Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 51a541fd..589704a5 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -60,6 +60,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { fn layout( &self, + _tree: &Tree, _renderer: &Renderer<Theme>, _limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 65d71ec2..cb908ec4 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -6,12 +6,12 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text; use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, - Shell, Widget, + Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, Shell, Size, + Widget, }; -use crate::{Row, Text}; pub use iced_style::radio::{Appearance, StyleSheet}; @@ -80,7 +80,7 @@ where width: Length, size: f32, spacing: f32, - text_size: Option<f32>, + text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, @@ -152,7 +152,7 @@ where /// Sets the text size of the [`Radio`] button. pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { - self.text_size = Some(text_size.into().0); + self.text_size = Some(text_size.into()); self } @@ -193,6 +193,14 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + crate::text::StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<widget::text::State<Renderer::Paragraph>>() + } + + fn state(&self) -> tree::State { + tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) + } + fn width(&self) -> Length { self.width } @@ -203,25 +211,35 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - Row::<(), Renderer>::new() - .width(self.width) - .spacing(self.spacing) - .align_items(Alignment::Center) - .push(Row::new().width(self.size).height(self.size)) - .push( - Text::new(&self.label) - .width(self.width) - .size( - self.text_size - .unwrap_or_else(|| renderer.default_size()), - ) - .line_height(self.text_line_height) - .shaping(self.text_shaping), - ) - .layout(renderer, limits) + layout::next_to_each_other( + &limits.width(self.width), + self.spacing, + |_| layout::Node::new(Size::new(self.size, self.size)), + |limits| { + let state = tree + .state + .downcast_ref::<widget::text::State<Renderer::Paragraph>>(); + + widget::text::layout( + state, + renderer, + limits, + self.width, + Length::Shrink, + &self.label, + self.text_line_height, + self.text_size, + self.font, + alignment::Horizontal::Left, + alignment::Vertical::Top, + self.text_shaping, + ) + }, + ) } fn on_event( @@ -267,7 +285,7 @@ where fn draw( &self, - _state: &Tree, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -327,16 +345,10 @@ where renderer, style, label_layout, - &self.label, - self.text_size, - self.text_line_height, - self.font, + tree.state.downcast_ref(), crate::text::Appearance { color: custom_style.text_color, }, - alignment::Horizontal::Left, - alignment::Vertical::Center, - self.text_shaping, ); } } diff --git a/widget/src/row.rs b/widget/src/row.rs index 99b2a0bf..17c49e67 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -114,6 +114,7 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -127,6 +128,7 @@ where self.spacing, self.align_items, &self.children, + &tree.children, ) } diff --git a/widget/src/rule.rs b/widget/src/rule.rs index d703e6ae..032ff860 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -72,6 +72,7 @@ where fn layout( &self, + _tree: &Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index a83ed985..ce96883d 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -230,6 +230,7 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -240,7 +241,11 @@ where self.height, &self.direction, |renderer, limits| { - self.content.as_widget().layout(renderer, limits) + self.content.as_widget().layout( + &tree.children[0], + renderer, + limits, + ) }, ) } diff --git a/widget/src/slider.rs b/widget/src/slider.rs index e41be7c9..b4c9198a 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -169,6 +169,7 @@ where fn layout( &self, + _tree: &Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/space.rs b/widget/src/space.rs index 9a5385e8..84331870 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -55,6 +55,7 @@ where fn layout( &self, + _tree: &Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 1ccc5d62..f61a4ce2 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -106,6 +106,7 @@ where fn layout( &self, + _tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 61fc0055..209ef968 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -17,7 +17,7 @@ use crate::core::keyboard; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; -use crate::core::text::{self, Text}; +use crate::core::text::{self, Paragraph as _, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; use crate::core::widget; @@ -30,6 +30,8 @@ use crate::core::{ }; use crate::runtime::Command; +use std::cell::RefCell; + pub use iced_style::text_input::{Appearance, StyleSheet}; /// A field that can be filled with text. @@ -67,7 +69,7 @@ where font: Option<Renderer::Font>, width: Length, padding: Padding, - size: Option<f32>, + size: Option<Pixels>, line_height: text::LineHeight, on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, @@ -178,7 +180,7 @@ where /// Sets the text size of the [`TextInput`]. pub fn size(mut self, size: impl Into<Pixels>) -> Self { - self.size = Some(size.into().0); + self.size = Some(size.into()); self } @@ -218,12 +220,8 @@ where theme, layout, cursor, - tree.state.downcast_ref::<State>(), + tree.state.downcast_ref::<State<Renderer::Paragraph>>(), value.unwrap_or(&self.value), - &self.placeholder, - self.size, - self.line_height, - self.font, self.on_input.is_none(), self.is_secure, self.icon.as_ref(), @@ -240,15 +238,15 @@ where Renderer::Theme: StyleSheet, { fn tag(&self) -> tree::Tag { - tree::Tag::of::<State>() + tree::Tag::of::<State<Renderer::Paragraph>>() } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::<Renderer::Paragraph>::new()) } fn diff(&self, tree: &mut Tree) { - let state = tree.state.downcast_mut::<State>(); + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); // Unfocus text input if it becomes disabled if self.on_input.is_none() { @@ -269,6 +267,7 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { @@ -278,8 +277,13 @@ where self.width, self.padding, self.size, + self.font, self.line_height, self.icon.as_ref(), + tree.state.downcast_ref::<State<Renderer::Paragraph>>(), + &self.value, + &self.placeholder, + self.is_secure, ) } @@ -290,7 +294,7 @@ where _renderer: &Renderer, operation: &mut dyn Operation<Message>, ) { - let state = tree.state.downcast_mut::<State>(); + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); operation.focusable(state, self.id.as_ref().map(|id| &id.0)); operation.text_input(state, self.id.as_ref().map(|id| &id.0)); @@ -322,7 +326,7 @@ where self.on_input.as_deref(), self.on_paste.as_deref(), &self.on_submit, - || tree.state.downcast_mut::<State>(), + || tree.state.downcast_mut::<State<Renderer::Paragraph>>(), ) } @@ -341,12 +345,8 @@ where theme, layout, cursor, - tree.state.downcast_ref::<State>(), + tree.state.downcast_ref::<State<Renderer::Paragraph>>(), &self.value, - &self.placeholder, - self.size, - self.line_height, - self.font, self.on_input.is_none(), self.is_secure, self.icon.as_ref(), @@ -388,7 +388,7 @@ pub struct Icon<Font> { /// The unicode code point that will be used as the icon. pub code_point: char, /// The font size of the content. - pub size: Option<f32>, + pub size: Option<Pixels>, /// The spacing between the [`Icon`] and the text in a [`TextInput`]. pub spacing: f32, /// The side of a [`TextInput`] where to display the [`Icon`]. @@ -465,29 +465,65 @@ pub fn layout<Renderer>( limits: &layout::Limits, width: Length, padding: Padding, - size: Option<f32>, + size: Option<Pixels>, + font: Option<Renderer::Font>, line_height: text::LineHeight, icon: Option<&Icon<Renderer::Font>>, + state: &State<Renderer::Paragraph>, + value: &Value, + placeholder: &str, + is_secure: bool, ) -> layout::Node where Renderer: text::Renderer, { + let font = font.unwrap_or_else(|| renderer.default_font()); let text_size = size.unwrap_or_else(|| renderer.default_size()); + let padding = padding.fit(Size::ZERO, limits.max()); let limits = limits .width(width) .pad(padding) - .height(line_height.to_absolute(Pixels(text_size))); + .height(line_height.to_absolute(text_size)); let text_bounds = limits.resolve(Size::ZERO); - if let Some(icon) = icon { - let icon_width = renderer.measure_width( - &icon.code_point.to_string(), - icon.size.unwrap_or_else(|| renderer.default_size()), - icon.font, - text::Shaping::Advanced, + let placeholder_text = Text { + font, + line_height, + content: placeholder, + bounds: Size::new(f32::INFINITY, text_bounds.height), + size: text_size, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }; + + renderer.update_paragraph( + &mut state.placeholder_paragraph.borrow_mut(), + placeholder_text, + ); + + if is_secure { + renderer.update_paragraph( + &mut state.paragraph.borrow_mut(), + Text { + content: &value.secure().to_string(), + ..placeholder_text + }, + ); + } else { + renderer.update_paragraph( + &mut state.paragraph.borrow_mut(), + Text { + content: &value.to_string(), + ..placeholder_text + }, ); + } + + if let Some(icon) = icon { + let icon_width = 0.0; // TODO let mut text_node = layout::Node::new( text_bounds - Size::new(icon_width + icon.spacing, 0.0), @@ -537,19 +573,31 @@ pub fn update<'a, Message, Renderer>( clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, value: &mut Value, - size: Option<f32>, + size: Option<Pixels>, line_height: text::LineHeight, font: Option<Renderer::Font>, is_secure: bool, on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, on_submit: &Option<Message>, - state: impl FnOnce() -> &'a mut State, + state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>, ) -> event::Status where Message: Clone, Renderer: text::Renderer, { + let update_cache = |state, value| { + replace_paragraph( + renderer, + state, + layout, + value, + font, + size, + line_height, + ) + }; + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -592,11 +640,7 @@ where }; find_cursor_position( - renderer, text_layout.bounds(), - font, - size, - line_height, &value, state, target, @@ -621,11 +665,7 @@ where state.cursor.select_all(value); } else { let position = find_cursor_position( - renderer, text_layout.bounds(), - font, - size, - line_height, value, state, target, @@ -671,11 +711,7 @@ where }; let position = find_cursor_position( - renderer, text_layout.bounds(), - font, - size, - line_height, &value, state, target, @@ -710,6 +746,8 @@ where focus.updated_at = Instant::now(); + update_cache(state, value); + return event::Status::Captured; } } @@ -749,6 +787,8 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + + update_cache(state, value); } keyboard::KeyCode::Delete => { if platform::is_jump_modifier_pressed(modifiers) @@ -769,6 +809,8 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + + update_cache(state, value); } keyboard::KeyCode::Left => { if platform::is_jump_modifier_pressed(modifiers) @@ -844,6 +886,8 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + + update_cache(state, value); } keyboard::KeyCode::V => { if state.keyboard_modifiers.command() @@ -876,6 +920,8 @@ where shell.publish(message); state.is_pasting = Some(content); + + update_cache(state, value); } else { state.is_pasting = None; } @@ -979,12 +1025,8 @@ pub fn draw<Renderer>( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - state: &State, + state: &State<Renderer::Paragraph>, value: &Value, - placeholder: &str, - size: Option<f32>, - line_height: text::LineHeight, - font: Option<Renderer::Font>, is_disabled: bool, is_secure: bool, icon: Option<&Icon<Renderer::Font>>, @@ -1023,28 +1065,14 @@ pub fn draw<Renderer>( appearance.background, ); - if let Some(icon) = icon { - let icon_layout = children_layout.next().unwrap(); + if let Some(_icon) = icon { + let _icon_layout = children_layout.next().unwrap(); - renderer.fill_text(Text { - content: &icon.code_point.to_string(), - size: icon.size.unwrap_or_else(|| renderer.default_size()), - line_height: text::LineHeight::default(), - font: icon.font, - color: appearance.icon_color, - bounds: Rectangle { - y: text_bounds.center_y(), - ..icon_layout.bounds() - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - }); + // TODO } let text = value.to_string(); - let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size()); + let paragraph = &state.paragraph.borrow() as &Renderer::Paragraph; let (cursor, offset) = if let Some(focus) = state .is_focused @@ -1055,12 +1083,9 @@ pub fn draw<Renderer>( cursor::State::Index(position) => { let (text_value_width, offset) = measure_cursor_and_scroll_offset( - renderer, + paragraph, text_bounds, - value, - size, position, - font, ); let is_cursor_visible = ((focus.now - focus.updated_at) @@ -1096,22 +1121,16 @@ pub fn draw<Renderer>( let (left_position, left_offset) = measure_cursor_and_scroll_offset( - renderer, + paragraph, text_bounds, - value, - size, left, - font, ); let (right_position, right_offset) = measure_cursor_and_scroll_offset( - renderer, + paragraph, text_bounds, - value, - size, right, - font, ); let width = right_position - left_position; @@ -1143,12 +1162,7 @@ pub fn draw<Renderer>( (None, 0.0) }; - let text_width = renderer.measure_width( - if text.is_empty() { placeholder } else { &text }, - size, - font, - text::Shaping::Advanced, - ); + let text_width = paragraph.min_width(); let render = |renderer: &mut Renderer| { if let Some((cursor, color)) = cursor { @@ -1157,27 +1171,23 @@ pub fn draw<Renderer>( renderer.with_translation(Vector::ZERO, |_| {}); } - renderer.fill_text(Text { - content: if text.is_empty() { placeholder } else { &text }, - color: if text.is_empty() { + let placeholder_paragraph = state.placeholder_paragraph.borrow(); + + renderer.fill_paragraph( + if text.is_empty() { + &placeholder_paragraph + } else { + paragraph + }, + Point::new(text_bounds.x, text_bounds.center_y()), + if text.is_empty() { theme.placeholder_color(style) } else if is_disabled { theme.disabled_color(style) } else { theme.value_color(style) }, - font, - bounds: Rectangle { - y: text_bounds.center_y(), - width: f32::INFINITY, - ..text_bounds - }, - size, - line_height, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - }); + ); }; if text_width > text_bounds.width { @@ -1208,7 +1218,9 @@ pub fn mouse_interaction( /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] -pub struct State { +pub struct State<P: text::Paragraph> { + paragraph: RefCell<P>, + placeholder_paragraph: RefCell<P>, is_focused: Option<Focus>, is_dragging: bool, is_pasting: Option<Value>, @@ -1225,7 +1237,7 @@ struct Focus { is_window_focused: bool, } -impl State { +impl<P: text::Paragraph> State<P> { /// Creates a new [`State`], representing an unfocused [`TextInput`]. pub fn new() -> Self { Self::default() @@ -1234,6 +1246,8 @@ impl State { /// Creates a new [`State`], representing a focused [`TextInput`]. pub fn focused() -> Self { Self { + paragraph: RefCell::new(P::default()), + placeholder_paragraph: RefCell::new(P::default()), is_focused: None, is_dragging: false, is_pasting: None, @@ -1292,7 +1306,7 @@ impl State { } } -impl operation::Focusable for State { +impl<P: text::Paragraph> operation::Focusable for State<P> { fn is_focused(&self) -> bool { State::is_focused(self) } @@ -1306,7 +1320,7 @@ impl operation::Focusable for State { } } -impl operation::TextInput for State { +impl<P: text::Paragraph> operation::TextInput for State<P> { fn move_cursor_to_front(&mut self) { State::move_cursor_to_front(self) } @@ -1336,17 +1350,11 @@ mod platform { } } -fn offset<Renderer>( - renderer: &Renderer, +fn offset<P: text::Paragraph>( text_bounds: Rectangle, - font: Renderer::Font, - size: f32, value: &Value, - state: &State, -) -> f32 -where - Renderer: text::Renderer, -{ + state: &State<P>, +) -> f32 { if state.is_focused() { let cursor = state.cursor(); @@ -1356,12 +1364,9 @@ where }; let (_, offset) = measure_cursor_and_scroll_offset( - renderer, + &state.paragraph.borrow() as &P, text_bounds, - value, - size, focus_position, - font, ); offset @@ -1370,63 +1375,35 @@ where } } -fn measure_cursor_and_scroll_offset<Renderer>( - renderer: &Renderer, +fn measure_cursor_and_scroll_offset( + paragraph: &impl text::Paragraph, text_bounds: Rectangle, - value: &Value, - size: f32, cursor_index: usize, - font: Renderer::Font, -) -> (f32, f32) -where - Renderer: text::Renderer, -{ - let text_before_cursor = value.until(cursor_index).to_string(); +) -> (f32, f32) { + let grapheme_position = paragraph + .grapheme_position(0, cursor_index) + .unwrap_or(Point::ORIGIN); - let text_value_width = renderer.measure_width( - &text_before_cursor, - size, - font, - text::Shaping::Advanced, - ); + let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0); - let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); - - (text_value_width, offset) + (grapheme_position.x, offset) } /// Computes the position of the text cursor at the given X coordinate of /// a [`TextInput`]. -fn find_cursor_position<Renderer>( - renderer: &Renderer, +fn find_cursor_position<P: text::Paragraph>( text_bounds: Rectangle, - font: Option<Renderer::Font>, - size: Option<f32>, - line_height: text::LineHeight, value: &Value, - state: &State, + state: &State<P>, x: f32, -) -> Option<usize> -where - Renderer: text::Renderer, -{ - let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size()); - - let offset = offset(renderer, text_bounds, font, size, value, state); +) -> Option<usize> { + let offset = offset(text_bounds, value, state); let value = value.to_string(); - let char_offset = renderer - .hit_test( - &value, - size, - line_height, - font, - Size::INFINITY, - text::Shaping::Advanced, - Point::new(x + offset, text_bounds.height / 2.0), - true, - ) + let char_offset = state + .paragraph + .borrow() + .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) .map(text::Hit::cursor)?; Some( @@ -1438,4 +1415,33 @@ where ) } +fn replace_paragraph<Renderer>( + renderer: &Renderer, + state: &mut State<Renderer::Paragraph>, + layout: Layout<'_>, + value: &Value, + font: Option<Renderer::Font>, + text_size: Option<Pixels>, + line_height: text::LineHeight, +) where + Renderer: text::Renderer, +{ + let font = font.unwrap_or_else(|| renderer.default_font()); + let text_size = text_size.unwrap_or_else(|| renderer.default_size()); + + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); + + *state.paragraph.get_mut() = renderer.create_paragraph(Text { + font, + line_height, + content: &value.to_string(), + bounds: Size::new(f32::INFINITY, text_bounds.height), + size: text_size, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); +} + const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index c8187181..4ddc8db6 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -6,12 +6,12 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text; use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, - Shell, Widget, + Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, + Widget, }; -use crate::{Row, Text}; pub use crate::style::toggler::{Appearance, StyleSheet}; @@ -42,7 +42,7 @@ where label: Option<String>, width: Length, size: f32, - text_size: Option<f32>, + text_size: Option<Pixels>, text_line_height: text::LineHeight, text_alignment: alignment::Horizontal, text_shaping: text::Shaping, @@ -105,7 +105,7 @@ where /// Sets the text size o the [`Toggler`]. pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { - self.text_size = Some(text_size.into().0); + self.text_size = Some(text_size.into()); self } @@ -160,6 +160,14 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + crate::text::StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<widget::text::State<Renderer::Paragraph>>() + } + + fn state(&self) -> tree::State { + tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) + } + fn width(&self) -> Length { self.width } @@ -170,32 +178,41 @@ where fn layout( &self, + tree: &Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let mut row = Row::<(), Renderer>::new() - .width(self.width) - .spacing(self.spacing) - .align_items(Alignment::Center); - - if let Some(label) = &self.label { - row = row.push( - Text::new(label) - .horizontal_alignment(self.text_alignment) - .font(self.font.unwrap_or_else(|| renderer.default_font())) - .width(self.width) - .size( - self.text_size - .unwrap_or_else(|| renderer.default_size()), + let limits = limits.width(self.width); + + layout::next_to_each_other( + &limits, + self.spacing, + |_| layout::Node::new(Size::new(2.0 * self.size, self.size)), + |limits| { + if let Some(label) = self.label.as_deref() { + let state = tree + .state + .downcast_ref::<widget::text::State<Renderer::Paragraph>>(); + + widget::text::layout( + state, + renderer, + limits, + self.width, + Length::Shrink, + label, + self.text_line_height, + self.text_size, + self.font, + self.text_alignment, + alignment::Vertical::Top, + self.text_shaping, ) - .line_height(self.text_line_height) - .shaping(self.text_shaping), - ); - } - - row = row.push(Row::new().width(2.0 * self.size).height(self.size)); - - row.layout(renderer, limits) + } else { + layout::Node::new(Size::ZERO) + } + }, + ) } fn on_event( @@ -243,7 +260,7 @@ where fn draw( &self, - _state: &Tree, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -259,28 +276,21 @@ where const SPACE_RATIO: f32 = 0.05; let mut children = layout.children(); + let toggler_layout = children.next().unwrap(); - if let Some(label) = &self.label { + if self.label.is_some() { let label_layout = children.next().unwrap(); crate::text::draw( renderer, style, label_layout, - label, - self.text_size, - self.text_line_height, - self.font, + tree.state.downcast_ref(), Default::default(), - self.text_alignment, - alignment::Vertical::Center, - self.text_shaping, ); } - let toggler_layout = children.next().unwrap(); let bounds = toggler_layout.bounds(); - let is_mouse_over = cursor.is_over(layout.bounds()); let style = if is_mouse_over { diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index faa3f3e1..0444850e 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -107,11 +107,14 @@ where Renderer::Theme: container::StyleSheet + crate::text::StyleSheet, { fn children(&self) -> Vec<widget::Tree> { - vec![widget::Tree::new(&self.content)] + vec![ + widget::Tree::new(&self.content), + widget::Tree::new(&self.tooltip as &dyn Widget<Message, _>), + ] } fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + tree.diff_children(&[self.content.as_widget(), &self.tooltip]) } fn state(&self) -> widget::tree::State { @@ -132,10 +135,11 @@ where fn layout( &self, + tree: &widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.content.as_widget().layout(renderer, limits) + self.content.as_widget().layout(tree, renderer, limits) } fn on_event( @@ -214,8 +218,10 @@ where ) -> Option<overlay::Element<'b, Message, Renderer>> { let state = tree.state.downcast_ref::<State>(); + let mut children = tree.children.iter_mut(); + let content = self.content.as_widget_mut().overlay( - &mut tree.children[0], + children.next().unwrap(), layout, renderer, ); @@ -225,6 +231,7 @@ where layout.position(), Box::new(Overlay { tooltip: &self.tooltip, + state: children.next().unwrap(), cursor_position, content_bounds: layout.bounds(), snap_within_viewport: self.snap_within_viewport, @@ -295,6 +302,7 @@ where Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, { tooltip: &'b Text<'a, Renderer>, + state: &'b widget::Tree, cursor_position: Point, content_bounds: Rectangle, snap_within_viewport: bool, @@ -320,6 +328,7 @@ where let text_layout = Widget::<(), Renderer>::layout( self.tooltip, + self.state, renderer, &layout::Limits::new( Size::ZERO, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index efca302a..a11fec75 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -166,6 +166,7 @@ where fn layout( &self, + _tree: &Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { |