diff options
Diffstat (limited to '')
-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 |
4 files changed, 385 insertions, 15 deletions
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(), |