diff options
author | 2024-07-18 22:39:49 +0200 | |
---|---|---|
committer | 2024-07-18 22:39:49 +0200 | |
commit | 23ad15391c88f562c90f4344d3949f76b6f9caf9 (patch) | |
tree | 883f5752e3cfe516ee22048015e9255b502bb04e /widget | |
parent | 616689ca54942a13aac3615e571ae995ad4571b6 (diff) | |
parent | 06acb740fba1889c6a9fb48dfa3ae3aaac1df3ab (diff) | |
download | iced-23ad15391c88f562c90f4344d3949f76b6f9caf9.tar.gz iced-23ad15391c88f562c90f4344d3949f76b6f9caf9.tar.bz2 iced-23ad15391c88f562c90f4344d3949f76b6f9caf9.zip |
Merge pull request #2508 from iced-rs/feature/rich-text
`rich_text` and `markdown` widgets
Diffstat (limited to '')
-rw-r--r-- | widget/Cargo.toml | 10 | ||||
-rw-r--r-- | widget/src/helpers.rs | 45 | ||||
-rw-r--r-- | widget/src/lib.rs | 3 | ||||
-rw-r--r-- | widget/src/markdown.rs | 246 | ||||
-rw-r--r-- | widget/src/pick_list.rs | 9 | ||||
-rw-r--r-- | widget/src/scrollable.rs | 18 | ||||
-rw-r--r-- | widget/src/text.rs | 4 | ||||
-rw-r--r-- | widget/src/text/rich.rs | 335 | ||||
-rw-r--r-- | widget/src/text_editor.rs | 29 | ||||
-rw-r--r-- | widget/src/text_input.rs | 32 |
10 files changed, 701 insertions, 30 deletions
diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 498a768b..2f483b79 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -22,8 +22,10 @@ lazy = ["ouroboros"] image = ["iced_renderer/image"] svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] -qr_code = ["canvas", "qrcode"] +qr_code = ["canvas", "dep:qrcode"] wgpu = ["iced_renderer/wgpu"] +markdown = ["dep:pulldown-cmark"] +highlighter = ["dep:iced_highlighter"] advanced = [] [dependencies] @@ -41,3 +43,9 @@ ouroboros.optional = true qrcode.workspace = true qrcode.optional = true + +pulldown-cmark.workspace = true +pulldown-cmark.optional = true + +iced_highlighter.workspace = true +iced_highlighter.optional = true diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1f282f54..aa9394cb 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -24,7 +24,7 @@ use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; use crate::{Column, MouseArea, Row, Space, Stack, Themer}; -use std::borrow::Borrow; +use std::borrow::{Borrow, Cow}; use std::ops::RangeInclusive; /// Creates a [`Column`] with the given children. @@ -112,6 +112,19 @@ macro_rules! text { }; } +/// Creates some [`Rich`] text with the given spans. +/// +/// [`Rich`]: text::Rich +#[macro_export] +macro_rules! rich_text { + () => ( + $crate::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::text::Rich::with_spans([$($crate::text::Span::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container @@ -646,8 +659,6 @@ where } /// Creates a new [`Text`] widget with the provided content. -/// -/// [`Text`]: core::widget::Text pub fn text<'a, Theme, Renderer>( text: impl text::IntoFragment<'a>, ) -> Text<'a, Theme, Renderer> @@ -659,8 +670,6 @@ where } /// Creates a new [`Text`] widget that displays the provided value. -/// -/// [`Text`]: core::widget::Text pub fn value<'a, Theme, Renderer>( value: impl ToString, ) -> Text<'a, Theme, Renderer> @@ -671,6 +680,32 @@ where Text::new(value.to_string()) } +/// Creates a new [`Rich`] text widget with the provided spans. +/// +/// [`Rich`]: text::Rich +pub fn rich_text<'a, Theme, Renderer>( + spans: impl Into<Cow<'a, [text::Span<'a, Renderer::Font>]>>, +) -> text::Rich<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: core::text::Renderer, +{ + text::Rich::with_spans(spans) +} + +/// Creates a new [`Span`] of text with the provided content. +/// +/// [`Span`]: text::Span +pub fn span<'a, Font>( + text: impl text::IntoFragment<'a>, +) -> text::Span<'a, Font> { + text::Span::new(text) +} + +#[cfg(feature = "markdown")] +#[doc(inline)] +pub use crate::markdown::view as markdown; + /// Creates a new [`Checkbox`]. /// /// [`Checkbox`]: crate::Checkbox diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 00e9aaa4..115a29e5 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -130,5 +130,8 @@ pub mod qr_code; #[doc(no_inline)] pub use qr_code::QRCode; +#[cfg(feature = "markdown")] +pub mod markdown; + pub use crate::core::theme::{self, Theme}; pub use renderer::Renderer; diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs new file mode 100644 index 00000000..bbb5b463 --- /dev/null +++ b/widget/src/markdown.rs @@ -0,0 +1,246 @@ +//! Parse and display Markdown. +//! +//! You can enable the `highlighter` feature for syntax highligting +//! in code blocks. +//! +//! Only the variants of [`Item`] are currently supported. +use crate::core::font::{self, Font}; +use crate::core::padding; +use crate::core::theme::{self, Theme}; +use crate::core::{self, Element, Length}; +use crate::{column, container, rich_text, row, span, text}; + +/// A Markdown item. +#[derive(Debug, Clone)] +pub enum Item { + /// A heading. + Heading(Vec<text::Span<'static>>), + /// A paragraph. + Paragraph(Vec<text::Span<'static>>), + /// A code block. + /// + /// You can enable the `highlighter` feature for syntax highligting. + CodeBlock(Vec<text::Span<'static>>), + /// A list. + List { + /// The first number of the list, if it is ordered. + start: Option<u64>, + /// The items of the list. + items: Vec<Vec<text::Span<'static>>>, + }, +} + +/// Parse the given Markdown content. +pub fn parse( + markdown: &str, + palette: theme::Palette, +) -> impl Iterator<Item = Item> + '_ { + let mut spans = Vec::new(); + let mut heading = None; + let mut strong = false; + let mut emphasis = false; + let mut link = false; + let mut list = Vec::new(); + let mut list_start = None; + + #[cfg(feature = "highlighter")] + let mut highlighter = None; + + let parser = pulldown_cmark::Parser::new(markdown); + + // We want to keep the `spans` capacity + #[allow(clippy::drain_collect)] + parser.filter_map(move |event| match event { + pulldown_cmark::Event::Start(tag) => match tag { + pulldown_cmark::Tag::Heading { level, .. } => { + heading = Some(level); + None + } + pulldown_cmark::Tag::Strong => { + strong = true; + None + } + pulldown_cmark::Tag::Emphasis => { + emphasis = true; + None + } + pulldown_cmark::Tag::Link { .. } => { + link = true; + None + } + pulldown_cmark::Tag::List(first_item) => { + list_start = first_item; + None + } + pulldown_cmark::Tag::CodeBlock( + pulldown_cmark::CodeBlockKind::Fenced(_language), + ) => { + #[cfg(feature = "highlighter")] + { + use iced_highlighter::{self, Highlighter}; + use text::Highlighter as _; + + highlighter = + Some(Highlighter::new(&iced_highlighter::Settings { + theme: iced_highlighter::Theme::Base16Ocean, + token: _language.to_string(), + })); + } + + None + } + _ => None, + }, + pulldown_cmark::Event::End(tag) => match tag { + pulldown_cmark::TagEnd::Heading(_) => { + heading = None; + Some(Item::Heading(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::Emphasis => { + emphasis = false; + None + } + pulldown_cmark::TagEnd::Strong => { + strong = false; + None + } + pulldown_cmark::TagEnd::Link => { + link = false; + None + } + pulldown_cmark::TagEnd::Paragraph => { + Some(Item::Paragraph(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::List(_) => Some(Item::List { + start: list_start, + items: list.drain(..).collect(), + }), + pulldown_cmark::TagEnd::Item => { + list.push(spans.drain(..).collect()); + None + } + pulldown_cmark::TagEnd::CodeBlock => { + #[cfg(feature = "highlighter")] + { + highlighter = None; + } + + Some(Item::CodeBlock(spans.drain(..).collect())) + } + _ => None, + }, + pulldown_cmark::Event::Text(text) => { + #[cfg(feature = "highlighter")] + if let Some(highlighter) = &mut highlighter { + use text::Highlighter as _; + + for (range, highlight) in + highlighter.highlight_line(text.as_ref()) + { + let span = span(text[range].to_owned()) + .color_maybe(highlight.color()) + .font_maybe(highlight.font()); + + spans.push(span); + } + + return None; + } + + let span = span(text.into_string()); + + let span = match heading { + None => span, + Some(heading) => span.size(match heading { + pulldown_cmark::HeadingLevel::H1 => 32, + pulldown_cmark::HeadingLevel::H2 => 28, + pulldown_cmark::HeadingLevel::H3 => 24, + pulldown_cmark::HeadingLevel::H4 => 20, + pulldown_cmark::HeadingLevel::H5 => 16, + pulldown_cmark::HeadingLevel::H6 => 16, + }), + }; + + let span = if strong || emphasis { + span.font(Font { + weight: if strong { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if emphasis { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::default() + }) + } else { + span + }; + + let span = span.color_maybe(link.then_some(palette.primary)); + + spans.push(span); + + None + } + pulldown_cmark::Event::Code(code) => { + spans.push(span(code.into_string()).font(Font::MONOSPACE)); + None + } + pulldown_cmark::Event::SoftBreak => { + spans.push(span(" ")); + None + } + pulldown_cmark::Event::HardBreak => { + spans.push(span("\n")); + None + } + _ => None, + }) +} + +/// Display a bunch of Markdown items. +/// +/// You can obtain the items with [`parse`]. +pub fn view<'a, Message, Renderer>( + items: impl IntoIterator<Item = &'a Item>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + let blocks = items.into_iter().enumerate().map(|(i, item)| match item { + Item::Heading(heading) => container(rich_text(heading)) + .padding(padding::top(if i > 0 { 8 } else { 0 })) + .into(), + Item::Paragraph(paragraph) => rich_text(paragraph).into(), + Item::List { start: None, items } => column( + items + .iter() + .map(|item| row!["•", rich_text(item)].spacing(10).into()), + ) + .spacing(10) + .into(), + Item::List { + start: Some(start), + items, + } => column(items.iter().enumerate().map(|(i, item)| { + row![text!("{}.", i as u64 + *start), rich_text(item)] + .spacing(10) + .into() + })) + .spacing(10) + .into(), + Item::CodeBlock(code) => { + container(rich_text(code).font(Font::MONOSPACE).size(12)) + .width(Length::Fill) + .padding(10) + .style(container::rounded_box) + .into() + } + }); + + Element::new(column(blocks).width(Length::Fill).spacing(16)) +} diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 97de5b48..f7f7b65b 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -6,7 +6,8 @@ use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::text::{self, Paragraph as _, Text}; +use crate::core::text::paragraph; +use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ @@ -622,8 +623,8 @@ struct State<P: text::Paragraph> { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, - options: Vec<P>, - placeholder: P, + options: Vec<paragraph::Plain<P>>, + placeholder: paragraph::Plain<P>, } impl<P: text::Paragraph> State<P> { @@ -635,7 +636,7 @@ impl<P: text::Paragraph> State<P> { is_open: bool::default(), hovered_option: Option::default(), options: Vec::new(), - placeholder: P::default(), + placeholder: paragraph::Plain::default(), } } } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index b1082203..6dd593cb 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -62,19 +62,27 @@ where .validate() } - fn validate(self) -> Self { + fn validate(mut self) -> Self { + let size_hint = self.content.as_widget().size_hint(); + debug_assert!( - self.direction.vertical().is_none() - || !self.content.as_widget().size_hint().height.is_fill(), + self.direction.vertical().is_none() || !size_hint.height.is_fill(), "scrollable content must not fill its vertical scrolling axis" ); debug_assert!( - self.direction.horizontal().is_none() - || !self.content.as_widget().size_hint().width.is_fill(), + self.direction.horizontal().is_none() || !size_hint.width.is_fill(), "scrollable content must not fill its horizontal scrolling axis" ); + if self.direction.horizontal().is_none() { + self.width = self.width.enclose(size_hint.width); + } + + if self.direction.vertical().is_none() { + self.height = self.height.enclose(size_hint.height); + } + self } diff --git a/widget/src/text.rs b/widget/src/text.rs index 0d689295..9bf7fce4 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,5 +1,9 @@ //! Draw and interact with text. +mod rich; + +pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span}; pub use crate::core::widget::text::*; +pub use rich::Rich; /// A paragraph. pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> = diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs new file mode 100644 index 00000000..5c44ed9e --- /dev/null +++ b/widget/src/text/rich.rs @@ -0,0 +1,335 @@ +use crate::core::alignment; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text::{Paragraph, Span}; +use crate::core::widget::text::{ + self, Catalog, LineHeight, Shaping, Style, StyleFn, +}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + self, Color, Element, Length, Pixels, Rectangle, Size, Widget, +}; + +use std::borrow::Cow; + +/// A bunch of [`Rich`] text. +#[derive(Debug)] +pub struct Rich<'a, Theme = crate::Theme, Renderer = crate::Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + spans: Cow<'a, [Span<'a, Renderer::Font>]>, + size: Option<Pixels>, + line_height: LineHeight, + width: Length, + height: Length, + font: Option<Renderer::Font>, + align_x: alignment::Horizontal, + align_y: alignment::Vertical, + class: Theme::Class<'a>, +} + +impl<'a, Theme, Renderer> Rich<'a, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + /// Creates a new empty [`Rich`] text. + pub fn new() -> Self { + Self { + spans: Cow::default(), + size: None, + line_height: LineHeight::default(), + width: Length::Shrink, + height: Length::Shrink, + font: None, + align_x: alignment::Horizontal::Left, + align_y: alignment::Vertical::Top, + class: Theme::default(), + } + } + + /// Creates a new [`Rich`] text with the given text spans. + pub fn with_spans( + spans: impl Into<Cow<'a, [Span<'a, Renderer::Font>]>>, + ) -> Self { + Self { + spans: spans.into(), + ..Self::new() + } + } + + /// Sets the default size of the [`Rich`] text. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = Some(size.into()); + self + } + + /// Sets the defualt [`LineHeight`] of the [`Rich`] text. + pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self { + self.line_height = line_height.into(); + self + } + + /// Sets the default font of the [`Rich`] text. + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the width of the [`Rich`] text boundaries. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Rich`] text boundaries. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Centers the [`Rich`] text, both horizontally and vertically. + pub fn center(self) -> Self { + self.align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center) + } + + /// Sets the [`alignment::Horizontal`] of the [`Rich`] text. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.align_x = alignment.into(); + self + } + + /// Sets the [`alignment::Vertical`] of the [`Rich`] text. + pub fn align_y( + mut self, + alignment: impl Into<alignment::Vertical>, + ) -> Self { + self.align_y = alignment.into(); + self + } + + /// Sets the default style of the [`Rich`] text. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the default [`Color`] of the [`Rich`] text. + pub fn color(self, color: impl Into<Color>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.color_maybe(Some(color)) + } + + /// Sets the default [`Color`] of the [`Rich`] text, if `Some`. + pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + let color = color.map(Into::into); + + self.style(move |_theme| Style { color }) + } + + /// Sets the default style class of the [`Rich`] text. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } + + /// Adds a new text [`Span`] to the [`Rich`] text. + pub fn push(mut self, span: impl Into<Span<'a, Renderer::Font>>) -> Self { + self.spans.to_mut().push(span.into()); + self + } +} + +impl<'a, Theme, Renderer> Default for Rich<'a, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +struct State<P: Paragraph> { + spans: Vec<Span<'static, P::Font>>, + paragraph: P, +} + +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Rich<'a, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State<Renderer::Paragraph>>() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + spans: Vec::new(), + paragraph: Renderer::Paragraph::default(), + }) + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + tree.state.downcast_mut::<State<Renderer::Paragraph>>(), + renderer, + limits, + self.width, + self.height, + self.spans.as_ref(), + self.line_height, + self.size, + self.font, + self.align_x, + self.align_y, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + defaults: &renderer::Style, + layout: Layout<'_>, + _cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + let style = theme.style(&self.class); + + text::draw( + renderer, + defaults, + layout, + &state.paragraph, + style, + viewport, + ); + } +} + +fn layout<Renderer>( + state: &mut State<Renderer::Paragraph>, + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + spans: &[Span<'_, Renderer::Font>], + line_height: LineHeight, + size: Option<Pixels>, + font: Option<Renderer::Font>, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, +) -> layout::Node +where + Renderer: core::text::Renderer, +{ + layout::sized(limits, width, height, |limits| { + let bounds = limits.max(); + + let size = size.unwrap_or_else(|| renderer.default_size()); + let font = font.unwrap_or_else(|| renderer.default_font()); + + let text_with_spans = || core::Text { + content: spans, + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping: Shaping::Advanced, + }; + + if state.spans != spans { + state.paragraph = + Renderer::Paragraph::with_spans(text_with_spans()); + state.spans = spans.iter().cloned().map(Span::to_static).collect(); + } else { + match state.paragraph.compare(core::Text { + content: (), + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping: Shaping::Advanced, + }) { + core::text::Difference::None => {} + core::text::Difference::Bounds => { + state.paragraph.resize(bounds); + } + core::text::Difference::Shape => { + state.paragraph = + Renderer::Paragraph::with_spans(text_with_spans()); + } + } + } + + state.paragraph.min_bounds() + }) +} + +impl<'a, Theme, Renderer> FromIterator<Span<'a, Renderer::Font>> + for Rich<'a, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + fn from_iter<T: IntoIterator<Item = Span<'a, Renderer::Font>>>( + spans: T, + ) -> Self { + Self { + spans: spans.into_iter().collect(), + ..Self::new() + } + } +} + +impl<'a, Message, Theme, Renderer> From<Rich<'a, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Theme: Catalog + 'a, + Renderer: core::text::Renderer + 'a, +{ + fn from( + text: Rich<'a, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(text) + } +} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0156b960..e494a3b0 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -9,6 +9,7 @@ use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight}; +use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::{ Background, Border, Color, Element, Length, Padding, Pixels, Rectangle, @@ -338,6 +339,22 @@ impl<Highlighter: text::Highlighter> State<Highlighter> { } } +impl<Highlighter: text::Highlighter> operation::Focusable + for State<Highlighter> +{ + fn is_focused(&self) -> bool { + self.is_focused + } + + fn focus(&mut self) { + self.is_focused = true; + } + + fn unfocus(&mut self) { + self.is_focused = false; + } +} + impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextEditor<'a, Highlighter, Message, Theme, Renderer> where @@ -640,6 +657,18 @@ where mouse::Interaction::default() } } + + fn operate( + &self, + tree: &mut widget::Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + operation: &mut dyn widget::Operation<()>, + ) { + let state = tree.state.downcast_mut::<State<Highlighter>>(); + + operation.focusable(state, None); + } } impl<'a, Highlighter, Message, Theme, Renderer> diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index ba2fbc13..a0fe14a0 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -19,7 +19,8 @@ use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; -use crate::core::text::{self, Paragraph as _, Text}; +use crate::core::text::paragraph; +use crate::core::text::{self, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; use crate::core::widget; @@ -360,7 +361,7 @@ where let icon_layout = children_layout.next().unwrap(); renderer.fill_paragraph( - &state.icon, + state.icon.raw(), icon_layout.bounds().center(), style.icon, *viewport, @@ -378,7 +379,7 @@ where cursor::State::Index(position) => { let (text_value_width, offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, position, ); @@ -415,14 +416,14 @@ where let (left_position, left_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, left, ); let (right_position, right_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, right, ); @@ -469,9 +470,9 @@ where renderer.fill_paragraph( if text.is_empty() { - &state.placeholder + state.placeholder.raw() } else { - &state.value + state.value.raw() }, Point::new(text_bounds.x, text_bounds.center_y()) - Vector::new(offset, 0.0), @@ -1178,9 +1179,9 @@ pub fn select_all<T>(id: Id) -> Task<T> { /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State<P: text::Paragraph> { - value: P, - placeholder: P, - icon: P, + value: paragraph::Plain<P>, + placeholder: paragraph::Plain<P>, + icon: paragraph::Plain<P>, is_focused: Option<Focus>, is_dragging: bool, is_pasting: Option<Value>, @@ -1212,9 +1213,9 @@ impl<P: text::Paragraph> State<P> { /// Creates a new [`State`], representing a focused [`TextInput`]. pub fn focused() -> Self { Self { - value: P::default(), - placeholder: P::default(), - icon: P::default(), + value: paragraph::Plain::default(), + placeholder: paragraph::Plain::default(), + icon: paragraph::Plain::default(), is_focused: None, is_dragging: false, is_pasting: None, @@ -1319,7 +1320,7 @@ fn offset<P: text::Paragraph>( }; let (_, offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, focus_position, ); @@ -1357,6 +1358,7 @@ fn find_cursor_position<P: text::Paragraph>( let char_offset = state .value + .raw() .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) .map(text::Hit::cursor)?; @@ -1386,7 +1388,7 @@ fn replace_paragraph<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = Renderer::Paragraph::with_text(Text { + state.value = paragraph::Plain::new(Text { font, line_height, content: &value.to_string(), |