summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--core/src/layout/limits.rs2
-rw-r--r--core/src/lib.rs2
-rw-r--r--core/src/renderer/null.rs38
-rw-r--r--core/src/text.rs123
-rw-r--r--core/src/text/editor.rs68
-rw-r--r--core/src/text/paragraph.rs59
-rw-r--r--examples/editor/Cargo.toml10
-rw-r--r--examples/editor/src/main.rs49
-rw-r--r--graphics/src/damage.rs7
-rw-r--r--graphics/src/primitive.rs10
-rw-r--r--graphics/src/renderer.rs14
-rw-r--r--graphics/src/text.rs2
-rw-r--r--graphics/src/text/editor.rs327
-rw-r--r--renderer/src/lib.rs19
-rw-r--r--style/src/lib.rs1
-rw-r--r--style/src/text_editor.rs47
-rw-r--r--style/src/theme.rs113
-rw-r--r--tiny_skia/src/backend.rs25
-rw-r--r--tiny_skia/src/text.rs32
-rw-r--r--wgpu/src/layer.rs15
-rw-r--r--wgpu/src/layer/text.rs8
-rw-r--r--wgpu/src/text.rs28
-rw-r--r--widget/src/helpers.rs15
-rw-r--r--widget/src/lib.rs5
-rw-r--r--widget/src/text_editor.rs457
25 files changed, 1384 insertions, 92 deletions
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/lib.rs b/core/src/lib.rs
index 1bfba7bd..13a9f06b 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -12,7 +12,7 @@
#![forbid(unsafe_code, rust_2018_idioms)]
#![deny(
missing_debug_implementations,
- missing_docs,
+ // missing_docs,
unused_results,
clippy::extra_unused_lifetimes,
clippy::from_over_into,
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs
index 0ffd3649..adf75969 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';
@@ -66,6 +67,14 @@ impl text::Renderer for Null {
) {
}
+ fn fill_editor(
+ &mut self,
+ _editor: &Self::Editor,
+ _position: Point,
+ _color: Color,
+ ) {
+ }
+
fn fill_text(
&mut self,
_paragraph: Text<'_, Self::Font>,
@@ -106,3 +115,32 @@ impl text::Paragraph for () {
None
}
}
+
+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 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,
+ ) {
+ }
+}
diff --git a/core/src/text.rs b/core/src/text.rs
index ff85696e..5aacbcc5 100644
--- a/core/src/text.rs
+++ b/core/src/text.rs
@@ -1,4 +1,11 @@
//! Draw and interact with text.
+mod paragraph;
+
+pub mod editor;
+
+pub use editor::Editor;
+pub use paragraph::Paragraph;
+
use crate::alignment;
use crate::{Color, Pixels, Point, Size};
@@ -126,6 +133,31 @@ impl Hit {
}
}
+/// 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,
+}
+
/// A renderer capable of measuring and drawing [`Text`].
pub trait Renderer: crate::Renderer {
/// The font type used.
@@ -134,6 +166,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;
@@ -165,6 +200,13 @@ pub trait Renderer: crate::Renderer {
color: 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(
@@ -174,84 +216,3 @@ pub trait Renderer: crate::Renderer {
color: Color,
);
}
-
-/// 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
- }
-}
-
-/// 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,
-}
diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs
new file mode 100644
index 00000000..a4fd0ec1
--- /dev/null
+++ b/core/src/text/editor.rs
@@ -0,0 +1,68 @@
+use crate::text::LineHeight;
+use crate::{Pixels, Point, Rectangle, Size};
+
+pub trait Editor: Sized + Default {
+ type Font: Copy + PartialEq + Default;
+
+ /// Creates a new [`Editor`] laid out with the given text.
+ fn with_text(text: &str) -> Self;
+
+ fn cursor(&self) -> Cursor;
+
+ fn perform(&mut self, action: Action);
+
+ /// Returns the current boundaries of the [`Editor`].
+ fn bounds(&self) -> Size;
+
+ /// Returns the minimum boundaries that can fit the contents of the
+ /// [`Editor`].
+ fn min_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,
+ );
+
+ /// Returns the minimum width that can fit the contents of the [`Editor`].
+ fn min_width(&self) -> f32 {
+ self.min_bounds().width
+ }
+
+ /// Returns the minimum height that can fit the contents of the [`Editor`].
+ fn min_height(&self) -> f32 {
+ self.min_bounds().height
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Action {
+ MoveLeft,
+ MoveRight,
+ MoveUp,
+ MoveDown,
+ MoveLeftWord,
+ MoveRightWord,
+ MoveHome,
+ MoveEnd,
+ SelectWord,
+ SelectLine,
+ Insert(char),
+ Backspace,
+ Delete,
+ Click(Point),
+ Drag(Point),
+}
+
+/// 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/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/examples/editor/Cargo.toml b/examples/editor/Cargo.toml
new file mode 100644
index 00000000..528cf23c
--- /dev/null
+++ b/examples/editor/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "editor"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced.workspace = true
+iced.features = ["debug"] \ No newline at end of file
diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs
new file mode 100644
index 00000000..50989ac5
--- /dev/null
+++ b/examples/editor/src/main.rs
@@ -0,0 +1,49 @@
+use iced::widget::{container, text_editor};
+use iced::{Element, Font, Sandbox, Settings};
+
+pub fn main() -> iced::Result {
+ Editor::run(Settings::default())
+}
+
+struct Editor {
+ content: text_editor::Content,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ Edit(text_editor::Action),
+}
+
+impl Sandbox for Editor {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self {
+ content: text_editor::Content::with(include_str!(
+ "../../../README.md"
+ )),
+ }
+ }
+
+ fn title(&self) -> String {
+ String::from("Editor - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::Edit(action) => {
+ self.content.edit(action);
+ }
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ container(
+ text_editor(&self.content)
+ .on_edit(Message::Edit)
+ .font(Font::with_name("Hasklug Nerd Font Mono")),
+ )
+ .padding(20)
+ .into()
+ }
+}
diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs
index 3276c2d4..595cc274 100644
--- a/graphics/src/damage.rs
+++ b/graphics/src/damage.rs
@@ -66,6 +66,13 @@ impl<T: Damage> Damage for Primitive<T> {
bounds.expand(1.5)
}
+ Self::Editor {
+ editor, position, ..
+ } => {
+ let bounds = Rectangle::new(*position, editor.bounds);
+
+ bounds.expand(1.5)
+ }
Self::Quad { bounds, .. }
| Self::Image { bounds, .. }
| Self::Svg { bounds, .. } => bounds.expand(1.0),
diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs
index 8a97e6e7..ce0b734b 100644
--- a/graphics/src/primitive.rs
+++ b/graphics/src/primitive.rs
@@ -4,6 +4,7 @@ use crate::core::image;
use crate::core::svg;
use crate::core::text;
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
+use crate::text::editor;
use crate::text::paragraph;
use std::sync::Arc;
@@ -41,6 +42,15 @@ pub enum Primitive<T> {
/// The color of the paragraph.
color: Color,
},
+ /// An editor primitive
+ Editor {
+ /// The [`editor::Weak`] reference.
+ editor: editor::Weak,
+ /// The position of the paragraph.
+ position: Point,
+ /// The color of the paragraph.
+ color: Color,
+ },
/// A quad primitive
Quad {
/// The bounds of the quad
diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs
index c5033d36..9b699183 100644
--- a/graphics/src/renderer.rs
+++ b/graphics/src/renderer.rs
@@ -141,6 +141,7 @@ where
{
type Font = Font;
type Paragraph = text::Paragraph;
+ type Editor = text::Editor;
const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = '\u{f00c}';
@@ -171,6 +172,19 @@ where
});
}
+ fn fill_editor(
+ &mut self,
+ editor: &Self::Editor,
+ position: Point,
+ color: Color,
+ ) {
+ self.primitives.push(Primitive::Editor {
+ editor: editor.downgrade(),
+ position,
+ color,
+ });
+ }
+
fn fill_text(
&mut self,
text: Text<'_, Self::Font>,
diff --git a/graphics/src/text.rs b/graphics/src/text.rs
index f5ccaf52..280e4f01 100644
--- a/graphics/src/text.rs
+++ b/graphics/src/text.rs
@@ -1,7 +1,9 @@
pub mod cache;
+pub mod editor;
pub mod paragraph;
pub use cache::Cache;
+pub use editor::Editor;
pub use paragraph::Paragraph;
pub use cosmic_text;
diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs
new file mode 100644
index 00000000..53f63fea
--- /dev/null
+++ b/graphics/src/text/editor.rs
@@ -0,0 +1,327 @@
+use crate::core::text::editor::{self, Action, Cursor};
+use crate::core::text::LineHeight;
+use crate::core::{Font, Pixels, Point, Size};
+use crate::text;
+
+use cosmic_text::Edit;
+
+use std::fmt;
+use std::sync::{self, Arc};
+
+#[derive(Debug, PartialEq)]
+pub struct Editor(Option<Arc<Internal>>);
+
+struct Internal {
+ editor: cosmic_text::Editor,
+ font: Font,
+ bounds: Size,
+ min_bounds: Size,
+ version: text::Version,
+}
+
+impl Editor {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn buffer(&self) -> &cosmic_text::Buffer {
+ &self.internal().editor.buffer()
+ }
+
+ pub fn downgrade(&self) -> Weak {
+ let editor = self.internal();
+
+ Weak {
+ raw: Arc::downgrade(editor),
+ bounds: editor.bounds,
+ }
+ }
+
+ fn internal(&self) -> &Arc<Internal> {
+ self.0
+ .as_ref()
+ .expect("editor should always be initialized")
+ }
+}
+
+impl editor::Editor for Editor {
+ type Font = Font;
+
+ fn with_text(text: &str) -> Self {
+ let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
+ font_size: 1.0,
+ line_height: 1.0,
+ });
+
+ buffer.set_text(
+ text::font_system()
+ .write()
+ .expect("Write font system")
+ .raw(),
+ text,
+ cosmic_text::Attrs::new(),
+ cosmic_text::Shaping::Advanced,
+ );
+
+ Editor(Some(Arc::new(Internal {
+ editor: cosmic_text::Editor::new(buffer),
+ ..Default::default()
+ })))
+ }
+
+ fn cursor(&self) -> editor::Cursor {
+ let internal = self.internal();
+
+ match internal.editor.select_opt() {
+ Some(selection) => {
+ // TODO
+ Cursor::Selection(vec![])
+ }
+ None => {
+ let cursor = internal.editor.cursor();
+ let buffer = internal.editor.buffer();
+
+ let lines_before_cursor: usize = buffer
+ .lines
+ .iter()
+ .take(cursor.line)
+ .map(|line| {
+ line.layout_opt()
+ .as_ref()
+ .expect("Line layout should be cached")
+ .len()
+ })
+ .sum();
+
+ let line = buffer
+ .lines
+ .get(cursor.line)
+ .expect("Cursor line should be present");
+
+ let layout = line
+ .layout_opt()
+ .as_ref()
+ .expect("Line layout should be cached");
+
+ let mut lines = layout.iter().enumerate();
+
+ let (subline, offset) = lines
+ .find_map(|(i, line)| {
+ let start = line
+ .glyphs
+ .first()
+ .map(|glyph| glyph.start)
+ .unwrap_or(0);
+ let end = line
+ .glyphs
+ .last()
+ .map(|glyph| glyph.end)
+ .unwrap_or(0);
+
+ let is_cursor_after_start = start <= cursor.index;
+
+ let is_cursor_before_end = match cursor.affinity {
+ cosmic_text::Affinity::Before => {
+ cursor.index <= end
+ }
+ cosmic_text::Affinity::After => cursor.index < end,
+ };
+
+ if is_cursor_after_start && is_cursor_before_end {
+ let offset = line
+ .glyphs
+ .iter()
+ .take_while(|glyph| cursor.index > glyph.start)
+ .map(|glyph| glyph.w)
+ .sum();
+
+ Some((i, offset))
+ } else {
+ None
+ }
+ })
+ .unwrap_or((0, 0.0));
+
+ let line_height = buffer.metrics().line_height;
+
+ let scroll_offset = buffer.scroll() as f32 * line_height;
+
+ Cursor::Caret(Point::new(
+ offset,
+ (lines_before_cursor + subline) as f32 * line_height
+ - scroll_offset,
+ ))
+ }
+ }
+ }
+
+ fn perform(&mut self, action: Action) {
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ let editor =
+ self.0.take().expect("Editor should always be initialized");
+
+ // TODO: Handle multiple strong references somehow
+ let mut internal = Arc::try_unwrap(editor)
+ .expect("Editor cannot have multiple strong references");
+
+ let editor = &mut internal.editor;
+
+ let mut act = |action| editor.action(font_system.raw(), action);
+
+ match action {
+ Action::MoveLeft => act(cosmic_text::Action::Left),
+ Action::MoveRight => act(cosmic_text::Action::Right),
+ Action::MoveUp => act(cosmic_text::Action::Up),
+ Action::MoveDown => act(cosmic_text::Action::Down),
+ Action::Insert(c) => act(cosmic_text::Action::Insert(c)),
+ Action::Backspace => act(cosmic_text::Action::Backspace),
+ Action::Delete => act(cosmic_text::Action::Delete),
+ Action::Click(position) => act(cosmic_text::Action::Click {
+ x: position.x as i32,
+ y: position.y as i32,
+ }),
+ Action::Drag(position) => act(cosmic_text::Action::Drag {
+ x: position.x as i32,
+ y: position.y as i32,
+ }),
+ _ => todo!(),
+ }
+
+ editor.shape_as_needed(font_system.raw());
+
+ self.0 = Some(Arc::new(internal));
+ }
+
+ fn bounds(&self) -> Size {
+ self.internal().bounds
+ }
+
+ fn min_bounds(&self) -> Size {
+ self.internal().min_bounds
+ }
+
+ fn update(
+ &mut self,
+ new_bounds: Size,
+ new_font: Font,
+ new_size: Pixels,
+ new_line_height: LineHeight,
+ ) {
+ let editor =
+ self.0.take().expect("editor should always be initialized");
+
+ let mut internal = Arc::try_unwrap(editor)
+ .expect("Editor cannot have multiple strong references");
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ let mut changed = false;
+
+ if new_font != internal.font {
+ for line in internal.editor.buffer_mut().lines.iter_mut() {
+ let _ = line.set_attrs_list(cosmic_text::AttrsList::new(
+ text::to_attributes(new_font),
+ ));
+ }
+
+ changed = true;
+ }
+
+ let metrics = internal.editor.buffer().metrics();
+ let new_line_height = new_line_height.to_absolute(new_size);
+
+ if new_size.0 != metrics.font_size
+ || new_line_height.0 != metrics.line_height
+ {
+ internal.editor.buffer_mut().set_metrics(
+ font_system.raw(),
+ cosmic_text::Metrics::new(new_size.0, new_line_height.0),
+ );
+
+ changed = true;
+ }
+
+ if new_bounds != internal.bounds {
+ internal.editor.buffer_mut().set_size(
+ font_system.raw(),
+ new_bounds.width,
+ new_bounds.height,
+ );
+
+ internal.bounds = new_bounds;
+ changed = true;
+ }
+
+ if changed {
+ internal.min_bounds = text::measure(&internal.editor.buffer());
+ }
+
+ self.0 = Some(Arc::new(internal));
+ }
+}
+
+impl Default for Editor {
+ fn default() -> Self {
+ Self(Some(Arc::new(Internal::default())))
+ }
+}
+
+impl PartialEq for Internal {
+ fn eq(&self, other: &Self) -> bool {
+ self.font == other.font
+ && self.bounds == other.bounds
+ && self.min_bounds == other.min_bounds
+ && self.editor.buffer().metrics() == other.editor.buffer().metrics()
+ }
+}
+
+impl Default for Internal {
+ fn default() -> Self {
+ Self {
+ editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty(
+ cosmic_text::Metrics {
+ font_size: 1.0,
+ line_height: 1.0,
+ },
+ )),
+ font: Font::default(),
+ bounds: Size::ZERO,
+ min_bounds: Size::ZERO,
+ version: text::Version::default(),
+ }
+ }
+}
+
+impl fmt::Debug for Internal {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Internal")
+ .field("font", &self.font)
+ .field("bounds", &self.bounds)
+ .field("min_bounds", &self.min_bounds)
+ .finish()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Weak {
+ raw: sync::Weak<Internal>,
+ pub bounds: Size,
+}
+
+impl Weak {
+ pub fn upgrade(&self) -> Option<Editor> {
+ self.raw.upgrade().map(Some).map(Editor)
+ }
+}
+
+impl PartialEq for Weak {
+ fn eq(&self, other: &Self) -> bool {
+ match (self.raw.upgrade(), other.raw.upgrade()) {
+ (Some(p1), Some(p2)) => p1 == p2,
+ _ => false,
+ }
+ }
+}
diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs
index 73e56890..6f044af6 100644
--- a/renderer/src/lib.rs
+++ b/renderer/src/lib.rs
@@ -32,6 +32,7 @@ use crate::core::text::{self, Text};
use crate::core::{
Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
};
+use crate::graphics::text::Editor;
use crate::graphics::text::Paragraph;
use crate::graphics::Mesh;
@@ -159,6 +160,7 @@ impl<T> core::Renderer for Renderer<T> {
impl<T> text::Renderer for Renderer<T> {
type Font = Font;
type Paragraph = Paragraph;
+ type Editor = Editor;
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON;
@@ -179,14 +181,27 @@ impl<T> text::Renderer for Renderer<T> {
fn fill_paragraph(
&mut self,
- text: &Self::Paragraph,
+ paragraph: &Self::Paragraph,
position: Point,
color: Color,
) {
delegate!(
self,
renderer,
- renderer.fill_paragraph(text, position, color)
+ renderer.fill_paragraph(paragraph, position, color)
+ );
+ }
+
+ fn fill_editor(
+ &mut self,
+ editor: &Self::Editor,
+ position: Point,
+ color: Color,
+ ) {
+ delegate!(
+ self,
+ renderer,
+ renderer.fill_editor(editor, position, color)
);
}
diff --git a/style/src/lib.rs b/style/src/lib.rs
index 0c555ed8..7a97ac77 100644
--- a/style/src/lib.rs
+++ b/style/src/lib.rs
@@ -35,6 +35,7 @@ pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod svg;
+pub mod text_editor;
pub mod text_input;
pub mod theme;
pub mod toggler;
diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs
new file mode 100644
index 00000000..45c9bad8
--- /dev/null
+++ b/style/src/text_editor.rs
@@ -0,0 +1,47 @@
+//! Change the appearance of a text editor.
+use iced_core::{Background, BorderRadius, Color};
+
+/// The appearance of a text input.
+#[derive(Debug, Clone, Copy)]
+pub struct Appearance {
+ /// The [`Background`] of the text input.
+ pub background: Background,
+ /// The border radius of the text input.
+ pub border_radius: BorderRadius,
+ /// The border width of the text input.
+ pub border_width: f32,
+ /// The border [`Color`] of the text input.
+ pub border_color: Color,
+}
+
+/// A set of rules that dictate the style of a text input.
+pub trait StyleSheet {
+ /// The supported style of the [`StyleSheet`].
+ type Style: Default;
+
+ /// Produces the style of an active text input.
+ fn active(&self, style: &Self::Style) -> Appearance;
+
+ /// Produces the style of a focused text input.
+ fn focused(&self, style: &Self::Style) -> Appearance;
+
+ /// Produces the [`Color`] of the placeholder of a text input.
+ fn placeholder_color(&self, style: &Self::Style) -> Color;
+
+ /// Produces the [`Color`] of the value of a text input.
+ fn value_color(&self, style: &Self::Style) -> Color;
+
+ /// Produces the [`Color`] of the value of a disabled text input.
+ fn disabled_color(&self, style: &Self::Style) -> Color;
+
+ /// Produces the [`Color`] of the selection of a text input.
+ fn selection_color(&self, style: &Self::Style) -> Color;
+
+ /// Produces the style of an hovered text input.
+ fn hovered(&self, style: &Self::Style) -> Appearance {
+ self.focused(style)
+ }
+
+ /// Produces the style of a disabled text input.
+ fn disabled(&self, style: &Self::Style) -> Appearance;
+}
diff --git a/style/src/theme.rs b/style/src/theme.rs
index 893d7202..a1501c01 100644
--- a/style/src/theme.rs
+++ b/style/src/theme.rs
@@ -17,6 +17,7 @@ use crate::rule;
use crate::scrollable;
use crate::slider;
use crate::svg;
+use crate::text_editor;
use crate::text_input;
use crate::toggler;
@@ -1174,3 +1175,115 @@ impl text_input::StyleSheet for Theme {
self.placeholder_color(style)
}
}
+
+/// The style of a text input.
+#[derive(Default)]
+pub enum TextEditor {
+ /// The default style.
+ #[default]
+ Default,
+ /// A custom style.
+ Custom(Box<dyn text_editor::StyleSheet<Style = Theme>>),
+}
+
+impl text_editor::StyleSheet for Theme {
+ type Style = TextEditor;
+
+ fn active(&self, style: &Self::Style) -> text_editor::Appearance {
+ if let TextEditor::Custom(custom) = style {
+ return custom.active(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_editor::Appearance {
+ background: palette.background.base.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.background.strong.color,
+ }
+ }
+
+ fn hovered(&self, style: &Self::Style) -> text_editor::Appearance {
+ if let TextEditor::Custom(custom) = style {
+ return custom.hovered(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_editor::Appearance {
+ background: palette.background.base.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.background.base.text,
+ }
+ }
+
+ fn focused(&self, style: &Self::Style) -> text_editor::Appearance {
+ if let TextEditor::Custom(custom) = style {
+ return custom.focused(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_editor::Appearance {
+ background: palette.background.base.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.primary.strong.color,
+ }
+ }
+
+ fn placeholder_color(&self, style: &Self::Style) -> Color {
+ if let TextEditor::Custom(custom) = style {
+ return custom.placeholder_color(self);
+ }
+
+ let palette = self.extended_palette();
+
+ palette.background.strong.color
+ }
+
+ fn value_color(&self, style: &Self::Style) -> Color {
+ if let TextEditor::Custom(custom) = style {
+ return custom.value_color(self);
+ }
+
+ let palette = self.extended_palette();
+
+ palette.background.base.text
+ }
+
+ fn selection_color(&self, style: &Self::Style) -> Color {
+ if let TextEditor::Custom(custom) = style {
+ return custom.selection_color(self);
+ }
+
+ let palette = self.extended_palette();
+
+ palette.primary.weak.color
+ }
+
+ fn disabled(&self, style: &Self::Style) -> text_editor::Appearance {
+ if let TextEditor::Custom(custom) = style {
+ return custom.disabled(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_editor::Appearance {
+ background: palette.background.weak.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.background.strong.color,
+ }
+ }
+
+ fn disabled_color(&self, style: &Self::Style) -> Color {
+ if let TextEditor::Custom(custom) = style {
+ return custom.disabled_color(self);
+ }
+
+ self.placeholder_color(style)
+ }
+}
diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs
index 72184c8a..5f66dff2 100644
--- a/tiny_skia/src/backend.rs
+++ b/tiny_skia/src/backend.rs
@@ -383,6 +383,31 @@ impl Backend {
clip_mask,
);
}
+ Primitive::Editor {
+ editor,
+ position,
+ color,
+ } => {
+ let physical_bounds =
+ (Rectangle::new(*position, editor.bounds) + translation)
+ * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw_editor(
+ editor,
+ *position + translation,
+ *color,
+ scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
Primitive::Text {
content,
bounds,
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs
index 4f6e3941..d055c749 100644
--- a/tiny_skia/src/text.rs
+++ b/tiny_skia/src/text.rs
@@ -2,6 +2,7 @@ use crate::core::alignment;
use crate::core::text::{LineHeight, Shaping};
use crate::core::{Color, Font, Pixels, Point, Rectangle};
use crate::graphics::text::cache::{self, Cache};
+use crate::graphics::text::editor;
use crate::graphics::text::font_system;
use crate::graphics::text::paragraph;
@@ -64,6 +65,37 @@ impl Pipeline {
);
}
+ pub fn draw_editor(
+ &mut self,
+ editor: &editor::Weak,
+ position: Point,
+ color: Color,
+ scale_factor: f32,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ use crate::core::text::Editor as _;
+
+ let Some(editor) = editor.upgrade() else {
+ return;
+ };
+
+ let mut font_system = font_system().write().expect("Write font system");
+
+ draw(
+ font_system.raw(),
+ &mut self.glyph_cache,
+ editor.buffer(),
+ Rectangle::new(position, editor.min_bounds()),
+ color,
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
+
pub fn draw_cached(
&mut self,
content: &str,
diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs
index 7a5a0f7c..10b3332d 100644
--- a/wgpu/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -120,12 +120,25 @@ impl<'a> Layer<'a> {
} => {
let layer = &mut layers[current_layer];
- layer.text.push(Text::Managed {
+ layer.text.push(Text::Paragraph {
paragraph: paragraph.clone(),
position: *position + translation,
color: *color,
});
}
+ Primitive::Editor {
+ editor,
+ position,
+ color,
+ } => {
+ let layer = &mut layers[current_layer];
+
+ layer.text.push(Text::Editor {
+ editor: editor.clone(),
+ position: *position + translation,
+ color: *color,
+ });
+ }
Primitive::Text {
content,
bounds,
diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs
index b61615d6..d46b39da 100644
--- a/wgpu/src/layer/text.rs
+++ b/wgpu/src/layer/text.rs
@@ -1,16 +1,22 @@
use crate::core::alignment;
use crate::core::text;
use crate::core::{Color, Font, Pixels, Point, Rectangle};
+use crate::graphics::text::editor;
use crate::graphics::text::paragraph;
/// A paragraph of text.
#[derive(Debug, Clone)]
pub enum Text<'a> {
- Managed {
+ Paragraph {
paragraph: paragraph::Weak,
position: Point,
color: Color,
},
+ Editor {
+ editor: editor::Weak,
+ position: Point,
+ color: Color,
+ },
Cached(Cached<'a>),
}
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index 5c9f4d7e..397c38dd 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -2,7 +2,7 @@ use crate::core::alignment;
use crate::core::{Rectangle, Size};
use crate::graphics::color;
use crate::graphics::text::cache::{self, Cache};
-use crate::graphics::text::{font_system, Paragraph};
+use crate::graphics::text::{font_system, Editor, Paragraph};
use crate::layer::Text;
use std::borrow::Cow;
@@ -74,15 +74,19 @@ impl Pipeline {
enum Allocation {
Paragraph(Paragraph),
+ Editor(Editor),
Cache(cache::KeyHash),
}
let allocations: Vec<_> = sections
.iter()
.map(|section| match section {
- Text::Managed { paragraph, .. } => {
+ Text::Paragraph { paragraph, .. } => {
paragraph.upgrade().map(Allocation::Paragraph)
}
+ Text::Editor { editor, .. } => {
+ editor.upgrade().map(Allocation::Editor)
+ }
Text::Cached(text) => {
let (key, _) = cache.allocate(
font_system,
@@ -117,7 +121,7 @@ impl Pipeline {
vertical_alignment,
color,
) = match section {
- Text::Managed {
+ Text::Paragraph {
position, color, ..
} => {
use crate::core::text::Paragraph as _;
@@ -135,6 +139,24 @@ impl Pipeline {
*color,
)
}
+ Text::Editor {
+ position, color, ..
+ } => {
+ use crate::core::text::Editor as _;
+
+ let Some(Allocation::Editor(editor)) = allocation
+ else {
+ return None;
+ };
+
+ (
+ editor.buffer(),
+ Rectangle::new(*position, editor.min_bounds()),
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ *color,
+ )
+ }
Text::Cached(text) => {
let Some(Allocation::Cache(key)) = allocation else {
return None;
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 3c9c2b29..61541eac 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -16,6 +16,7 @@ use crate::runtime::Command;
use crate::scrollable::{self, Scrollable};
use crate::slider::{self, Slider};
use crate::text::{self, Text};
+use crate::text_editor::{self, TextEditor};
use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip};
@@ -206,6 +207,20 @@ where
TextInput::new(placeholder, value)
}
+/// Creates a new [`TextEditor`].
+///
+/// [`TextEditor`]: crate::TextEditor
+pub fn text_editor<'a, Message, Renderer>(
+ content: &'a text_editor::Content<Renderer>,
+) -> TextEditor<'a, Message, Renderer>
+where
+ Message: Clone,
+ Renderer: core::text::Renderer,
+ Renderer::Theme: text_editor::StyleSheet,
+{
+ TextEditor::new(content)
+}
+
/// Creates a new [`Slider`].
///
/// [`Slider`]: crate::Slider
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 7e204171..f8e5e865 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -4,8 +4,8 @@
)]
#![forbid(unsafe_code, rust_2018_idioms)]
#![deny(
- missing_debug_implementations,
- missing_docs,
+ // missing_debug_implementations,
+ // missing_docs,
unused_results,
clippy::extra_unused_lifetimes,
clippy::from_over_into,
@@ -41,6 +41,7 @@ pub mod scrollable;
pub mod slider;
pub mod space;
pub mod text;
+pub mod text_editor;
pub mod text_input;
pub mod toggler;
pub mod tooltip;
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
new file mode 100644
index 00000000..d09f2c3e
--- /dev/null
+++ b/widget/src/text_editor.rs
@@ -0,0 +1,457 @@
+use crate::core::event::{self, Event};
+use crate::core::keyboard;
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text::editor::{Cursor, Editor as _};
+use crate::core::text::{self, LineHeight};
+use crate::core::widget::{self, Widget};
+use crate::core::{
+ Clipboard, Color, Element, Length, Padding, Pixels, Point, Rectangle,
+ Shell, Vector,
+};
+
+use std::cell::RefCell;
+
+pub use crate::style::text_editor::{Appearance, StyleSheet};
+pub use text::editor::Action;
+
+pub struct TextEditor<'a, Message, Renderer = crate::Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ content: &'a Content<Renderer>,
+ font: Option<Renderer::Font>,
+ text_size: Option<Pixels>,
+ line_height: LineHeight,
+ width: Length,
+ height: Length,
+ padding: Padding,
+ style: <Renderer::Theme as StyleSheet>::Style,
+ on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
+}
+
+impl<'a, Message, Renderer> TextEditor<'a, Message, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ pub fn new(content: &'a Content<Renderer>) -> Self {
+ Self {
+ content,
+ font: None,
+ text_size: None,
+ line_height: LineHeight::default(),
+ width: Length::Fill,
+ height: Length::Fill,
+ padding: Padding::new(5.0),
+ style: Default::default(),
+ on_edit: None,
+ }
+ }
+
+ pub fn on_edit(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self {
+ self.on_edit = Some(Box::new(on_edit));
+ self
+ }
+
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
+ self.padding = padding.into();
+ self
+ }
+}
+
+pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>)
+where
+ R: text::Renderer;
+
+struct Internal<R>
+where
+ R: text::Renderer,
+{
+ editor: R::Editor,
+ is_dirty: bool,
+}
+
+impl<R> Content<R>
+where
+ R: text::Renderer,
+{
+ pub fn new() -> Self {
+ Self::with("")
+ }
+
+ pub fn with(text: &str) -> Self {
+ Self(RefCell::new(Internal {
+ editor: R::Editor::with_text(text),
+ is_dirty: true,
+ }))
+ }
+
+ pub fn edit(&mut self, action: Action) {
+ let internal = self.0.get_mut();
+
+ internal.editor.perform(action);
+ internal.is_dirty = true;
+ }
+}
+
+impl<Renderer> Default for Content<Renderer>
+where
+ Renderer: text::Renderer,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+struct State {
+ is_focused: bool,
+ is_dragging: bool,
+ last_click: Option<mouse::Click>,
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for TextEditor<'a, Message, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn tag(&self) -> widget::tree::Tag {
+ widget::tree::Tag::of::<State>()
+ }
+
+ fn state(&self) -> widget::tree::State {
+ widget::tree::State::new(State {
+ is_focused: false,
+ is_dragging: false,
+ last_click: None,
+ })
+ }
+
+ fn width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ self.height
+ }
+
+ fn layout(
+ &self,
+ _tree: &mut widget::Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> iced_renderer::core::layout::Node {
+ let mut internal = self.content.0.borrow_mut();
+
+ internal.editor.update(
+ limits.pad(self.padding).max(),
+ self.font.unwrap_or_else(|| renderer.default_font()),
+ self.text_size.unwrap_or_else(|| renderer.default_size()),
+ self.line_height,
+ );
+
+ layout::Node::new(limits.max())
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut widget::Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
+ ) -> event::Status {
+ let Some(on_edit) = self.on_edit.as_ref() else {
+ return event::Status::Ignored;
+ };
+
+ let state = tree.state.downcast_mut::<State>();
+
+ let Some(update) = Update::from_event(
+ event,
+ state,
+ layout.bounds(),
+ self.padding,
+ cursor,
+ ) else {
+ return event::Status::Ignored;
+ };
+
+ match update {
+ Update::Focus { click, action } => {
+ state.is_focused = true;
+ state.last_click = Some(click);
+ shell.publish(on_edit(action));
+ }
+ Update::Unfocus => {
+ state.is_focused = false;
+ state.is_dragging = false;
+ }
+ Update::Click { click, action } => {
+ state.last_click = Some(click);
+ state.is_dragging = true;
+ shell.publish(on_edit(action));
+ }
+ Update::StopDragging => {
+ state.is_dragging = false;
+ }
+ Update::Edit(action) => {
+ shell.publish(on_edit(action));
+ }
+ Update::Copy => {}
+ Update::Paste => if let Some(_contents) = clipboard.read() {},
+ }
+
+ event::Status::Captured
+ }
+
+ fn draw(
+ &self,
+ tree: &widget::Tree,
+ renderer: &mut Renderer,
+ theme: &<Renderer as renderer::Renderer>::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ ) {
+ let bounds = layout.bounds();
+
+ let internal = self.content.0.borrow();
+ let state = tree.state.downcast_ref::<State>();
+
+ let is_disabled = self.on_edit.is_none();
+ let is_mouse_over = cursor.is_over(bounds);
+
+ let appearance = if is_disabled {
+ theme.disabled(&self.style)
+ } else if state.is_focused {
+ theme.focused(&self.style)
+ } else if is_mouse_over {
+ theme.hovered(&self.style)
+ } else {
+ theme.active(&self.style)
+ };
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds,
+ border_radius: appearance.border_radius,
+ border_width: appearance.border_width,
+ border_color: appearance.border_color,
+ },
+ appearance.background,
+ );
+
+ renderer.fill_editor(
+ &internal.editor,
+ bounds.position()
+ + Vector::new(self.padding.left, self.padding.top),
+ style.text_color,
+ );
+
+ if state.is_focused {
+ match internal.editor.cursor() {
+ Cursor::Caret(position) => {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: position.x + bounds.x + self.padding.left,
+ y: position.y + bounds.y + self.padding.top,
+ width: 1.0,
+ height: self
+ .line_height
+ .to_absolute(self.text_size.unwrap_or_else(
+ || renderer.default_size(),
+ ))
+ .into(),
+ },
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ theme.value_color(&self.style),
+ );
+ }
+ Cursor::Selection(ranges) => {
+ for range in ranges {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: range + Vector::new(bounds.x, bounds.y),
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ theme.selection_color(&self.style),
+ );
+ }
+ }
+ }
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ _state: &widget::Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ let is_disabled = self.on_edit.is_none();
+
+ if cursor.is_over(layout.bounds()) {
+ if is_disabled {
+ mouse::Interaction::NotAllowed
+ } else {
+ mouse::Interaction::Text
+ }
+ } else {
+ mouse::Interaction::default()
+ }
+ }
+}
+
+impl<'a, Message, Renderer> From<TextEditor<'a, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Message: 'a,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(text_editor: TextEditor<'a, Message, Renderer>) -> Self {
+ Self::new(text_editor)
+ }
+}
+
+enum Update {
+ Focus { click: mouse::Click, action: Action },
+ Unfocus,
+ Click { click: mouse::Click, action: Action },
+ StopDragging,
+ Edit(Action),
+ Copy,
+ Paste,
+}
+
+impl Update {
+ fn from_event(
+ event: Event,
+ state: &State,
+ bounds: Rectangle,
+ padding: Padding,
+ cursor: mouse::Cursor,
+ ) -> Option<Self> {
+ match event {
+ Event::Mouse(event) => match event {
+ mouse::Event::ButtonPressed(mouse::Button::Left) => {
+ if let Some(cursor_position) = cursor.position_in(bounds) {
+ let cursor_position = cursor_position
+ - Vector::new(padding.top, padding.left);
+
+ if state.is_focused {
+ let click = mouse::Click::new(
+ cursor_position,
+ state.last_click,
+ );
+
+ let action = match click.kind() {
+ mouse::click::Kind::Single => {
+ Action::Click(cursor_position)
+ }
+ mouse::click::Kind::Double => {
+ Action::SelectWord
+ }
+ mouse::click::Kind::Triple => {
+ Action::SelectLine
+ }
+ };
+
+ Some(Update::Click { click, action })
+ } else {
+ Some(Update::Focus {
+ click: mouse::Click::new(cursor_position, None),
+ action: Action::Click(cursor_position),
+ })
+ }
+ } else if state.is_focused {
+ Some(Update::Unfocus)
+ } else {
+ None
+ }
+ }
+ mouse::Event::ButtonReleased(mouse::Button::Left) => {
+ Some(Update::StopDragging)
+ }
+ mouse::Event::CursorMoved { .. } if state.is_dragging => {
+ let cursor_position = cursor.position_in(bounds)?
+ - Vector::new(padding.top, padding.left);
+
+ Some(Self::Edit(Action::Drag(cursor_position)))
+ }
+ _ => None,
+ },
+ Event::Keyboard(event) => match event {
+ keyboard::Event::KeyPressed {
+ key_code,
+ modifiers,
+ } if state.is_focused => match key_code {
+ keyboard::KeyCode::Left => {
+ if platform::is_jump_modifier_pressed(modifiers) {
+ Some(Self::Edit(Action::MoveLeftWord))
+ } else {
+ Some(Self::Edit(Action::MoveLeft))
+ }
+ }
+ keyboard::KeyCode::Right => {
+ if platform::is_jump_modifier_pressed(modifiers) {
+ Some(Self::Edit(Action::MoveRightWord))
+ } else {
+ Some(Self::Edit(Action::MoveRight))
+ }
+ }
+ keyboard::KeyCode::Up => Some(Self::Edit(Action::MoveUp)),
+ keyboard::KeyCode::Down => {
+ Some(Self::Edit(Action::MoveDown))
+ }
+ keyboard::KeyCode::Backspace => {
+ Some(Self::Edit(Action::Backspace))
+ }
+ keyboard::KeyCode::Delete => {
+ Some(Self::Edit(Action::Delete))
+ }
+ keyboard::KeyCode::Escape => Some(Self::Unfocus),
+ _ => None,
+ },
+ keyboard::Event::CharacterReceived(c) if state.is_focused => {
+ Some(Self::Edit(Action::Insert(c)))
+ }
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+}
+
+mod platform {
+ use crate::core::keyboard;
+
+ pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
+ if cfg!(target_os = "macos") {
+ modifiers.alt()
+ } else {
+ modifiers.control()
+ }
+ }
+}