Draft Editor API and TextEditor widget
This commit is contained in:
parent
346af3f8b0
commit
6448429103
25 changed files with 1384 additions and 92 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
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