use crate::core::{Color, Font, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::impl_widget_property_hooks;
use crate::property_names_of;
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::undo::{TextSnapshotCommand, UndoStack};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::text_utils::floor_char_boundary;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use std::cell::RefCell;
use std::rc::Rc;
pub struct TextEdit {
base: BaseWidget,
text: String,
placeholder_text: String,
max_length: Option<usize>,
read_only: bool,
line_wrap: bool,
undo_stack: UndoStack,
history_target: Rc<RefCell<String>>,
restoring_history: bool,
pub text_changed: Signal1<String>,
}
impl TextEdit {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::TextEdit, geometry, "TextEdit"),
text: String::new(),
placeholder_text: String::new(),
max_length: None,
read_only: false,
line_wrap: true,
undo_stack: UndoStack::new(),
history_target: Rc::new(RefCell::new(String::new())),
restoring_history: false,
text_changed: Signal1::new(),
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: impl Into<String>) {
let text = text.into();
if self.text == text {
return;
}
let before = self.text.clone();
self.text = text;
if !self.restoring_history {
*self.history_target.borrow_mut() = self.text.clone();
self.undo_stack.push(Box::new(TextSnapshotCommand::new(
self.history_target.clone(),
before,
self.text.clone(),
"text_edit_text",
)));
}
self.text_changed.emit(self.text.clone());
self.base.request_redraw();
}
pub fn placeholder_text(&self) -> &str {
&self.placeholder_text
}
pub fn set_placeholder_text(&mut self, text: String) {
self.placeholder_text = text;
self.base.request_redraw();
}
pub fn max_length(&self) -> Option<usize> {
self.max_length
}
pub fn set_max_length(&mut self, max_length: Option<usize>) {
self.max_length = max_length;
if let Some(max) = max_length {
if self.text.len() > max {
let boundary = floor_char_boundary(&self.text, max);
let truncated = self.text[..boundary].to_string();
self.set_text(truncated);
}
}
}
pub fn is_read_only(&self) -> bool {
self.read_only
}
pub fn set_read_only(&mut self, read_only: bool) {
self.read_only = read_only;
self.base.request_redraw();
}
pub fn line_wrap(&self) -> bool {
self.line_wrap
}
pub fn set_line_wrap(&mut self, wrap: bool) {
self.line_wrap = wrap;
self.base.request_redraw();
}
pub fn line_count(&self) -> usize {
if self.text.is_empty() {
1
} else {
self.text.chars().filter(|&c| c == '\n').count() + 1
}
}
pub fn line_text(&self, line: usize) -> Option<&str> {
let mut start = 0;
let mut current_line = 0;
for (i, ch) in self.text.char_indices() {
if ch == '\n' {
if current_line == line {
return Some(&self.text[start..i]);
}
start = i + 1;
current_line += 1;
}
}
if current_line == line {
Some(&self.text[start..])
} else {
None
}
}
pub fn append(&mut self, text: &str) {
if text.is_empty() {
return;
}
let mut next = self.text.clone();
next.push_str(text);
self.set_text(next);
}
pub fn clear(&mut self) {
self.set_text(String::new());
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn undo(&mut self) -> bool {
if self.undo_stack.undo().is_err() {
return false;
}
self.restore_history_text();
true
}
pub fn redo(&mut self) -> bool {
if self.undo_stack.redo().is_err() {
return false;
}
self.restore_history_text();
true
}
pub fn can_undo(&self) -> bool {
self.undo_stack.can_undo()
}
pub fn can_redo(&self) -> bool {
self.undo_stack.can_redo()
}
fn restore_history_text(&mut self) {
let text = self.history_target.borrow().clone();
self.restoring_history = true;
self.text = text;
self.restoring_history = false;
self.text_changed.emit(self.text.clone());
self.base.request_redraw();
}
}
impl Widget for TextEdit {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
Size::new(200, 24)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for TextEdit {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"text" => Ok(CapabilityValue::String(self.text.clone())),
"placeholder_text" => Ok(CapabilityValue::String(self.placeholder_text.clone())),
"max_length" => match self.max_length {
Some(limit) => Ok(CapabilityValue::UInt(limit as u64)),
None => Ok(CapabilityValue::Null),
},
"read_only" => Ok(CapabilityValue::Bool(self.read_only)),
"line_wrap" => Ok(CapabilityValue::Bool(self.line_wrap)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"text" => match value {
CapabilityValue::String(text) => {
self.set_text(text);
return Ok(());
}
_ => return Err(CapabilityAccessError::TypeMismatch),
},
"placeholder_text" => match value {
CapabilityValue::String(text) => {
self.set_placeholder_text(text);
return Ok(());
}
_ => return Err(CapabilityAccessError::TypeMismatch),
},
"max_length" => match value {
CapabilityValue::UInt(limit) => {
let limit =
usize::try_from(limit).map_err(|_| CapabilityAccessError::OutOfRange)?;
self.set_max_length(Some(limit));
return Ok(());
}
_ => return Err(CapabilityAccessError::TypeMismatch),
},
"read_only" => match value {
CapabilityValue::Bool(flag) => {
self.set_read_only(flag);
return Ok(());
}
_ => return Err(CapabilityAccessError::TypeMismatch),
},
"line_wrap" => match value {
CapabilityValue::Bool(flag) => {
self.set_line_wrap(flag);
return Ok(());
}
_ => return Err(CapabilityAccessError::TypeMismatch),
},
_ => {}
}
base_property_set(self, name, value)
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"text",
"placeholder_text",
"max_length",
"read_only",
"line_wrap",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"set_text"
| "set_placeholder_text"
| "set_max_length"
| "set_read_only"
| "set_line_wrap" => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl EventHandler for TextEdit {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() || self.read_only {
return;
}
if let Event::KeyPress { key, modifiers } = event {
match *key {
8 => {
if !self.text.is_empty() {
let mut next = self.text.clone();
next.pop();
self.set_text(next);
}
}
13 => {
let mut next = self.text.clone();
next.push('\n');
self.set_text(next);
}
90 if modifiers & 2 != 0 => {
let _ = self.undo();
}
89 if modifiers & 2 != 0 => {
let _ = self.redo();
}
_ => {
if let Some(ch) = char::from_u32(*key) {
if ch.is_ascii_graphic() || ch == ' ' || ch == '\t' {
let mut next = self.text.clone();
next.push(ch);
self.set_text(next);
}
}
}
}
}
}
}
impl Draw for TextEdit {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let padding = 4;
let text_x = rect.x + padding;
let text_y = rect.y + padding;
let style = self.base.style().clone();
let theme = crate::style::resolved_theme_style("text_edit")
.or_else(|| crate::style::resolved_theme_style("line_edit"));
let field_from_theme = theme
.as_ref()
.and_then(|t| t.background_color)
.unwrap_or_else(|| Color::rgb(255, 255, 255));
let window_fill = {
let manager = crate::style::theme_manager();
manager.current_theme().map(|active| active.colors.background).unwrap_or(Color::WHITE)
};
let field = match style.background_color {
Some(resolved) if resolved != window_fill => resolved,
_ => field_from_theme,
};
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::BLACK);
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.unwrap_or_else(|| field.blend(&ink, 0.22));
context.fill_rect(rect, field);
context.draw_rect(rect, border);
let display_text = if self.text.is_empty() && !self.placeholder_text.is_empty() {
&self.placeholder_text
} else {
&self.text
};
if !display_text.is_empty() {
let text_color = if self.text.is_empty() { ink.blend(&field, 0.45) } else { ink };
context.draw_text(
Point::new(text_x, text_y),
display_text,
&Font::default(),
text_color,
HorizontalAlignment::Left,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Rect;
#[test]
fn textedit_property_route_reaches_its_own_accessors() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
use crate::widget::capability::types::CapabilityValue;
te.set("text", CapabilityValue::String("hello".to_string()))
.expect("`text` is a published, writable property");
assert_eq!(te.text(), "hello");
assert_eq!(te.get("text").unwrap(), CapabilityValue::String("hello".to_string()));
te.set("placeholder_text", CapabilityValue::String("type here".to_string()))
.expect("`placeholder_text` is writable");
assert_eq!(te.placeholder_text(), "type here");
te.set("max_length", CapabilityValue::UInt(16)).expect("`max_length` is writable");
assert_eq!(te.max_length(), Some(16));
te.set("read_only", CapabilityValue::Bool(true)).expect("`read_only` is writable");
assert!(te.is_read_only());
te.set("line_wrap", CapabilityValue::Bool(false)).expect("`line_wrap` is writable");
assert!(!te.line_wrap());
assert!(te.set("read_only", CapabilityValue::UInt(1)).is_err());
}
#[test]
fn textedit_creation_defaults() {
let te = TextEdit::new(Rect::new(0, 0, 300, 200));
assert!(te.text().is_empty());
assert!(te.placeholder_text().is_empty());
assert_eq!(te.max_length(), None);
assert!(!te.is_read_only());
assert!(te.line_wrap());
assert!(te.is_empty());
assert_eq!(te.line_count(), 1);
}
#[test]
fn textedit_set_text() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.set_text("Hello World".to_string());
assert_eq!(te.text(), "Hello World");
assert!(!te.is_empty());
}
#[test]
fn textedit_set_text_empty() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.set_text("Some text".to_string());
te.set_text(String::new());
assert!(te.text().is_empty());
}
#[test]
fn textedit_placeholder() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
assert!(te.placeholder_text().is_empty());
te.set_placeholder_text("Enter text here".to_string());
assert_eq!(te.placeholder_text(), "Enter text here");
te.set_placeholder_text(String::new());
assert!(te.placeholder_text().is_empty());
}
#[test]
fn textedit_max_length() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
assert_eq!(te.max_length(), None);
te.set_max_length(Some(10));
assert_eq!(te.max_length(), Some(10));
te.set_max_length(None);
assert_eq!(te.max_length(), None);
}
#[test]
fn textedit_max_length_truncates() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.set_text("Hello World Too Long".to_string());
te.set_max_length(Some(10));
assert_eq!(te.text().len(), 10);
}
#[test]
fn textedit_read_only() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
assert!(!te.is_read_only());
te.set_read_only(true);
assert!(te.is_read_only());
te.set_read_only(false);
assert!(!te.is_read_only());
}
#[test]
fn textedit_line_wrap() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
assert!(te.line_wrap());
te.set_line_wrap(false);
assert!(!te.line_wrap());
te.set_line_wrap(true);
assert!(te.line_wrap());
}
#[test]
fn textedit_line_count() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
assert_eq!(te.line_count(), 1);
te.set_text("Line 1\nLine 2\nLine 3".to_string());
assert_eq!(te.line_count(), 3);
}
#[test]
fn textedit_line_text() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.set_text("First\nSecond\nThird".to_string());
assert_eq!(te.line_text(0), Some("First"));
assert_eq!(te.line_text(1), Some("Second"));
assert_eq!(te.line_text(2), Some("Third"));
assert_eq!(te.line_text(5), None);
}
#[test]
fn textedit_append() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.append("Hello");
assert_eq!(te.text(), "Hello");
te.append(" World");
assert_eq!(te.text(), "Hello World");
}
#[test]
fn textedit_undo_redo_restores_text() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.set_text("one");
te.set_text("two");
assert!(te.undo());
assert_eq!(te.text(), "one");
assert!(te.redo());
assert_eq!(te.text(), "two");
}
#[test]
fn textedit_control_z_and_control_y_drive_history() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.set_text("before");
te.set_text("after");
te.handle_event(&Event::key_press(90, 2));
assert_eq!(te.text(), "before");
te.handle_event(&Event::key_press(89, 2));
assert_eq!(te.text(), "after");
}
#[test]
fn textedit_clear() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.set_text("Some text".to_string());
te.clear();
assert!(te.is_empty());
}
#[test]
fn textedit_geometry_delegation() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
te.set_geometry(Rect::new(10, 10, 400, 300));
assert_eq!(te.geometry(), Rect::new(10, 10, 400, 300));
}
#[test]
fn textedit_visibility() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
assert!(te.is_visible());
te.hide();
assert!(!te.is_visible());
te.show();
assert!(te.is_visible());
}
#[test]
fn textedit_enabled() {
let mut te = TextEdit::new(Rect::new(0, 0, 300, 200));
assert!(te.is_enabled());
te.set_enabled(false);
assert!(!te.is_enabled());
te.set_enabled(true);
assert!(te.is_enabled());
}
#[test]
fn textedit_id_kind() {
let te_a = TextEdit::new(Rect::new(0, 0, 100, 100));
let te_b = TextEdit::new(Rect::new(0, 0, 100, 100));
assert_ne!(te_a.id(), te_b.id());
assert_eq!(te_a.kind(), WidgetKind::TextEdit);
assert_eq!(te_b.kind(), WidgetKind::TextEdit);
}
#[test]
fn textedit_signal_accessors() {
let te = TextEdit::new(Rect::new(0, 0, 100, 100));
let _ = &te.text_changed;
}
}