Draft Editor API and TextEditor widget
This commit is contained in:
parent
346af3f8b0
commit
6448429103
25 changed files with 1384 additions and 92 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
use crate::{Length, Padding, Size};
|
use crate::{Length, Padding, Size};
|
||||||
|
|
||||||
/// A set of size constraints for layouting.
|
/// A set of size constraints for layouting.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Limits {
|
pub struct Limits {
|
||||||
min: Size,
|
min: Size,
|
||||||
max: Size,
|
max: Size,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
#![forbid(unsafe_code, rust_2018_idioms)]
|
#![forbid(unsafe_code, rust_2018_idioms)]
|
||||||
#![deny(
|
#![deny(
|
||||||
missing_debug_implementations,
|
missing_debug_implementations,
|
||||||
missing_docs,
|
// missing_docs,
|
||||||
unused_results,
|
unused_results,
|
||||||
clippy::extra_unused_lifetimes,
|
clippy::extra_unused_lifetimes,
|
||||||
clippy::from_over_into,
|
clippy::from_over_into,
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ impl Renderer for Null {
|
||||||
impl text::Renderer for Null {
|
impl text::Renderer for Null {
|
||||||
type Font = Font;
|
type Font = Font;
|
||||||
type Paragraph = ();
|
type Paragraph = ();
|
||||||
|
type Editor = ();
|
||||||
|
|
||||||
const ICON_FONT: Font = Font::DEFAULT;
|
const ICON_FONT: Font = Font::DEFAULT;
|
||||||
const CHECKMARK_ICON: char = '0';
|
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(
|
fn fill_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
_paragraph: Text<'_, Self::Font>,
|
_paragraph: Text<'_, Self::Font>,
|
||||||
|
|
@ -106,3 +115,32 @@ impl text::Paragraph for () {
|
||||||
None
|
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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
123
core/src/text.rs
123
core/src/text.rs
|
|
@ -1,4 +1,11 @@
|
||||||
//! Draw and interact with text.
|
//! Draw and interact with text.
|
||||||
|
mod paragraph;
|
||||||
|
|
||||||
|
pub mod editor;
|
||||||
|
|
||||||
|
pub use editor::Editor;
|
||||||
|
pub use paragraph::Paragraph;
|
||||||
|
|
||||||
use crate::alignment;
|
use crate::alignment;
|
||||||
use crate::{Color, Pixels, Point, Size};
|
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`].
|
/// A renderer capable of measuring and drawing [`Text`].
|
||||||
pub trait Renderer: crate::Renderer {
|
pub trait Renderer: crate::Renderer {
|
||||||
/// The font type used.
|
/// The font type used.
|
||||||
|
|
@ -134,6 +166,9 @@ pub trait Renderer: crate::Renderer {
|
||||||
/// The [`Paragraph`] of this [`Renderer`].
|
/// The [`Paragraph`] of this [`Renderer`].
|
||||||
type Paragraph: Paragraph<Font = Self::Font> + 'static;
|
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.
|
/// The icon font of the backend.
|
||||||
const ICON_FONT: Self::Font;
|
const ICON_FONT: Self::Font;
|
||||||
|
|
||||||
|
|
@ -165,6 +200,13 @@ pub trait Renderer: crate::Renderer {
|
||||||
color: Color,
|
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
|
/// Draws the given [`Text`] at the given position and with the given
|
||||||
/// [`Color`].
|
/// [`Color`].
|
||||||
fn fill_text(
|
fn fill_text(
|
||||||
|
|
@ -174,84 +216,3 @@ pub trait Renderer: crate::Renderer {
|
||||||
color: Color,
|
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,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
68
core/src/text/editor.rs
Normal file
68
core/src/text/editor.rs
Normal file
|
|
@ -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>),
|
||||||
|
}
|
||||||
59
core/src/text/paragraph.rs
Normal file
59
core/src/text/paragraph.rs
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
10
examples/editor/Cargo.toml
Normal file
10
examples/editor/Cargo.toml
Normal file
|
|
@ -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"]
|
||||||
49
examples/editor/src/main.rs
Normal file
49
examples/editor/src/main.rs
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -66,6 +66,13 @@ impl<T: Damage> Damage for Primitive<T> {
|
||||||
|
|
||||||
bounds.expand(1.5)
|
bounds.expand(1.5)
|
||||||
}
|
}
|
||||||
|
Self::Editor {
|
||||||
|
editor, position, ..
|
||||||
|
} => {
|
||||||
|
let bounds = Rectangle::new(*position, editor.bounds);
|
||||||
|
|
||||||
|
bounds.expand(1.5)
|
||||||
|
}
|
||||||
Self::Quad { bounds, .. }
|
Self::Quad { bounds, .. }
|
||||||
| Self::Image { bounds, .. }
|
| Self::Image { bounds, .. }
|
||||||
| Self::Svg { bounds, .. } => bounds.expand(1.0),
|
| Self::Svg { bounds, .. } => bounds.expand(1.0),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use crate::core::image;
|
||||||
use crate::core::svg;
|
use crate::core::svg;
|
||||||
use crate::core::text;
|
use crate::core::text;
|
||||||
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
|
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
|
||||||
|
use crate::text::editor;
|
||||||
use crate::text::paragraph;
|
use crate::text::paragraph;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -41,6 +42,15 @@ pub enum Primitive<T> {
|
||||||
/// The color of the paragraph.
|
/// The color of the paragraph.
|
||||||
color: Color,
|
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
|
/// A quad primitive
|
||||||
Quad {
|
Quad {
|
||||||
/// The bounds of the quad
|
/// The bounds of the quad
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ where
|
||||||
{
|
{
|
||||||
type Font = Font;
|
type Font = Font;
|
||||||
type Paragraph = text::Paragraph;
|
type Paragraph = text::Paragraph;
|
||||||
|
type Editor = text::Editor;
|
||||||
|
|
||||||
const ICON_FONT: Font = Font::with_name("Iced-Icons");
|
const ICON_FONT: Font = Font::with_name("Iced-Icons");
|
||||||
const CHECKMARK_ICON: char = '\u{f00c}';
|
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(
|
fn fill_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
text: Text<'_, Self::Font>,
|
text: Text<'_, Self::Font>,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod editor;
|
||||||
pub mod paragraph;
|
pub mod paragraph;
|
||||||
|
|
||||||
pub use cache::Cache;
|
pub use cache::Cache;
|
||||||
|
pub use editor::Editor;
|
||||||
pub use paragraph::Paragraph;
|
pub use paragraph::Paragraph;
|
||||||
|
|
||||||
pub use cosmic_text;
|
pub use cosmic_text;
|
||||||
|
|
|
||||||
327
graphics/src/text/editor.rs
Normal file
327
graphics/src/text/editor.rs
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,7 @@ use crate::core::text::{self, Text};
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
|
Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
|
||||||
};
|
};
|
||||||
|
use crate::graphics::text::Editor;
|
||||||
use crate::graphics::text::Paragraph;
|
use crate::graphics::text::Paragraph;
|
||||||
use crate::graphics::Mesh;
|
use crate::graphics::Mesh;
|
||||||
|
|
||||||
|
|
@ -159,6 +160,7 @@ impl<T> core::Renderer for Renderer<T> {
|
||||||
impl<T> text::Renderer for Renderer<T> {
|
impl<T> text::Renderer for Renderer<T> {
|
||||||
type Font = Font;
|
type Font = Font;
|
||||||
type Paragraph = Paragraph;
|
type Paragraph = Paragraph;
|
||||||
|
type Editor = Editor;
|
||||||
|
|
||||||
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
|
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
|
||||||
const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON;
|
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(
|
fn fill_paragraph(
|
||||||
&mut self,
|
&mut self,
|
||||||
text: &Self::Paragraph,
|
paragraph: &Self::Paragraph,
|
||||||
position: Point,
|
position: Point,
|
||||||
color: Color,
|
color: Color,
|
||||||
) {
|
) {
|
||||||
delegate!(
|
delegate!(
|
||||||
self,
|
self,
|
||||||
renderer,
|
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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ pub mod rule;
|
||||||
pub mod scrollable;
|
pub mod scrollable;
|
||||||
pub mod slider;
|
pub mod slider;
|
||||||
pub mod svg;
|
pub mod svg;
|
||||||
|
pub mod text_editor;
|
||||||
pub mod text_input;
|
pub mod text_input;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod toggler;
|
pub mod toggler;
|
||||||
|
|
|
||||||
47
style/src/text_editor.rs
Normal file
47
style/src/text_editor.rs
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ use crate::rule;
|
||||||
use crate::scrollable;
|
use crate::scrollable;
|
||||||
use crate::slider;
|
use crate::slider;
|
||||||
use crate::svg;
|
use crate::svg;
|
||||||
|
use crate::text_editor;
|
||||||
use crate::text_input;
|
use crate::text_input;
|
||||||
use crate::toggler;
|
use crate::toggler;
|
||||||
|
|
||||||
|
|
@ -1174,3 +1175,115 @@ impl text_input::StyleSheet for Theme {
|
||||||
self.placeholder_color(style)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -383,6 +383,31 @@ impl Backend {
|
||||||
clip_mask,
|
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 {
|
Primitive::Text {
|
||||||
content,
|
content,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use crate::core::alignment;
|
||||||
use crate::core::text::{LineHeight, Shaping};
|
use crate::core::text::{LineHeight, Shaping};
|
||||||
use crate::core::{Color, Font, Pixels, Point, Rectangle};
|
use crate::core::{Color, Font, Pixels, Point, Rectangle};
|
||||||
use crate::graphics::text::cache::{self, Cache};
|
use crate::graphics::text::cache::{self, Cache};
|
||||||
|
use crate::graphics::text::editor;
|
||||||
use crate::graphics::text::font_system;
|
use crate::graphics::text::font_system;
|
||||||
use crate::graphics::text::paragraph;
|
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(
|
pub fn draw_cached(
|
||||||
&mut self,
|
&mut self,
|
||||||
content: &str,
|
content: &str,
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,25 @@ impl<'a> Layer<'a> {
|
||||||
} => {
|
} => {
|
||||||
let layer = &mut layers[current_layer];
|
let layer = &mut layers[current_layer];
|
||||||
|
|
||||||
layer.text.push(Text::Managed {
|
layer.text.push(Text::Paragraph {
|
||||||
paragraph: paragraph.clone(),
|
paragraph: paragraph.clone(),
|
||||||
position: *position + translation,
|
position: *position + translation,
|
||||||
color: *color,
|
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 {
|
Primitive::Text {
|
||||||
content,
|
content,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
use crate::core::alignment;
|
use crate::core::alignment;
|
||||||
use crate::core::text;
|
use crate::core::text;
|
||||||
use crate::core::{Color, Font, Pixels, Point, Rectangle};
|
use crate::core::{Color, Font, Pixels, Point, Rectangle};
|
||||||
|
use crate::graphics::text::editor;
|
||||||
use crate::graphics::text::paragraph;
|
use crate::graphics::text::paragraph;
|
||||||
|
|
||||||
/// A paragraph of text.
|
/// A paragraph of text.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Text<'a> {
|
pub enum Text<'a> {
|
||||||
Managed {
|
Paragraph {
|
||||||
paragraph: paragraph::Weak,
|
paragraph: paragraph::Weak,
|
||||||
position: Point,
|
position: Point,
|
||||||
color: Color,
|
color: Color,
|
||||||
},
|
},
|
||||||
|
Editor {
|
||||||
|
editor: editor::Weak,
|
||||||
|
position: Point,
|
||||||
|
color: Color,
|
||||||
|
},
|
||||||
Cached(Cached<'a>),
|
Cached(Cached<'a>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::core::alignment;
|
||||||
use crate::core::{Rectangle, Size};
|
use crate::core::{Rectangle, Size};
|
||||||
use crate::graphics::color;
|
use crate::graphics::color;
|
||||||
use crate::graphics::text::cache::{self, Cache};
|
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 crate::layer::Text;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
@ -74,15 +74,19 @@ impl Pipeline {
|
||||||
|
|
||||||
enum Allocation {
|
enum Allocation {
|
||||||
Paragraph(Paragraph),
|
Paragraph(Paragraph),
|
||||||
|
Editor(Editor),
|
||||||
Cache(cache::KeyHash),
|
Cache(cache::KeyHash),
|
||||||
}
|
}
|
||||||
|
|
||||||
let allocations: Vec<_> = sections
|
let allocations: Vec<_> = sections
|
||||||
.iter()
|
.iter()
|
||||||
.map(|section| match section {
|
.map(|section| match section {
|
||||||
Text::Managed { paragraph, .. } => {
|
Text::Paragraph { paragraph, .. } => {
|
||||||
paragraph.upgrade().map(Allocation::Paragraph)
|
paragraph.upgrade().map(Allocation::Paragraph)
|
||||||
}
|
}
|
||||||
|
Text::Editor { editor, .. } => {
|
||||||
|
editor.upgrade().map(Allocation::Editor)
|
||||||
|
}
|
||||||
Text::Cached(text) => {
|
Text::Cached(text) => {
|
||||||
let (key, _) = cache.allocate(
|
let (key, _) = cache.allocate(
|
||||||
font_system,
|
font_system,
|
||||||
|
|
@ -117,7 +121,7 @@ impl Pipeline {
|
||||||
vertical_alignment,
|
vertical_alignment,
|
||||||
color,
|
color,
|
||||||
) = match section {
|
) = match section {
|
||||||
Text::Managed {
|
Text::Paragraph {
|
||||||
position, color, ..
|
position, color, ..
|
||||||
} => {
|
} => {
|
||||||
use crate::core::text::Paragraph as _;
|
use crate::core::text::Paragraph as _;
|
||||||
|
|
@ -135,6 +139,24 @@ impl Pipeline {
|
||||||
*color,
|
*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) => {
|
Text::Cached(text) => {
|
||||||
let Some(Allocation::Cache(key)) = allocation else {
|
let Some(Allocation::Cache(key)) = allocation else {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use crate::runtime::Command;
|
||||||
use crate::scrollable::{self, Scrollable};
|
use crate::scrollable::{self, Scrollable};
|
||||||
use crate::slider::{self, Slider};
|
use crate::slider::{self, Slider};
|
||||||
use crate::text::{self, Text};
|
use crate::text::{self, Text};
|
||||||
|
use crate::text_editor::{self, TextEditor};
|
||||||
use crate::text_input::{self, TextInput};
|
use crate::text_input::{self, TextInput};
|
||||||
use crate::toggler::{self, Toggler};
|
use crate::toggler::{self, Toggler};
|
||||||
use crate::tooltip::{self, Tooltip};
|
use crate::tooltip::{self, Tooltip};
|
||||||
|
|
@ -206,6 +207,20 @@ where
|
||||||
TextInput::new(placeholder, value)
|
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`].
|
/// Creates a new [`Slider`].
|
||||||
///
|
///
|
||||||
/// [`Slider`]: crate::Slider
|
/// [`Slider`]: crate::Slider
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
)]
|
)]
|
||||||
#![forbid(unsafe_code, rust_2018_idioms)]
|
#![forbid(unsafe_code, rust_2018_idioms)]
|
||||||
#![deny(
|
#![deny(
|
||||||
missing_debug_implementations,
|
// missing_debug_implementations,
|
||||||
missing_docs,
|
// missing_docs,
|
||||||
unused_results,
|
unused_results,
|
||||||
clippy::extra_unused_lifetimes,
|
clippy::extra_unused_lifetimes,
|
||||||
clippy::from_over_into,
|
clippy::from_over_into,
|
||||||
|
|
@ -41,6 +41,7 @@ pub mod scrollable;
|
||||||
pub mod slider;
|
pub mod slider;
|
||||||
pub mod space;
|
pub mod space;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
pub mod text_editor;
|
||||||
pub mod text_input;
|
pub mod text_input;
|
||||||
pub mod toggler;
|
pub mod toggler;
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
|
||||||
457
widget/src/text_editor.rs
Normal file
457
widget/src/text_editor.rs
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue