From 92023835963f17c01b370d751fb54a3ad9117d99 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 14:01:42 -0500 Subject: [PATCH] chore: update to 0.12 --- Cargo.toml | 2 - cosmic-config/src/settings_daemon.rs | 0 examples/cosmic/Cargo.toml | 1 + examples/cosmic/src/window.rs | 5 +- examples/cosmic/src/window/demo.rs | 4 +- iced | 2 +- src/app/cosmic.rs | 3 +- src/app/mod.rs | 2 +- src/app/settings.rs | 2 +- src/font.rs | 10 +- src/keyboard_nav.rs | 3 +- src/lib.rs | 1 + src/theme/style/iced.rs | 17 - src/widget/aspect_ratio.rs | 10 +- src/widget/button/widget.rs | 13 +- src/widget/color_picker/mod.rs | 11 +- src/widget/context_drawer/overlay.rs | 12 +- src/widget/context_drawer/widget.rs | 11 +- src/widget/cosmic_container.rs | 13 +- src/widget/dropdown/menu/mod.rs | 42 +- src/widget/dropdown/widget.rs | 132 +++-- src/widget/flex_row/layout.rs | 6 +- src/widget/flex_row/widget.rs | 8 +- src/widget/grid/layout.rs | 16 +- src/widget/grid/widget.rs | 8 +- src/widget/menu/flex.rs | 14 +- src/widget/menu/menu_bar.rs | 4 +- src/widget/menu/menu_inner.rs | 74 ++- src/widget/popover.rs | 18 +- src/widget/rectangle_tracker/mod.rs | 8 +- src/widget/segmented_button/horizontal.rs | 11 +- src/widget/segmented_button/vertical.rs | 11 +- src/widget/segmented_button/widget.rs | 115 +++-- src/widget/text_input/input.rs | 577 +++++++++++----------- 34 files changed, 712 insertions(+), 454 deletions(-) create mode 100644 cosmic-config/src/settings_daemon.rs diff --git a/Cargo.toml b/Cargo.toml index 962c14f1..aeb86e1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,5 +143,3 @@ exclude = ["examples/design-demo", "iced"] [patch."https://github.com/pop-os/libcosmic"] libcosmic = { path = "./" } -# [patch."https://github.com/pop-os/cosmic-time"] -# cosmic-time = { path = "../cosmic-time" } diff --git a/cosmic-config/src/settings_daemon.rs b/cosmic-config/src/settings_daemon.rs new file mode 100644 index 00000000..e69de29b diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index a4c3bf3e..fdc939b9 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -17,4 +17,5 @@ log = "0.4.17" [dependencies.cosmic-time] git = "https://github.com/pop-os/cosmic-time" default-features = false +branch = "update-0.12" features = ["libcosmic", "once_cell"] \ No newline at end of file diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 023dadf7..c8e67484 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -12,6 +12,7 @@ use cosmic::{ widget::{self, column, container, horizontal_space, row, text}, window::{self, close, drag, minimize, toggle_maximize}, }, + iced_futures::event::listen_raw, keyboard_nav, prelude::*, theme::{self, Theme}, @@ -358,7 +359,7 @@ impl Application for Window { } fn subscription(&self) -> Subscription { - let window_break = subscription::events_with(|event, _| match event { + let window_break = listen_raw(|event, _| match event { cosmic::iced::Event::Window( _window_id, window::Event::Resized { width, height: _ }, @@ -450,7 +451,7 @@ impl Application for Window { _ => (), }, Message::ToggleWarning => self.toggle_warning(), - Message::FontsLoaded => {} + Message::FontsLoaded => {} // Message::Tick(instant) => self.timeline.borrow_mut().now(instant), Message::Tick(instant) => self.timeline.borrow_mut().now(instant), Message::Tick(instant) => self.timeline.borrow_mut().now(instant), } ret diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 5a484f09..824b243d 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -4,7 +4,8 @@ use apply::Apply; use cosmic::{ cosmic_theme, iced::widget::{checkbox, column, progress_bar, radio, slider, text, text_input}, - iced::{id, Alignment, Length}, + iced::{Alignment, Length}, + iced_core::id, theme::ThemeType, widget::{ button, color_picker::ColorPickerUpdate, cosmic_container::container, dropdown, icon, @@ -498,7 +499,6 @@ impl State { .on_input(Message::InputChanged) .into(), cosmic::widget::search_input("test", &self.entry_value) - .on_clear(Message::InputChanged("".to_string())) .width(Length::Fixed(100.0)) .on_input(Message::InputChanged) .into(), diff --git a/iced b/iced index b3ede4f9..9af6bbb5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b3ede4f9a72275cfeb29fac80a31546f728783fd +Subproject commit 9af6bbb55c3443a21b835b01f20b2c0032cb50bc diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index f621c792..33042725 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -13,6 +13,7 @@ use iced::event::wayland::{self, WindowEvent}; #[cfg(feature = "wayland")] use iced::event::PlatformSpecific; use iced::window; +use iced_futures::event::listen_raw; #[cfg(not(feature = "wayland"))] use iced_runtime::command::Action; #[cfg(not(feature = "wayland"))] @@ -126,7 +127,7 @@ where } fn subscription(&self) -> Subscription { - let window_events = iced::subscription::events_with(|event, _| { + let window_events = listen_raw(|event, _| { match event { iced::Event::Window(id, window::Event::Resized { width, height }) => { return Some(Message::WindowResize(id, width, height)); diff --git a/src/app/mod.rs b/src/app/mod.rs index 328fa9c6..f775aba1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -82,7 +82,7 @@ pub(crate) fn iced_settings( iced.antialiasing = settings.antialiasing; iced.default_font = settings.default_font; - iced.default_text_size = settings.default_text_size; + iced.default_text_size = iced::Pixels(settings.default_text_size); iced.exit_on_close_request = settings.exit_on_close; iced.id = Some(App::APP_ID.to_owned()); diff --git a/src/app/settings.rs b/src/app/settings.rs index 10ed4dd3..21d649f7 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -5,7 +5,7 @@ use crate::{font, Theme}; #[cfg(feature = "wayland")] -use iced::Limits; +use iced_core::layout::Limits; use iced_core::Font; /// Configure a new COSMIC application. diff --git a/src/font.rs b/src/font.rs index 8f1be266..926ef14d 100644 --- a/src/font.rs +++ b/src/font.rs @@ -16,7 +16,7 @@ pub const FONT: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Normal, stretch: iced_core::font::Stretch::Normal, - monospaced: false, + style: iced_core::font::Style::Normal, }; pub const FONT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Regular.otf"); @@ -25,7 +25,7 @@ pub const FONT_LIGHT: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Light, stretch: iced_core::font::Stretch::Normal, - monospaced: false, + style: iced_core::font::Style::Normal, }; pub const FONT_LIGHT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Light.otf"); @@ -34,7 +34,7 @@ pub const FONT_SEMIBOLD: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Semibold, stretch: iced_core::font::Stretch::Normal, - monospaced: false, + style: iced_core::font::Style::Normal, }; pub const FONT_SEMIBOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-SemiBold.otf"); @@ -43,7 +43,7 @@ pub const FONT_BOLD: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Bold, stretch: iced_core::font::Stretch::Normal, - monospaced: false, + style: iced_core::font::Style::Normal, }; pub const FONT_BOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Bold.otf"); @@ -52,7 +52,7 @@ pub const FONT_MONO_REGULAR: Font = Font { family: Family::Name("Fira Mono"), weight: iced_core::font::Weight::Normal, stretch: iced_core::font::Stretch::Normal, - monospaced: true, + style: iced_core::font::Style::Normal, }; pub const FONT_MONO_REGULAR_DATA: &[u8] = include_bytes!("../res/Fira/FiraMono-Regular.otf"); diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index 40294a07..1f3e7ffd 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -12,6 +12,7 @@ use iced_core::{ widget::{operation, Id, Operation}, Rectangle, }; +use iced_futures::event::listen_raw; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Message { @@ -24,7 +25,7 @@ pub enum Message { } pub fn subscription() -> Subscription { - subscription::events_with(|event, status| { + listen_raw(|event, status| { if event::Status::Ignored != status { return None; } diff --git a/src/lib.rs b/src/lib.rs index 6a65e18d..b00ad829 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,5 +66,6 @@ pub use theme::{style, Theme}; pub mod widget; +type Paragraph = ::Paragraph; pub type Renderer = iced::Renderer; pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index d4868618..27635a5a 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -146,23 +146,6 @@ impl iced_button::StyleSheet for Theme { } } - fn focused(&self, style: &Self::Style) -> iced_button::Appearance { - if let Button::Custom { hover, .. } = style { - return hover(self); - } - - let active = self.active(style); - let component = style.cosmic(self); - iced_button::Appearance { - background: match style { - Button::Link => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.hover.into())), - }, - ..active - } - } - fn disabled(&self, style: &Self::Style) -> iced_button::Appearance { let active = self.active(style); diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 076abdb6..471cfaaf 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -161,12 +161,18 @@ where Widget::height(&self.container) } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { let custom_limits = layout::Limits::new( self.constrain_limits(limits.min()), self.constrain_limits(limits.max()), ); - self.container.layout(renderer, &custom_limits) + self.container + .layout(&mut tree.children[0], renderer, &custom_limits) } fn operate( diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 59799399..37cb58f6 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -244,14 +244,23 @@ where self.height } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { layout( renderer, limits, self.width, self.height, self.padding, - |renderer, limits| self.content.as_widget().layout(renderer, limits), + |renderer, limits| { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + }, ) } diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 327b0b7f..f9d116b9 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -515,8 +515,15 @@ where Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { - self.inner.as_widget().layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.inner + .as_widget() + .layout(&mut tree.children[0], renderer, limits) } #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 3c84cde9..f2a2bc79 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -20,12 +20,20 @@ impl<'a, 'b, Message> overlay::Overlay for Overlay<'a, where Message: Clone, { - fn layout(&self, renderer: &crate::Renderer, bounds: Size, position: Point) -> layout::Node { + fn layout( + &mut self, + renderer: &crate::Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { let limits = layout::Limits::new(Size::ZERO, bounds) .width(self.width) .height(bounds.height - 8.0 - position.y); - let mut node = self.content.as_widget().layout(renderer, &limits); + let mut node = + self.content + .as_widget() + .layout(&mut self.tree.children[0], renderer, &limits); let node_size = node.size(); node.move_to(Point { diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index c0cd941b..dee04a86 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -122,8 +122,15 @@ impl<'a, Message: Clone> Widget for ContextDrawer<'a, Message self.content.as_widget().height() } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.content.as_widget().layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) } fn operate( diff --git a/src/widget/cosmic_container.rs b/src/widget/cosmic_container.rs index 7d170dfd..4f9c7b78 100644 --- a/src/widget/cosmic_container.rs +++ b/src/widget/cosmic_container.rs @@ -144,6 +144,10 @@ where self.container.diff(tree); } + fn state(&self) -> iced_core::widget::tree::State { + self.container.state() + } + fn width(&self) -> Length { Widget::width(&self.container) } @@ -152,8 +156,13 @@ where Widget::height(&self.container) } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.container.layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.container.layout(tree, renderer, limits) } fn operate( diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 4797adb1..589ff295 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -187,7 +187,12 @@ impl<'a, Message: 'a> Overlay<'a, Message> { } impl<'a, Message> iced_core::Overlay for Overlay<'a, Message> { - fn layout(&self, renderer: &crate::Renderer, bounds: Size, position: Point) -> layout::Node { + fn layout( + &mut self, + renderer: &crate::Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; @@ -204,7 +209,7 @@ impl<'a, Message> iced_core::Overlay for Overlay<'a, M ) .width(self.width); - let mut node = self.container.layout(renderer, &limits); + let mut node = self.container.layout(&mut self.state, renderer, &limits); node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) @@ -289,13 +294,18 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + _tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { use std::f32; let limits = limits.width(Length::Fill).height(Length::Shrink); let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let text_line_height = self.text_line_height.to_absolute(Pixels(text_size)); @@ -335,7 +345,7 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S if let Some(cursor_position) = cursor.position_in(layout.bounds()) { let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) @@ -356,7 +366,7 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S if let Some(cursor_position) = cursor.position_in(layout.bounds()) { let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) @@ -408,7 +418,7 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + self.padding.vertical(); @@ -484,24 +494,26 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S (appearance.text_color, crate::font::FONT) }; + let bounds = Rectangle { + x: bounds.x + self.padding.left, + y: bounds.center_y(), + width: f32::INFINITY, + ..bounds + }; text::Renderer::fill_text( renderer, Text { content: option.as_ref(), - bounds: Rectangle { - x: bounds.x + self.padding.left, - y: bounds.center_y(), - width: f32::INFINITY, - ..bounds - }, - size: text_size, + bounds: bounds.size(), + size: Pixels(text_size), line_height: self.text_line_height, font, - color, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, }, + bounds.position(), + color, ); } } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index fbcf7dcb..632eaf56 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -6,7 +6,7 @@ use super::menu::{self, Menu}; use crate::widget::icon; use derive_setters::Setters; use iced_core::event::{self, Event}; -use iced_core::text::{self, Text}; +use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{alignment, keyboard, layout, mouse, overlay, renderer, svg, touch}; use iced_core::{Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Widget}; @@ -61,6 +61,28 @@ impl<'a, S: AsRef, Message> Dropdown<'a, S, Message> { font: None, } } + + fn update_paragraphs(&self, state: &mut tree::State) { + let state = state.downcast_mut::(); + + state + .selections + .resize_with(self.selections.len(), crate::Paragraph::new); + for (i, selection) in self.selections.iter().enumerate() { + state.selections[i].update(Text { + content: selection.as_ref(), + bounds: Size::INFINITY, + // TODO use the renderer default size + size: iced::Pixels(self.text_size.unwrap_or(14.0)), + + line_height: self.text_line_height, + font: self.font.unwrap_or(crate::font::FONT), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + } + } } impl<'a, S: AsRef, Message: 'a> Widget for Dropdown<'a, S, Message> { @@ -69,7 +91,31 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo } fn state(&self) -> tree::State { - tree::State::new(State::new()) + let mut tree = tree::State::new(State::new()); + self.update_paragraphs(&mut tree); + tree + } + + fn diff(&mut self, tree: &mut Tree) { + let state = tree.state.downcast_mut::(); + + state + .selections + .resize_with(self.selections.len(), crate::Paragraph::new); + for (i, selection) in self.selections.iter().enumerate() { + state.selections[i].update(Text { + content: selection.as_ref(), + bounds: Size::INFINITY, + // TODO use the renderer default size + size: iced::Pixels(self.text_size.unwrap_or(14.0)), + + line_height: self.text_line_height, + font: self.font.unwrap_or(crate::font::FONT), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + } } fn width(&self) -> Length { @@ -80,7 +126,12 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { layout( renderer, limits, @@ -90,9 +141,12 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo self.text_size.unwrap_or(14.0), self.text_line_height, self.font, - self.selected - .and_then(|id| self.selections.get(id)) - .map(AsRef::as_ref), + self.selected.and_then(|id| { + self.selections + .get(id) + .map(AsRef::as_ref) + .zip(tree.state.downcast_mut::().selections.get_mut(id)) + }), ) } @@ -173,6 +227,8 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo self.gap, self.padding, self.text_size.unwrap_or(14.0), + self.text_line_height, + self.font, self.selections, self.selected, &self.on_selected, @@ -196,6 +252,7 @@ pub struct State { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option, + selections: Vec, } impl State { @@ -214,6 +271,7 @@ impl State { keyboard_modifiers: keyboard::Modifiers::default(), is_open: false, hovered_option: None, + selections: Vec::new(), } } } @@ -235,7 +293,7 @@ pub fn layout( text_size: f32, text_line_height: text::LineHeight, font: Option, - selection: Option<&str>, + selection: Option<(&str, &mut crate::Paragraph)>, ) -> layout::Node { use std::f32; @@ -243,16 +301,18 @@ pub fn layout( let max_width = match width { Length::Shrink => { - let measure = |label: &str| -> f32 { - let width = text::Renderer::measure_width( - renderer, - label, - text_size, - font.unwrap_or_else(|| text::Renderer::default_font(renderer)), - text::Shaping::Advanced, - ); - - width.round() + let measure = move |(label, paragraph): (_, &mut crate::Paragraph)| -> f32 { + paragraph.update(Text { + content: label, + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size), + line_height: text_line_height, + font: font.unwrap_or_else(|| text::Renderer::default_font(renderer)), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + paragraph.min_width().round() }; selection.map(measure).unwrap_or_default() @@ -358,6 +418,8 @@ pub fn overlay<'a, S: AsRef, Message: 'a>( gap: f32, padding: Padding, text_size: f32, + text_line_height: text::LineHeight, + font: Option, selections: &'a [S], selected_option: Option, on_selected: &'a dyn Fn(usize) -> Message, @@ -378,21 +440,14 @@ pub fn overlay<'a, S: AsRef, Message: 'a>( None, ) .width({ - let measure = |label: &str| -> f32 { - let width = text::Renderer::measure_width( - renderer, - label, - text_size, - crate::font::FONT, - text::Shaping::Advanced, - ); - - width.round() + let measure = |label: &str, selection_paragraph: &mut crate::Paragraph| -> f32 { + selection_paragraph.min_width().round() }; selections .iter() - .map(|label| measure(label.as_ref())) + .zip(state.selections.iter_mut()) + .map(|(label, selection)| measure(label.as_ref(), selection)) .fold(0.0, |next, current| current.max(next)) + gap + 16.0 @@ -462,26 +517,27 @@ pub fn draw<'a, S>( } if let Some(content) = selected.map(AsRef::as_ref) { - let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer)); - + let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0); + let 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))), + }; text::Renderer::fill_text( renderer, Text { content, - size: text_size, + size: iced::Pixels(text_size), line_height: text_line_height, font, - color: style.text_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))), - }, + bounds: bounds.size(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, }, + bounds.position(), + style.text_color, ); } } diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index b1b75b28..ba4de64e 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -3,6 +3,7 @@ use crate::{Element, Renderer}; use iced_core::layout::{Limits, Node}; +use iced_core::widget::Tree; use iced_core::{Padding, Point, Size}; pub fn resolve( @@ -12,6 +13,7 @@ pub fn resolve( padding: Padding, column_spacing: f32, row_spacing: f32, + tree: &mut [Tree], ) -> Node { let limits = limits.pad(padding); @@ -26,9 +28,9 @@ pub fn resolve( let mut row_buffer = Vec::::with_capacity(8); - for child in items { + for (child, tree) in items.iter().zip(tree.into_iter()) { // Calculate the dimensions of the item. - let child_node = child.as_widget().layout(renderer, &limits); + let child_node = child.as_widget().layout(tree, renderer, &limits); let size = child_node.size(); // Calculate the required additional width to fit the item into the current row. diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 667b44b0..e969bfb8 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -58,7 +58,12 @@ impl<'a, Message: 'static + Clone> Widget for FlexRow<'a, Mes Length::Shrink } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { let limits = limits .max_width(self.max_width) .width(self.width()) @@ -71,6 +76,7 @@ impl<'a, Message: 'static + Clone> Widget for FlexRow<'a, Mes self.padding, f32::from(self.column_spacing), f32::from(self.row_spacing), + &mut tree.children, ) } diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index 79f6efcd..f746e88b 100644 --- a/src/widget/grid/layout.rs +++ b/src/widget/grid/layout.rs @@ -4,6 +4,7 @@ use super::widget::Assignment; use crate::{Element, Renderer}; use iced_core::layout::{Limits, Node}; +use iced_core::widget::Tree; use iced_core::{Alignment, Length, Padding, Point, Size}; use taffy::geometry::{Line, Rect}; @@ -24,6 +25,7 @@ pub fn resolve( row_alignment: Alignment, column_spacing: f32, row_spacing: f32, + tree: &mut [Tree], ) -> Node { let max_size = limits.max(); @@ -33,10 +35,10 @@ pub fn resolve( let mut taffy = TaffyTree::<()>::with_capacity(items.len() + 1); // Attach widgets as child nodes. - for (child, assignment) in items.iter().zip(assignments.iter()) { + for ((child, assignment), tree) in items.iter().zip(assignments.iter()).zip(tree.iter_mut()) { // Calculate the dimensions of the item. let child_widget = child.as_widget(); - let child_node = child_widget.layout(renderer, limits); + let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); nodes.push(child_node); @@ -155,12 +157,18 @@ pub fn resolve( } }; - for (leaf, (child, node)) in leafs.into_iter().zip(items.iter().zip(nodes.iter_mut())) { + for (((leaf, child), node), tree) in leafs + .into_iter() + .zip(items.iter()) + .zip(nodes.iter_mut()) + .zip(tree) + { if let Ok(leaf_layout) = taffy.layout(leaf) { let child_widget = child.as_widget(); match child_widget.width() { Length::Fill | Length::FillPortion(_) => { - *node = child_widget.layout(renderer, &limits.width(leaf_layout.size.width)); + *node = + child_widget.layout(tree, renderer, &limits.width(leaf_layout.size.width)); } _ => (), } diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index 9a8a70c6..782738fb 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -120,7 +120,12 @@ impl<'a, Message: 'static + Clone> Widget for Grid<'a, Messag self.height } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { let limits = limits .max_width(self.max_width) .width(self.width()) @@ -138,6 +143,7 @@ impl<'a, Message: 'static + Clone> Widget for Grid<'a, Messag self.row_alignment, f32::from(self.column_spacing), f32::from(self.row_spacing), + &mut tree.children, ) } diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index ecff8dab..17c7e104 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -1,5 +1,6 @@ // From iced_aw, license MIT +use iced_core::widget::Tree; use iced_widget::core::{ layout::{Limits, Node}, renderer, Alignment, Element, Padding, Point, Size, @@ -54,6 +55,7 @@ pub fn resolve<'a, E, Message, Renderer>( spacing: f32, align_items: Alignment, items: &[E], + tree: &mut [Tree], ) -> Node where E: std::borrow::Borrow>, @@ -73,7 +75,7 @@ where if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for child in items { + for (child, tree) in items.iter().zip(tree.iter_mut()) { let child = child.borrow(); let cross_fill_factor = match axis { Axis::Horizontal => child.as_widget().height(), @@ -86,7 +88,7 @@ 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(); fill_cross = fill_cross.max(axis.cross(size)); @@ -96,7 +98,7 @@ where cross = fill_cross; } - for (i, child) in items.iter().enumerate() { + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { let child = child.borrow(); let fill_factor = match axis { Axis::Horizontal => child.as_widget().width(), @@ -122,7 +124,7 @@ 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); let size = layout.size(); available -= axis.main(size); @@ -139,7 +141,7 @@ where let remaining = available.max(0.0); - for (i, child) in items.iter().enumerate() { + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { let child = child.borrow(); let fill_factor = match axis { Axis::Horizontal => child.as_widget().width(), @@ -172,7 +174,7 @@ 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); if align_items != Alignment::Center { cross = cross.max(axis.cross(layout.size())); diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 308b142e..a2bcdd3d 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -279,7 +279,7 @@ where .collect() } - fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { + fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { use super::flex; let limits = limits.width(self.width).height(self.height); @@ -296,6 +296,8 @@ where self.spacing, Alignment::Center, &children, + // the children of the tree are the menu roots + &mut tree.children, ) } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 63629ae4..3608cf33 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -252,8 +252,13 @@ impl MenuBounds { where Renderer: renderer::Renderer, { - let (children_size, child_positions, child_sizes) = - get_children_layout(menu_tree, renderer, item_width, item_height); + let (children_size, child_positions, child_sizes) = get_children_layout( + menu_tree, + renderer, + item_width, + item_height, + &mut Tree::new(&menu_tree.item), + ); // viewport space parent bounds let view_parent_bounds = parent_bounds + overlay_offset; @@ -299,6 +304,7 @@ impl MenuState { slice: MenuSlice, renderer: &Renderer, menu_tree: &MenuTree<'_, Message, Renderer>, + tree: &mut [Tree], ) -> Node where Renderer: renderer::Renderer, @@ -322,7 +328,8 @@ impl MenuState { .iter() .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) .zip(menu_tree.children[start_index..=end_index].iter()) - .map(|((cp, size), mt)| { + .zip(tree[start_index..=end_index].iter_mut()) + .map(|(((cp, size), mt), tree)| { let mut position = *cp; let mut size = *size; @@ -336,7 +343,7 @@ impl MenuState { let limits = Limits::new(Size::ZERO, size); - let mut node = mt.item.as_widget().layout(renderer, &limits); + let mut node = mt.item.as_widget().layout(tree, renderer, &limits); node.move_to(Point::new(0.0, position + self.scroll_offset)); node }) @@ -353,6 +360,7 @@ impl MenuState { index: usize, renderer: &Renderer, menu_tree: &MenuTree<'_, Message, Renderer>, + tree: &mut Tree, ) -> Node where Renderer: renderer::Renderer, @@ -363,7 +371,7 @@ impl MenuState { let position = self.menu_bounds.child_positions[index]; let limits = Limits::new(Size::ZERO, self.menu_bounds.child_sizes[index]); let parent_offset = children_bounds.position() - Point::ORIGIN; - let mut node = menu_tree.item.as_widget().layout(renderer, &limits); + let mut node = menu_tree.item.as_widget().layout(tree, renderer, &limits); node.move_to(Point::new( parent_offset.x, parent_offset.y + position + self.scroll_offset, @@ -458,9 +466,44 @@ where Renderer: renderer::Renderer, Renderer::Theme: StyleSheet, { - fn layout(&self, _renderer: &Renderer, bounds: Size, position: Point) -> Node { + fn layout(&mut self, renderer: &Renderer, bounds: Size, position: Point) -> Node { + // layout children + let state = self.tree.state.downcast_mut::(); + let overlay_offset = Point::ORIGIN - position; + let tree_children = &mut self.tree.children; + let children = state + .active_root + .map(|active_root| { + let root = &self.menu_roots[active_root]; + let active_tree = &mut tree_children[active_root]; + state.menu_states.iter().enumerate().fold( + (root, Vec::new()), + |(menu_root, mut nodes), (i, ms)| { + let slice = ms.slice(bounds, overlay_offset, self.item_height); + let start_index = slice.start_index; + let end_index = slice.end_index; + let children_node = ms.layout( + overlay_offset, + slice, + renderer, + menu_root, + &mut active_tree.children[start_index..=end_index], + ); + nodes.push(children_node); + // only the last menu can have a None active index + ( + ms.index + .map_or(menu_root, |active| &menu_root.children[active]), + nodes, + ) + }, + ) + }) + .map(|(_, l)| l) + .unwrap_or_default(); + // overlay space viewport rectangle - Node::new(bounds).translate(Point::ORIGIN - position) + Node::with_children(bounds, children).translate(Point::ORIGIN - position) } fn on_event( @@ -610,8 +653,9 @@ where state .menu_states .iter() + .zip(layout.children()) .enumerate() - .fold(root, |menu_root, (i, ms)| { + .fold(root, |menu_root, (i, (ms, children_layout))| { let draw_path = self.path_highlight.as_ref().map_or(false, |ph| match ph { PathHighlight::Full => true, PathHighlight::OmitActive => !indices.is_empty() && i < indices.len() - 1, @@ -631,9 +675,6 @@ where let start_index = slice.start_index; let end_index = slice.end_index; - // calc layout - let children_node = ms.layout(overlay_offset, slice, r, menu_root); - let children_layout = Layout::new(&children_node); let children_bounds = children_layout.bounds(); // draw menu background @@ -808,11 +849,17 @@ where // get layout let last_ms = &state.menu_states[indices.len() - 1]; + let last_tree = tree.children[active_root] + .children + .iter_mut() + .last() + .unwrap(); let child_node = last_ms.layout_single( overlay_offset, last_ms.index.expect("missing index within menu state."), renderer, mt, + last_tree, ); let child_layout = Layout::new(&child_node); @@ -1108,6 +1155,7 @@ fn get_children_layout( renderer: &Renderer, item_width: ItemWidth, item_height: ItemHeight, + tree: &mut Tree, ) -> (Size, Vec, Vec) where Renderer: renderer::Renderer, @@ -1130,13 +1178,15 @@ where ItemHeight::Dynamic(d) => menu_tree .children .iter() - .map(|mt| { + .zip(tree.children.iter_mut()) + .map(|(mt, tree)| { let w = mt.item.as_widget(); match w.height() { Length::Fixed(f) => Size::new(width, f), Length::Shrink => { let l_height = w .layout( + tree, renderer, &Limits::new(Size::ZERO, Size::new(width, f32::MAX)), ) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index b0871917..41a6e5c9 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -71,8 +71,14 @@ where self.content.as_widget().height() } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.content.as_widget().layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let tree = &mut tree.children[0]; + self.content.as_widget().layout(tree, renderer, limits) } fn operate( @@ -203,9 +209,13 @@ impl<'a, 'b, Message, Renderer> overlay::Overlay where Renderer: iced_core::Renderer, { - fn layout(&self, renderer: &Renderer, bounds: Size, mut position: Point) -> layout::Node { + fn layout(&mut self, renderer: &Renderer, bounds: Size, mut position: Point) -> layout::Node { let limits = layout::Limits::new(Size::UNIT, bounds); - let mut node = self.content.borrow().as_widget().layout(renderer, &limits); + let mut node = self + .content + .borrow() + .as_widget() + .layout(self.tree, renderer, &limits); if self.centered { // Position is set to the center bottom of the lower widget let width = node.size().width; diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 507e1d81..41563a95 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -195,8 +195,14 @@ where Widget::height(&self.container) } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { self.container.layout( + tree, renderer, if self.ignore_bounds { &layout::Limits::NONE diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 03d696b2..8fa28c7c 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -5,7 +5,7 @@ use super::model::{Model, Selectable}; use super::style::StyleSheet; -use super::widget::{SegmentedButton, SegmentedVariant}; +use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; @@ -61,9 +61,14 @@ where #[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] - fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn variant_layout( + &self, + state: &mut LocalState, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { let limits = limits.width(self.width); - let (mut width, height) = self.max_button_dimensions(renderer, limits.max()); + let (mut width, height) = self.max_button_dimensions(state, renderer, limits.max()); let num = self.model.items.len(); let spacing = f32::from(self.spacing); diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 7a5f8cf3..827f6cf4 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -5,7 +5,7 @@ use super::model::{Model, Selectable}; use super::style::StyleSheet; -use super::widget::{SegmentedButton, SegmentedVariant}; +use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; @@ -62,9 +62,14 @@ where #[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] - fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn variant_layout( + &self, + state: &mut LocalState, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { let limits = limits.width(self.width); - let (width, mut height) = self.max_button_dimensions(renderer, limits.max()); + let (width, mut height) = self.max_button_dimensions(state, renderer, limits.max()); let num = self.model.items.len(); let spacing = f32::from(self.spacing); diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index bb22a516..ae4fd78d 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -10,15 +10,16 @@ use iced::{ alignment, event, keyboard, mouse, touch, Background, Color, Command, Event, Length, Rectangle, Size, }; -use iced_core::text::{LineHeight, Renderer as TextRenderer, Shaping}; +use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping}; use iced_core::widget::{self, operation, tree}; use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; -use iced_core::{BorderRadius, Point, Renderer as IcedRenderer}; +use iced_core::{BorderRadius, Point, Renderer as IcedRenderer, Text}; +use slotmap::SecondaryMap; use std::marker::PhantomData; /// State that is maintained by each individual widget. #[derive(Default)] -struct LocalState { +pub struct LocalState { /// The first focusable key. first: Entity, /// If the widget is focused or not. @@ -27,6 +28,8 @@ struct LocalState { focused_key: Entity, /// The ID of the button that is being hovered. Defaults to null. hovered: Entity, + /// The paragraphs for each text. + paragraphs: SecondaryMap, } impl operation::Focusable for LocalState { @@ -57,7 +60,12 @@ pub trait SegmentedVariant { fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle; /// Calculates the layout of this variant. - fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node; + fn variant_layout( + &self, + state: &mut LocalState, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node; } /// A conjoined group of items that function together as a button. @@ -214,7 +222,12 @@ where event::Status::Ignored } - pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) { + pub(super) fn max_button_dimensions( + &self, + state: &mut LocalState, + renderer: &Renderer, + bounds: Size, + ) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; let font = renderer.default_font(); @@ -224,15 +237,21 @@ where let mut button_height = 0.0f32; // Add text to measurement if text was given. - if let Some(text) = self.model.text(key) { - let Size { width, height } = renderer.measure( - text, - self.font_size, - self.line_height, - font, - bounds, - Shaping::Advanced, - ); + if let Some((text, entry)) = self.model.text.get(key).zip(state.paragraphs.entry(key)) { + let paragraph = entry.or_insert_with(|| { + crate::Paragraph::with_text(Text { + content: text, + size: iced::Pixels(self.font_size), + bounds: Size::INFINITY, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }) + }); + + let Size { width, height } = paragraph.min_bounds(); button_width = width; button_height = height; @@ -282,12 +301,44 @@ where } fn state(&self) -> tree::State { + // update the paragraphs for the model tree::State::new(LocalState { first: self.model.order.iter().copied().next().unwrap_or_default(), + paragraphs: SecondaryMap::new(), ..LocalState::default() }) } + fn diff(&mut self, tree: &mut Tree) { + for e in self.model.order.iter().copied() { + if let Some(text) = self.model.text.get(e) { + let text = Text { + content: text, + size: iced::Pixels(self.font_size), + bounds: Size::INFINITY, + font: self.font_active.unwrap_or(crate::font::FONT), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }; + if let Some(paragraph) = tree + .state + .downcast_mut::() + .paragraphs + .get_mut(e) + { + paragraph.update(text); + } else { + tree.state + .downcast_mut::() + .paragraphs + .insert(e, crate::Paragraph::with_text(text)); + } + } + } + } + fn width(&self) -> Length { self.width } @@ -296,8 +347,13 @@ where self.height } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.variant_layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.variant_layout(tree.state.downcast_mut::(), renderer, limits) } fn on_event( @@ -548,7 +604,7 @@ where }); Widget::::draw( - &Element::::from(icon.clone()), + Element::::from(icon.clone()).as_widget(), &Tree::empty(), renderer, theme, @@ -575,17 +631,20 @@ where bounds.y = y; // Draw the text in this button. - renderer.fill_text(iced_core::text::Text { - content: text, - size: self.font_size, - bounds, - color: status_appearance.text_color, - font, - horizontal_alignment, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - line_height: self.line_height, - }); + renderer.fill_text( + iced_core::text::Text { + content: text, + size: iced::Pixels(self.font_size), + bounds: bounds.size(), + font, + horizontal_alignment, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }, + bounds.position(), + status_appearance.text_color, + ); } let show_close_button = diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 8930a55d..03286cba 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -5,6 +5,8 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. +use std::borrow::Cow; + use crate::theme::THEME; use super::cursor; @@ -20,7 +22,7 @@ use iced_core::keyboard; use iced_core::mouse::{self, click}; use iced_core::overlay::Group; use iced_core::renderer::{self, Renderer as CoreRenderer}; -use iced_core::text::{self, Renderer, Text}; +use iced_core::text::{self, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; use iced_core::touch; use iced_core::widget::operation::{self, Operation}; @@ -48,7 +50,10 @@ use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMim /// Creates a new [`TextInput`]. /// /// [`TextInput`]: widget::TextInput -pub fn text_input<'a, Message>(placeholder: &str, value: &str) -> TextInput<'a, Message> +pub fn text_input<'a, Message>( + placeholder: impl Into>, + value: impl Into>, +) -> TextInput<'a, Message> where Message: Clone + 'static, { @@ -58,7 +63,10 @@ where /// Creates a new search [`TextInput`]. /// /// [`TextInput`]: widget::TextInput -pub fn search_input<'a, Message>(placeholder: &str, value: &str) -> TextInput<'a, Message> +pub fn search_input<'a, Message>( + placeholder: impl Into>, + value: impl Into>, +) -> TextInput<'a, Message> where Message: Clone + 'static, { @@ -79,8 +87,8 @@ where /// /// [`TextInput`]: widget::TextInput pub fn secure_input<'a, Message>( - placeholder: &str, - value: &str, + placeholder: impl Into>, + value: impl Into>, on_visible_toggle: Option, hidden: bool, ) -> TextInput<'a, Message> @@ -119,7 +127,7 @@ where /// Creates a new inline [`TextInput`]. /// /// [`TextInput`]: widget::TextInput -pub fn inline_input<'a, Message>(value: &str) -> TextInput<'a, Message> +pub fn inline_input<'a, Message>(value: impl Into>) -> TextInput<'a, Message> where Message: Clone + 'static, { @@ -171,7 +179,7 @@ pub type DnDCommand = (); #[must_use] pub struct TextInput<'a, Message> { id: Option, - placeholder: String, + placeholder: Cow<'a, str>, value: Value, is_secure: bool, font: Option<::Font>, @@ -206,13 +214,14 @@ where /// It expects: /// - a placeholder, /// - the current value - pub fn new(placeholder: &str, value: &str) -> Self { + pub fn new(placeholder: impl Into>, value: impl Into>) -> Self { let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); + let v: Cow<'a, str> = value.into(); TextInput { id: None, - placeholder: String::from(placeholder), - value: Value::new(value), + placeholder: placeholder.into(), + value: Value::new(v.as_ref()), is_secure: false, font: None, width: Length::Fill, @@ -474,6 +483,22 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); + // TODO get values from renderer somehow. + replace_paragraph( + state, + Layout::new(&layout::Node::with_children( + Size::INFINITY, + vec![layout::Node::with_children( + Size::INFINITY, + vec![layout::Node::default()], + )], + )), + &self.value, + self.font.unwrap_or(crate::font::FONT), + Pixels(self.size.unwrap_or(14.0)), + self.line_height, + ); + // Unfocus text input if it becomes disabled if self.on_input.is_none() { state.last_click = None; @@ -506,23 +531,39 @@ where Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let font = self.font.unwrap_or_else(|| renderer.default_font()); if self.dnd_icon { let limits = limits.width(Length::Shrink).height(Length::Shrink); - let size = self.size.unwrap_or_else(|| renderer.default_size()); + let size = self.size.unwrap_or_else(|| renderer.default_size().0); let bounds = limits.max(); - let font = self.font.unwrap_or_else(|| renderer.default_font()); - let Size { width, height } = renderer.measure( - &self.value.to_string(), - size, - self.line_height, + let state = tree.state.downcast_mut::(); + let value_paragraph = &mut state.value; + let v = self.value.to_string(); + value_paragraph.update(Text { + content: if self.value.is_empty() { + &self.placeholder + } else { + &v + }, font, bounds, - text::Shaping::Advanced, - ); + size: iced::Pixels(size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + line_height: text::LineHeight::default(), + shaping: text::Shaping::Advanced, + }); + + let Size { width, height } = limits.resolve(value_paragraph.min_bounds()); let size = limits.resolve(Size::new(width, height)); layout::Node::with_children(size, vec![layout::Node::new(size)]) @@ -540,6 +581,8 @@ where self.helper_text, self.helper_size, self.helper_line_height, + font, + tree, ) } } @@ -697,6 +740,7 @@ where self.on_dnd_command_produced.as_deref(), self.surface_ids, self.line_height, + layout, ) } @@ -847,6 +891,8 @@ pub fn layout( helper_text: Option<&str>, helper_text_size: f32, helper_text_line_height: text::LineHeight, + font: iced_core::Font, + tree: &mut Tree, ) -> layout::Node { let limits = limits.width(width); let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); @@ -854,15 +900,19 @@ pub fn layout( let text_pos = if let Some(label) = label { let text_bounds = limits.resolve(Size::ZERO); - - let label_size = renderer.measure( - label, - size.unwrap_or_else(|| renderer.default_size()), + let state = tree.state.downcast_mut::(); + let label_paragraph = &mut state.label; + label_paragraph.update(Text { + content: label, + font, + bounds: text_bounds, + size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, line_height, - renderer.default_font(), - text_bounds, - text::Shaping::Advanced, - ); + shaping: text::Shaping::Advanced, + }); + let label_size = label_paragraph.min_bounds(); nodes.push(layout::Node::new(label_size)); Vector::new(0.0, label_size.height + f32::from(spacing)) @@ -870,41 +920,48 @@ pub fn layout( Vector::ZERO }; - let text_size = size.unwrap_or_else(|| renderer.default_size()); + let text_size = size.unwrap_or_else(|| renderer.default_size().0); let mut text_input_height = text_size * 1.2; let padding = padding.fit(Size::ZERO, limits.max()); let helper_pos = if leading_icon.is_some() || trailing_icon.is_some() { + let children = &mut tree.children; // TODO configurable icon spacing, maybe via appearance let limits_copy = limits; let limits = limits.pad(padding); let icon_spacing = 8.0; - let (leading_icon_width, mut leading_icon) = if let Some(icon) = leading_icon.as_ref() { - let icon_node = icon.layout( - renderer, - &Limits::NONE - .width(icon.as_widget().width()) - .height(icon.as_widget().height()), - ); - text_input_height = text_input_height.max(icon_node.bounds().height); - (icon_node.bounds().width + icon_spacing, Some(icon_node)) - } else { - (0.0, None) - }; + let mut c_i = 0; + let (leading_icon_width, mut leading_icon) = + if let Some((icon, tree)) = leading_icon.zip(children.get_mut(c_i)) { + let icon_node = icon.as_widget().layout( + tree, + renderer, + &Limits::NONE + .width(icon.as_widget().width()) + .height(icon.as_widget().height()), + ); + text_input_height = text_input_height.max(icon_node.bounds().height); + c_i += 1; + (icon_node.bounds().width + icon_spacing, Some(icon_node)) + } else { + (0.0, None) + }; - let (trailing_icon_width, mut trailing_icon) = if let Some(icon) = trailing_icon.as_ref() { - let icon_node = icon.layout( - renderer, - &Limits::NONE - .width(icon.as_widget().width()) - .height(icon.as_widget().height()), - ); - text_input_height = text_input_height.max(icon_node.bounds().height); - (icon_node.bounds().width + icon_spacing, Some(icon_node)) - } else { - (0.0, None) - }; + let (trailing_icon_width, mut trailing_icon) = + if let Some((icon, tree)) = trailing_icon.zip(children.get_mut(c_i)) { + let icon_node = icon.layout( + tree, + renderer, + &Limits::NONE + .width(icon.as_widget().width()) + .height(icon.as_widget().height()), + ); + text_input_height = text_input_height.max(icon_node.bounds().height); + (icon_node.bounds().width + icon_spacing, Some(icon_node)) + } else { + (0.0, None) + }; let text_limits = limits.width(width).height(text_size * 1.2); let text_bounds = text_limits.resolve(Size::ZERO); @@ -978,14 +1035,19 @@ pub fn layout( .height(helper_text_size * 1.2); let text_bounds = limits.resolve(Size::ZERO); - let helper_text_size = renderer.measure( - helper_text, - helper_text_size, - helper_text_line_height, - renderer.default_font(), - text_bounds, - text::Shaping::Advanced, - ); + let state = tree.state.downcast_mut::(); + let helper_text_paragraph = &mut state.label; + helper_text_paragraph.update(Text { + content: helper_text, + font, + bounds: text_bounds, + size: iced::Pixels(helper_text_size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + line_height: helper_text_line_height, + shaping: text::Shaping::Advanced, + }); + let helper_text_size = helper_text_paragraph.min_bounds(); nodes.push(layout::Node::new(helper_text_size).translate(helper_pos)); }; @@ -1013,16 +1075,16 @@ pub fn layout( #[allow(clippy::missing_panics_doc)] #[allow(clippy::cast_lossless)] #[allow(clippy::cast_possible_truncation)] -pub fn update<'a, Message, Renderer>( +pub fn update<'a, Message>( event: Event, text_layout: Layout<'_>, cursor_position: mouse::Cursor, - renderer: &Renderer, + renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, value: &mut Value, size: Option, - font: Option, + font: Option<::Font>, is_secure: bool, on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, @@ -1033,11 +1095,17 @@ pub fn update<'a, Message, Renderer>( on_dnd_command_produced: Option<&dyn Fn(DnDCommand) -> Message>, surface_ids: Option<(window::Id, window::Id)>, line_height: text::LineHeight, + layout: Layout<'_>, ) -> event::Status where Message: Clone, - Renderer: text::Renderer, { + let font = font.unwrap_or_else(|| renderer.default_font()); + let size = size.unwrap_or_else(|| renderer.default_size().0); + let update_cache = |state, value| { + replace_paragraph(state, layout, value, font, iced::Pixels(size), line_height); + }; + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -1056,9 +1124,6 @@ where None }; - let font: ::Font = - font.unwrap_or_else(|| renderer.default_font()); - if is_clicked { let Some(pos) = cursor_position.position() else { return event::Status::Ignored; @@ -1092,27 +1157,19 @@ where surface_ids, on_input, ) { - let actual_size = size.unwrap_or_else(|| renderer.default_size()); - let left = start.min(end); let right = end.max(start); let (left_position, _left_offset) = measure_cursor_and_scroll_offset( - renderer, + &state.value, text_layout.bounds(), - value, - actual_size, left, - font, ); let (right_position, _right_offset) = measure_cursor_and_scroll_offset( - renderer, + &state.value, text_layout.bounds(), - value, - actual_size, right, - font, ); let width = right_position - left_position; @@ -1160,14 +1217,10 @@ where }; find_cursor_position( - renderer, text_layout.bounds(), - font, - size, &value, - state, + &state, target, - line_height, ) } else { None @@ -1189,16 +1242,7 @@ where value.clone() }; - find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - line_height, - ) + find_cursor_position(text_layout.bounds(), &value, state, target) } else { None }; @@ -1210,17 +1254,9 @@ where if is_secure { state.cursor.select_all(value); } else { - let position = find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - value, - state, - target, - line_height, - ) - .unwrap_or(0); + let position = + find_cursor_position(text_layout.bounds(), value, state, target) + .unwrap_or(0); state.cursor.select_range( value.previous_start_of_word(position), @@ -1260,20 +1296,8 @@ where } else { value.clone() }; - let font: ::Font = - font.unwrap_or_else(|| renderer.default_font()); - - let position = find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - line_height, - ) - .unwrap_or(0); + let position = + find_cursor_position(text_layout.bounds(), &value, state, target).unwrap_or(0); state .cursor @@ -1303,6 +1327,8 @@ where focus.updated_at = Instant::now(); + update_cache(state, value); + return event::Status::Captured; } } @@ -1341,6 +1367,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) @@ -1359,6 +1387,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) && !is_secure { @@ -1417,6 +1447,8 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + + update_cache(state, value); } keyboard::KeyCode::V => { if state.keyboard_modifiers.command() { @@ -1445,6 +1477,8 @@ where shell.publish(message); state.is_pasting = Some(content); + + update_cache(state, value); } else { state.is_pasting = None; } @@ -1571,18 +1605,7 @@ where value.clone() }; - let font = font.unwrap_or_else(|| renderer.default_font()); - - find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - line_height, - ) + find_cursor_position(text_layout.bounds(), &value, state, target) } else { None }; @@ -1651,18 +1674,8 @@ where } else { value.clone() }; - let font = font.unwrap_or_else(|| renderer.default_font()); - find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - line_height, - ) + find_cursor_position(text_layout.bounds(), &value, state, target) } else { None }; @@ -1895,17 +1908,20 @@ pub fn draw<'a, Message>( // draw the label if it exists if let (Some(label_layout), Some(label)) = (label_layout, label) { - renderer.fill_text(Text { - content: label, - size: size.unwrap_or_else(|| renderer.default_size()), - font: font.unwrap_or_else(|| renderer.default_font()), - color: appearance.label_color, - bounds: label_layout.bounds(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - line_height, - shaping: text::Shaping::Advanced, - }); + renderer.fill_text( + Text { + content: label, + size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), + font: font.unwrap_or_else(|| renderer.default_font()), + bounds: label_layout.bounds().size(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + line_height, + shaping: text::Shaping::Advanced, + }, + bounds.position(), + appearance.label_color, + ); } let mut child_index = 0; let leading_icon_tree = children.get(child_index); @@ -1931,19 +1947,13 @@ pub fn draw<'a, Message>( let text = value.to_string(); let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size()); + let size = size.unwrap_or_else(|| renderer.default_size().0); let (cursor, offset) = if let Some(focus) = &state.is_focused { match state.cursor.state(value) { cursor::State::Index(position) => { - let (text_value_width, offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - position, - font, - ); + let (text_value_width, offset) = + measure_cursor_and_scroll_offset(&state.value, text_bounds, position); let is_cursor_visible = ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) % 2 @@ -1979,23 +1989,12 @@ pub fn draw<'a, Message>( let left = start.min(end); let right = end.max(start); - let (left_position, left_offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - left, - font, - ); + let value_paragraph = &state.value; + let (left_position, left_offset) = + measure_cursor_and_scroll_offset(value_paragraph, text_bounds, left); - let (right_position, right_offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - right, - font, - ); + let (right_position, right_offset) = + measure_cursor_and_scroll_offset(value_paragraph, text_bounds, right); let width = right_position - left_position; @@ -2030,12 +2029,7 @@ pub fn draw<'a, Message>( (None, 0.0) }; - let text_width = renderer.measure_width( - if text.is_empty() { placeholder } else { &text }, - size, - font, - text::Shaping::Advanced, - ); + let text_width = state.value.min_width(); let render = |renderer: &mut crate::Renderer| { if let Some((cursor, color)) = cursor { @@ -2044,25 +2038,30 @@ pub fn draw<'a, Message>( renderer.with_translation(Vector::ZERO, |_| {}); } - renderer.fill_text(Text { - content: if text.is_empty() { placeholder } else { &text }, - color: if text.is_empty() { - appearance.placeholder_color - } else { - appearance.text_color + let bounds = Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }; + let color = if text.is_empty() { + appearance.placeholder_color + } else { + appearance.text_color + }; + renderer.fill_text( + Text { + content: if text.is_empty() { placeholder } else { &text }, + font, + bounds: bounds.size(), + size: iced::Pixels(size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + line_height: text::LineHeight::default(), + shaping: text::Shaping::Advanced, }, - font, - bounds: Rectangle { - y: text_bounds.center_y(), - width: f32::INFINITY, - ..text_bounds - }, - size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - line_height: text::LineHeight::default(), - shaping: text::Shaping::Advanced, - }); + bounds.position(), + color, + ); }; if text_width > text_bounds.width { @@ -2096,17 +2095,20 @@ pub fn draw<'a, Message>( // draw the helper text if it exists if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) { - renderer.fill_text(Text { - content: helper_text, - size: helper_text_size, - font, - color: appearance.text_color, - bounds: helper_text_layout.bounds(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - line_height: helper_line_height, - shaping: text::Shaping::Advanced, - }); + renderer.fill_text( + Text { + content: helper_text, + size: iced::Pixels(helper_text_size), + font, + bounds: helper_text_layout.bounds().size(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + line_height: helper_line_height, + shaping: text::Shaping::Advanced, + }, + helper_text_layout.bounds().position(), + appearance.text_color, + ); } } @@ -2165,6 +2167,9 @@ pub(crate) struct DndOfferState; #[derive(Debug, Default, Clone)] #[must_use] pub struct State { + pub value: crate::Paragraph, + pub placeholder: crate::Paragraph, + pub label: crate::Paragraph, is_focused: Option, dragging_state: Option, dnd_offer: DndOfferState, @@ -2214,6 +2219,10 @@ impl State { /// Creates a new [`State`], representing a focused [`TextInput`]. pub fn focused() -> Self { Self { + value: crate::Paragraph::new(), + placeholder: crate::Paragraph::new(), + label: crate::Paragraph::new(), + is_focused: None, dragging_state: None, #[allow(clippy::default_constructed_unit_structs)] @@ -2307,6 +2316,70 @@ impl operation::TextInput for State { } } +fn measure_cursor_and_scroll_offset( + paragraph: &impl text::Paragraph, + text_bounds: Rectangle, + cursor_index: usize, +) -> (f32, f32) { + let grapheme_position = paragraph + .grapheme_position(0, cursor_index) + .unwrap_or(Point::ORIGIN); + + let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0); + + (grapheme_position.x, offset) +} + +/// Computes the position of the text cursor at the given X coordinate of +/// a [`TextInput`]. +fn find_cursor_position( + text_bounds: Rectangle, + value: &Value, + state: &State, + x: f32, +) -> Option { + let offset = offset(text_bounds, value, state); + let value = value.to_string(); + + let char_offset = state + .value + .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) + .map(text::Hit::cursor)?; + + Some( + unicode_segmentation::UnicodeSegmentation::graphemes( + &value[..char_offset.min(value.len())], + true, + ) + .count(), + ) +} + +fn replace_paragraph( + state: &mut State, + layout: Layout<'_>, + value: &Value, + font: ::Font, + text_size: Pixels, + line_height: text::LineHeight, +) { + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); + + state.value = crate::Paragraph::with_text(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; + mod platform { use iced_core::keyboard; @@ -2319,17 +2392,7 @@ mod platform { } } -fn offset( - renderer: &Renderer, - text_bounds: Rectangle, - font: Renderer::Font, - size: f32, - value: &Value, - state: &State, -) -> f32 -where - Renderer: text::Renderer, -{ +fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { if state.is_focused() { let cursor = state.cursor(); @@ -2338,77 +2401,11 @@ where cursor::State::Selection { end, .. } => end, }; - let (_, offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - focus_position, - font, - ); + let (_, offset) = + measure_cursor_and_scroll_offset(&state.value, text_bounds, focus_position); offset } else { 0.0 } } - -fn measure_cursor_and_scroll_offset( - renderer: &Renderer, - text_bounds: Rectangle, - value: &Value, - size: f32, - cursor_index: usize, - font: Renderer::Font, -) -> (f32, f32) -where - Renderer: text::Renderer, -{ - let text_before_cursor = value.until(cursor_index).to_string(); - - let text_value_width = - renderer.measure_width(&text_before_cursor, size, font, text::Shaping::Advanced); - - let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); - - (text_value_width, offset) -} - -/// Computes the position of the text cursor at the given X coordinate of -/// a [`TextInput`]. -#[allow(clippy::too_many_arguments)] -fn find_cursor_position( - renderer: &Renderer, - text_bounds: Rectangle, - font: Renderer::Font, - size: Option, - value: &Value, - state: &State, - x: f32, - line_height: text::LineHeight, -) -> Option -where - Renderer: text::Renderer, -{ - let size = size.unwrap_or_else(|| renderer.default_size()); - - let offset = offset(renderer, text_bounds, font, size, value, state); - let value = value.to_string(); - - let 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, - ) - .map(text::Hit::cursor)?; - - Some(unicode_segmentation::UnicodeSegmentation::graphemes(&value[..char_offset], true).count()) -} - -const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;