diff options
author | 2023-10-27 17:36:54 +0200 | |
---|---|---|
committer | 2023-10-27 17:36:54 +0200 | |
commit | d731996342118dccfd50df8db9607741d162a639 (patch) | |
tree | 9f7db10dea8e6faf25041b19d0fe595acb995e9c /core/src | |
parent | 3ec5ad42251d4f35861f3bed621223e383742b12 (diff) | |
parent | c8eca4e6bfae82013e6bb08e9d8bf66560b36564 (diff) | |
download | iced-d731996342118dccfd50df8db9607741d162a639.tar.gz iced-d731996342118dccfd50df8db9607741d162a639.tar.bz2 iced-d731996342118dccfd50df8db9607741d162a639.zip |
Merge pull request #2123 from iced-rs/text-editor
`TextEditor` widget (or multi-line text input)
Diffstat (limited to 'core/src')
-rw-r--r-- | core/src/color.rs | 20 | ||||
-rw-r--r-- | core/src/font.rs | 4 | ||||
-rw-r--r-- | core/src/layout/limits.rs | 2 | ||||
-rw-r--r-- | core/src/mouse/click.rs | 5 | ||||
-rw-r--r-- | core/src/renderer/null.rs | 91 | ||||
-rw-r--r-- | core/src/text.rs | 173 | ||||
-rw-r--r-- | core/src/text/editor.rs | 181 | ||||
-rw-r--r-- | core/src/text/highlighter.rs | 88 | ||||
-rw-r--r-- | core/src/text/paragraph.rs | 59 | ||||
-rw-r--r-- | core/src/widget/text.rs | 23 |
10 files changed, 475 insertions, 171 deletions
diff --git a/core/src/color.rs b/core/src/color.rs index 0e8b7475..13077628 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -89,6 +89,26 @@ impl Color { } } + /// Creates a [`Color`] from its linear RGBA components. + pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + // As described in: + // https://en.wikipedia.org/wiki/SRGB + fn gamma_component(u: f32) -> f32 { + if u < 0.0031308 { + 12.92 * u + } else { + 1.055 * u.powf(1.0 / 2.4) - 0.055 + } + } + + Self { + r: gamma_component(r), + g: gamma_component(g), + b: gamma_component(b), + a, + } + } + /// Converts the [`Color`] into its RGBA8 equivalent. #[must_use] pub fn into_rgba8(self) -> [u8; 4] { diff --git a/core/src/font.rs b/core/src/font.rs index 7f647847..2b68decf 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -12,8 +12,6 @@ pub struct Font { pub stretch: Stretch, /// The [`Style`] of the [`Font`]. pub style: Style, - /// Whether if the [`Font`] is monospaced or not. - pub monospaced: bool, } impl Font { @@ -23,13 +21,11 @@ impl Font { weight: Weight::Normal, stretch: Stretch::Normal, style: Style::Normal, - monospaced: false, }; /// A monospaced font with normal [`Weight`]. pub const MONOSPACE: Font = Font { family: Family::Monospace, - monospaced: true, ..Self::DEFAULT }; diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs index 5d3c1556..39a3d98b 100644 --- a/core/src/layout/limits.rs +++ b/core/src/layout/limits.rs @@ -2,7 +2,7 @@ use crate::{Length, Padding, Size}; /// A set of size constraints for layouting. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Limits { min: Size, max: Size, diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 9cc44a71..6f3844be 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -61,6 +61,11 @@ impl Click { self.kind } + /// Returns the position of the [`Click`]. + pub fn position(&self) -> Point { + self.position + } + fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { let duration = if time > self.time { Some(time - self.time) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 55d58a59..da0f32de 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -43,6 +43,7 @@ impl Renderer for Null { impl text::Renderer for Null { type Font = Font; type Paragraph = (); + type Editor = (); const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; @@ -58,19 +59,17 @@ impl text::Renderer for Null { fn load_font(&mut self, _font: Cow<'static, [u8]>) {} - fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph { - } - - fn resize_paragraph( - &self, - _paragraph: &mut Self::Paragraph, - _new_bounds: Size, + fn fill_paragraph( + &mut self, + _paragraph: &Self::Paragraph, + _position: Point, + _color: Color, ) { } - fn fill_paragraph( + fn fill_editor( &mut self, - _paragraph: &Self::Paragraph, + _editor: &Self::Editor, _position: Point, _color: Color, ) { @@ -88,47 +87,83 @@ impl text::Renderer for Null { impl text::Paragraph for () { type Font = Font; - fn content(&self) -> &str { - "" + fn with_text(_text: Text<'_, Self::Font>) -> Self {} + + fn resize(&mut self, _new_bounds: Size) {} + + fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference { + text::Difference::None } - fn text_size(&self) -> Pixels { - Pixels(16.0) + fn horizontal_alignment(&self) -> alignment::Horizontal { + alignment::Horizontal::Left } - fn font(&self) -> Self::Font { - Font::default() + fn vertical_alignment(&self) -> alignment::Vertical { + alignment::Vertical::Top } - fn line_height(&self) -> text::LineHeight { - text::LineHeight::default() + fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> { + None } - fn shaping(&self) -> text::Shaping { - text::Shaping::default() + fn min_bounds(&self) -> Size { + Size::ZERO } - fn horizontal_alignment(&self) -> alignment::Horizontal { - alignment::Horizontal::Left + fn hit_test(&self, _point: Point) -> Option<text::Hit> { + None } +} - fn vertical_alignment(&self) -> alignment::Vertical { - alignment::Vertical::Top +impl text::Editor for () { + type Font = Font; + + fn with_text(_text: &str) -> Self {} + + fn cursor(&self) -> text::editor::Cursor { + text::editor::Cursor::Caret(Point::ORIGIN) } - fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> { + fn cursor_position(&self) -> (usize, usize) { + (0, 0) + } + + fn selection(&self) -> Option<String> { + None + } + + fn line(&self, _index: usize) -> Option<&str> { None } + fn line_count(&self) -> usize { + 0 + } + + fn perform(&mut self, _action: text::editor::Action) {} + fn bounds(&self) -> Size { Size::ZERO } - fn min_bounds(&self) -> Size { - Size::ZERO + fn update( + &mut self, + _new_bounds: Size, + _new_font: Self::Font, + _new_size: Pixels, + _new_line_height: text::LineHeight, + _new_highlighter: &mut impl text::Highlighter, + ) { } - fn hit_test(&self, _point: Point) -> Option<text::Hit> { - None + fn highlight<H: text::Highlighter>( + &mut self, + _font: Self::Font, + _highlighter: &mut H, + _format_highlight: impl Fn( + &H::Highlight, + ) -> text::highlighter::Format<Self::Font>, + ) { } } diff --git a/core/src/text.rs b/core/src/text.rs index 0e3617b1..546d0b5c 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -1,4 +1,13 @@ //! Draw and interact with text. +mod paragraph; + +pub mod editor; +pub mod highlighter; + +pub use editor::Editor; +pub use highlighter::Highlighter; +pub use paragraph::Paragraph; + use crate::alignment; use crate::{Color, Pixels, Point, Size}; @@ -126,6 +135,33 @@ impl Hit { } } +/// The difference detected in some text. +/// +/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some +/// [`Text`]. +/// +/// [`compare`]: Paragraph::compare +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Difference { + /// No difference. + /// + /// The text can be reused as it is! + None, + + /// A bounds difference. + /// + /// This normally means a relayout is necessary, but the shape of the text can + /// be reused. + Bounds, + + /// A shape difference. + /// + /// The contents, alignment, sizes, fonts, or any other essential attributes + /// of the shape of the text have changed. A complete reshape and relayout of + /// the text is necessary. + Shape, +} + /// A renderer capable of measuring and drawing [`Text`]. pub trait Renderer: crate::Renderer { /// The font type used. @@ -134,6 +170,9 @@ pub trait Renderer: crate::Renderer { /// The [`Paragraph`] of this [`Renderer`]. type Paragraph: Paragraph<Font = Self::Font> + 'static; + /// The [`Editor`] of this [`Renderer`]. + type Editor: Editor<Font = Self::Font> + 'static; + /// The icon font of the backend. const ICON_FONT: Self::Font; @@ -156,33 +195,6 @@ pub trait Renderer: crate::Renderer { /// Loads a [`Self::Font`] from its bytes. fn load_font(&mut self, font: Cow<'static, [u8]>); - /// Creates a new [`Paragraph`] laid out with the given [`Text`]. - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph; - - /// Lays out the given [`Paragraph`] with some new boundaries. - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ); - - /// Updates a [`Paragraph`] to match the given [`Text`], if needed. - fn update_paragraph( - &self, - paragraph: &mut Self::Paragraph, - text: Text<'_, Self::Font>, - ) { - match compare(paragraph, text) { - Difference::None => {} - Difference::Bounds => { - self.resize_paragraph(paragraph, text.bounds); - } - Difference::Shape => { - *paragraph = self.create_paragraph(text); - } - } - } - /// Draws the given [`Paragraph`] at the given position and with the given /// [`Color`]. fn fill_paragraph( @@ -192,6 +204,15 @@ pub trait Renderer: crate::Renderer { color: Color, ); + /// Draws the given [`Editor`] at the given position and with the given + /// [`Color`]. + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + ); + /// Draws the given [`Text`] at the given position and with the given /// [`Color`]. fn fill_text( @@ -201,101 +222,3 @@ pub trait Renderer: crate::Renderer { color: Color, ); } -/// A text paragraph. -pub trait Paragraph: Default { - /// The font of this [`Paragraph`]. - type Font; - - /// Returns the content of the [`Paragraph`]. - fn content(&self) -> &str; - - /// Returns the text size of the [`Paragraph`]. - fn text_size(&self) -> Pixels; - - /// Returns the [`LineHeight`] of the [`Paragraph`]. - fn line_height(&self) -> LineHeight; - - /// Returns the [`Self::Font`] of the [`Paragraph`]. - fn font(&self) -> Self::Font; - - /// Returns the [`Shaping`] strategy of the [`Paragraph`]. - fn shaping(&self) -> Shaping; - - /// Returns the horizontal alignment of the [`Paragraph`]. - fn horizontal_alignment(&self) -> alignment::Horizontal; - - /// Returns the vertical alignment of the [`Paragraph`]. - fn vertical_alignment(&self) -> alignment::Vertical; - - /// Returns the boundaries of the [`Paragraph`]. - fn bounds(&self) -> Size; - - /// Returns the minimum boundaries that can fit the contents of the - /// [`Paragraph`]. - fn min_bounds(&self) -> Size; - - /// Tests whether the provided point is within the boundaries of the - /// [`Paragraph`], returning information about the nearest character. - fn hit_test(&self, point: Point) -> Option<Hit>; - - /// Returns the distance to the given grapheme index in the [`Paragraph`]. - fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; - - /// Returns the minimum width that can fit the contents of the [`Paragraph`]. - fn min_width(&self) -> f32 { - self.min_bounds().width - } - - /// Returns the minimum height that can fit the contents of the [`Paragraph`]. - fn min_height(&self) -> f32 { - self.min_bounds().height - } -} - -/// The difference detected in some text. -/// -/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some -/// [`Text`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Difference { - /// No difference. - /// - /// The text can be reused as it is! - None, - - /// A bounds difference. - /// - /// This normally means a relayout is necessary, but the shape of the text can - /// be reused. - Bounds, - - /// A shape difference. - /// - /// The contents, alignment, sizes, fonts, or any other essential attributes - /// of the shape of the text have changed. A complete reshape and relayout of - /// the text is necessary. - Shape, -} - -/// Compares a [`Paragraph`] with some desired [`Text`] and returns the -/// [`Difference`]. -pub fn compare<Font: PartialEq>( - paragraph: &impl Paragraph<Font = Font>, - text: Text<'_, Font>, -) -> Difference { - if paragraph.content() != text.content - || paragraph.text_size() != text.size - || paragraph.line_height().to_absolute(text.size) - != text.line_height.to_absolute(text.size) - || paragraph.font() != text.font - || paragraph.shaping() != text.shaping - || paragraph.horizontal_alignment() != text.horizontal_alignment - || paragraph.vertical_alignment() != text.vertical_alignment - { - Difference::Shape - } else if paragraph.bounds() != text.bounds { - Difference::Bounds - } else { - Difference::None - } -} diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs new file mode 100644 index 00000000..f3c6e342 --- /dev/null +++ b/core/src/text/editor.rs @@ -0,0 +1,181 @@ +//! Edit text. +use crate::text::highlighter::{self, Highlighter}; +use crate::text::LineHeight; +use crate::{Pixels, Point, Rectangle, Size}; + +use std::sync::Arc; + +/// A component that can be used by widgets to edit multi-line text. +pub trait Editor: Sized + Default { + /// The font of the [`Editor`]. + type Font: Copy + PartialEq + Default; + + /// Creates a new [`Editor`] laid out with the given text. + fn with_text(text: &str) -> Self; + + /// Returns the current [`Cursor`] of the [`Editor`]. + fn cursor(&self) -> Cursor; + + /// Returns the current cursor position of the [`Editor`]. + /// + /// Line and column, respectively. + fn cursor_position(&self) -> (usize, usize); + + /// Returns the current selected text of the [`Editor`]. + fn selection(&self) -> Option<String>; + + /// Returns the text of the given line in the [`Editor`], if it exists. + fn line(&self, index: usize) -> Option<&str>; + + /// Returns the amount of lines in the [`Editor`]. + fn line_count(&self) -> usize; + + /// Performs an [`Action`] on the [`Editor`]. + fn perform(&mut self, action: Action); + + /// Returns the current boundaries of the [`Editor`]. + fn bounds(&self) -> Size; + + /// Updates the [`Editor`] with some new attributes. + fn update( + &mut self, + new_bounds: Size, + new_font: Self::Font, + new_size: Pixels, + new_line_height: LineHeight, + new_highlighter: &mut impl Highlighter, + ); + + /// Runs a text [`Highlighter`] in the [`Editor`]. + fn highlight<H: Highlighter>( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, + ); +} + +/// An interaction with an [`Editor`]. +#[derive(Debug, Clone, PartialEq)] +pub enum Action { + /// Apply a [`Motion`]. + Move(Motion), + /// Select text with a given [`Motion`]. + Select(Motion), + /// Select the word at the current cursor. + SelectWord, + /// Select the line at the current cursor. + SelectLine, + /// Perform an [`Edit`]. + Edit(Edit), + /// Click the [`Editor`] at the given [`Point`]. + Click(Point), + /// Drag the mouse on the [`Editor`] to the given [`Point`]. + Drag(Point), + /// Scroll the [`Editor`] a certain amount of lines. + Scroll { + /// The amount of lines to scroll. + lines: i32, + }, +} + +impl Action { + /// Returns whether the [`Action`] is an editing action. + pub fn is_edit(&self) -> bool { + matches!(self, Self::Edit(_)) + } +} + +/// An action that edits text. +#[derive(Debug, Clone, PartialEq)] +pub enum Edit { + /// Insert the given character. + Insert(char), + /// Paste the given text. + Paste(Arc<String>), + /// Break the current line. + Enter, + /// Delete the previous character. + Backspace, + /// Delete the next character. + Delete, +} + +/// A cursor movement. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Motion { + /// Move left. + Left, + /// Move right. + Right, + /// Move up. + Up, + /// Move down. + Down, + /// Move to the left boundary of a word. + WordLeft, + /// Move to the right boundary of a word. + WordRight, + /// Move to the start of the line. + Home, + /// Move to the end of the line. + End, + /// Move to the start of the previous window. + PageUp, + /// Move to the start of the next window. + PageDown, + /// Move to the start of the text. + DocumentStart, + /// Move to the end of the text. + DocumentEnd, +} + +impl Motion { + /// Widens the [`Motion`], if possible. + pub fn widen(self) -> Self { + match self { + Self::Left => Self::WordLeft, + Self::Right => Self::WordRight, + Self::Home => Self::DocumentStart, + Self::End => Self::DocumentEnd, + _ => self, + } + } + + /// Returns the [`Direction`] of the [`Motion`]. + pub fn direction(&self) -> Direction { + match self { + Self::Left + | Self::Up + | Self::WordLeft + | Self::Home + | Self::PageUp + | Self::DocumentStart => Direction::Left, + Self::Right + | Self::Down + | Self::WordRight + | Self::End + | Self::PageDown + | Self::DocumentEnd => Direction::Right, + } + } +} + +/// A direction in some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + /// <- + Left, + /// -> + Right, +} + +/// The cursor of an [`Editor`]. +#[derive(Debug, Clone)] +pub enum Cursor { + /// Cursor without a selection + Caret(Point), + + /// Cursor selecting a range of text + Selection(Vec<Rectangle>), +} diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs new file mode 100644 index 00000000..a0535228 --- /dev/null +++ b/core/src/text/highlighter.rs @@ -0,0 +1,88 @@ +//! Highlight text. +use crate::Color; + +use std::ops::Range; + +/// A type capable of highlighting text. +/// +/// A [`Highlighter`] highlights lines in sequence. When a line changes, +/// it must be notified and the lines after the changed one must be fed +/// again to the [`Highlighter`]. +pub trait Highlighter: 'static { + /// The settings to configure the [`Highlighter`]. + type Settings: PartialEq + Clone; + + /// The output of the [`Highlighter`]. + type Highlight; + + /// The highlight iterator type. + type Iterator<'a>: Iterator<Item = (Range<usize>, Self::Highlight)> + where + Self: 'a; + + /// Creates a new [`Highlighter`] from its [`Self::Settings`]. + fn new(settings: &Self::Settings) -> Self; + + /// Updates the [`Highlighter`] with some new [`Self::Settings`]. + fn update(&mut self, new_settings: &Self::Settings); + + /// Notifies the [`Highlighter`] that the line at the given index has changed. + fn change_line(&mut self, line: usize); + + /// Highlights the given line. + /// + /// If a line changed prior to this, the first line provided here will be the + /// line that changed. + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>; + + /// Returns the current line of the [`Highlighter`]. + /// + /// If `change_line` has been called, this will normally be the least index + /// that changed. + fn current_line(&self) -> usize; +} + +/// A highlighter that highlights nothing. +#[derive(Debug, Clone, Copy)] +pub struct PlainText; + +impl Highlighter for PlainText { + type Settings = (); + type Highlight = (); + + type Iterator<'a> = std::iter::Empty<(Range<usize>, Self::Highlight)>; + + fn new(_settings: &Self::Settings) -> Self { + Self + } + + fn update(&mut self, _new_settings: &Self::Settings) {} + + fn change_line(&mut self, _line: usize) {} + + fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> { + std::iter::empty() + } + + fn current_line(&self) -> usize { + usize::MAX + } +} + +/// The format of some text. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Format<Font> { + /// The [`Color`] of the text. + pub color: Option<Color>, + /// The `Font` of the text. + pub font: Option<Font>, +} + +impl<Font> Default for Format<Font> { + fn default() -> Self { + Self { + color: None, + font: None, + } + } +} diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs new file mode 100644 index 00000000..de1fb74d --- /dev/null +++ b/core/src/text/paragraph.rs @@ -0,0 +1,59 @@ +use crate::alignment; +use crate::text::{Difference, Hit, Text}; +use crate::{Point, Size}; + +/// A text paragraph. +pub trait Paragraph: Sized + Default { + /// The font of this [`Paragraph`]. + type Font: Copy + PartialEq; + + /// Creates a new [`Paragraph`] laid out with the given [`Text`]. + fn with_text(text: Text<'_, Self::Font>) -> Self; + + /// Lays out the [`Paragraph`] with some new boundaries. + fn resize(&mut self, new_bounds: Size); + + /// Compares the [`Paragraph`] with some desired [`Text`] and returns the + /// [`Difference`]. + fn compare(&self, text: Text<'_, Self::Font>) -> Difference; + + /// Returns the horizontal alignment of the [`Paragraph`]. + fn horizontal_alignment(&self) -> alignment::Horizontal; + + /// Returns the vertical alignment of the [`Paragraph`]. + fn vertical_alignment(&self) -> alignment::Vertical; + + /// Returns the minimum boundaries that can fit the contents of the + /// [`Paragraph`]. + fn min_bounds(&self) -> Size; + + /// Tests whether the provided point is within the boundaries of the + /// [`Paragraph`], returning information about the nearest character. + fn hit_test(&self, point: Point) -> Option<Hit>; + + /// Returns the distance to the given grapheme index in the [`Paragraph`]. + fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; + + /// Updates the [`Paragraph`] to match the given [`Text`], if needed. + fn update(&mut self, text: Text<'_, Self::Font>) { + match self.compare(text) { + Difference::None => {} + Difference::Bounds => { + self.resize(text.bounds); + } + Difference::Shape => { + *self = Self::with_text(text); + } + } + } + + /// Returns the minimum width that can fit the contents of the [`Paragraph`]. + fn min_width(&self) -> f32 { + self.min_bounds().width + } + + /// Returns the minimum height that can fit the contents of the [`Paragraph`]. + fn min_height(&self) -> f32 { + self.min_bounds().height + } +} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index ba98f2d8..97e0acac 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -212,19 +212,16 @@ where let State(ref mut paragraph) = state; - renderer.update_paragraph( - paragraph, - text::Text { - content, - bounds, - size, - line_height, - font, - horizontal_alignment, - vertical_alignment, - shaping, - }, - ); + paragraph.update(text::Text { + content, + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping, + }); let size = limits.resolve(paragraph.min_bounds()); |