use crate::core::{Color, Font, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
pub struct InplaceEditor {
base: BaseWidget,
text: String,
is_editing: bool,
original_text: String,
font_size: f32,
padding: i32,
cursor_position: usize,
pub edit_accepted: Signal1<String>,
pub edit_cancelled: GenericSignal,
}
impl InplaceEditor {
pub fn new(text: &str, geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::InplaceEditor, geometry, "InplaceEditor"),
text: text.to_string(),
is_editing: false,
original_text: text.to_string(),
font_size: 14.0,
padding: 4,
cursor_position: text.len(),
edit_accepted: Signal1::new(),
edit_cancelled: GenericSignal::new(),
}
}
pub fn start_edit(&mut self) {
if !self.is_editing {
self.is_editing = true;
self.original_text = self.text.clone();
self.cursor_position = self.text.len();
self.base.request_redraw();
}
}
pub fn finish_edit(&mut self, accept: bool) {
if !self.is_editing {
return;
}
self.is_editing = false;
if accept {
self.edit_accepted.emit(self.text.clone());
} else {
self.text = self.original_text.clone();
self.edit_cancelled.emit();
}
self.base.request_redraw();
}
pub fn is_editing(&self) -> bool {
self.is_editing
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: &str) {
self.text = text.to_string();
self.cursor_position = self.text.len();
self.base.request_redraw();
}
pub fn set_font_size(&mut self, size: f32) {
self.font_size = size.max(4.0);
self.base.request_redraw();
}
pub fn font_size(&self) -> f32 {
self.font_size
}
pub fn set_padding(&mut self, padding: i32) {
self.padding = padding.max(0);
self.base.request_redraw();
}
pub fn padding(&self) -> i32 {
self.padding
}
fn insert_char(&mut self, c: char) {
if c == '\u{7f}' {
if self.cursor_position > 0 {
let mut chars: Vec<char> = self.text.chars().collect();
chars.remove(self.cursor_position - 1);
self.text = chars.into_iter().collect();
self.cursor_position = self.cursor_position.saturating_sub(1);
self.base.request_redraw();
}
} else if c == '\u{ffff}' {
if self.cursor_position < self.text.chars().count() {
let mut chars: Vec<char> = self.text.chars().collect();
chars.remove(self.cursor_position);
self.text = chars.into_iter().collect();
self.base.request_redraw();
}
} else {
let mut chars: Vec<char> = self.text.chars().collect();
chars.insert(self.cursor_position, c);
self.text = chars.into_iter().collect();
self.cursor_position += 1;
self.base.request_redraw();
}
}
fn cursor_left(&mut self) {
if self.cursor_position > 0 {
self.cursor_position -= 1;
self.base.request_redraw();
}
}
fn cursor_right(&mut self) {
let char_count = self.text.chars().count();
if self.cursor_position < char_count {
self.cursor_position += 1;
self.base.request_redraw();
}
}
}
impl Widget for InplaceEditor {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
}
impl Draw for InplaceEditor {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let font = Font::new("sans-serif", self.font_size, false, false);
if self.is_editing {
context.fill_rect(rect, Color::rgba(255, 255, 255, 255));
context.draw_rect_stroke(rect, Color::rgba(0, 120, 255, 255), 2);
let text_x = rect.x + self.padding;
let text_y = rect.y + self.padding + self.font_size as i32;
context.draw_text(Point::new(text_x, text_y), &self.text, &font, Color::BLACK);
let cursor_x = text_x + self.cursor_position as i32 * 8;
context.draw_line(
Point::new(cursor_x, rect.y + self.padding),
Point::new(cursor_x, rect.y + rect.height as i32 - self.padding),
Color::rgba(0, 0, 0, 200),
);
} else {
context.fill_rect(rect, Color::rgba(245, 245, 245, 255));
context.draw_rect_stroke(rect, Color::rgba(200, 200, 200, 255), 1);
let text_x = rect.x + self.padding;
let text_y = rect.y + self.padding + self.font_size as i32;
context.draw_text(
Point::new(text_x, text_y),
&self.text,
&font,
Color::rgba(50, 50, 50, 255),
);
}
}
}
impl EventHandler for InplaceEditor {
fn handle_event(&mut self, event: &Event) {
if !self.base.is_enabled() {
return;
}
match event {
Event::MouseDoubleClick { pos: _, button } if *button == 1 => {
self.start_edit();
}
Event::KeyPress { key, modifiers: _ } => {
if !self.is_editing {
self.base.handle_event(event);
return;
}
match *key {
0x1B => {
self.finish_edit(false);
}
0x0D | 0x09 => {
self.finish_edit(true);
}
0x08 => {
self.insert_char('\u{7f}');
}
0x2E => {
self.insert_char('\u{ffff}');
}
0x25 => {
self.cursor_left();
}
0x27 => {
self.cursor_right();
}
_ => {
if let Some(c) = char::from_u32(*key) {
if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation()
{
self.insert_char(c);
}
}
}
}
}
_ => {
self.base.handle_event(event);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
#[test]
fn inplace_editor_initial_state() {
let ie = InplaceEditor::new("Hello", Rect::new(0, 0, 200, 30));
assert_eq!(ie.text(), "Hello");
assert!(!ie.is_editing());
assert!((ie.font_size() - 14.0).abs() < 0.01);
assert_eq!(ie.padding(), 4);
assert_eq!(ie.kind(), WidgetKind::InplaceEditor);
}
#[test]
fn inplace_editor_set_text() {
let mut ie = InplaceEditor::new("Hello", Rect::new(0, 0, 200, 30));
ie.set_text("World");
assert_eq!(ie.text(), "World");
}
#[test]
fn inplace_editor_start_and_finish_edit_accept() {
let mut ie = InplaceEditor::new("Hello", Rect::new(0, 0, 200, 30));
let accepted = std::sync::Arc::new(std::sync::Mutex::new(None));
let accepted_clone = accepted.clone();
ie.edit_accepted.connect(move |text| {
*accepted_clone.lock().unwrap() = Some((*text).clone());
});
ie.start_edit();
assert!(ie.is_editing());
ie.set_text("Hello World");
ie.finish_edit(true);
assert!(!ie.is_editing());
assert_eq!(ie.text(), "Hello World");
assert_eq!(*accepted.lock().unwrap(), Some("Hello World".to_string()));
}
#[test]
fn inplace_editor_start_and_finish_edit_cancel() {
let mut ie = InplaceEditor::new("Hello", Rect::new(0, 0, 200, 30));
let cancelled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let cancelled_clone = cancelled.clone();
ie.edit_cancelled.connect(move || {
cancelled_clone.store(true, std::sync::atomic::Ordering::SeqCst);
});
ie.start_edit();
assert!(ie.is_editing());
ie.set_text("Modified Text");
ie.finish_edit(false);
assert!(!ie.is_editing());
assert_eq!(ie.text(), "Hello"); assert!(cancelled.load(std::sync::atomic::Ordering::SeqCst));
}
#[test]
fn inplace_editor_double_click_starts_edit() {
let mut ie = InplaceEditor::new("Hello", Rect::new(0, 0, 200, 30));
ie.handle_event(&Event::MouseDoubleClick { pos: Point::new(50, 15), button: 1 });
assert!(ie.is_editing());
}
#[test]
fn inplace_editor_escape_cancels_edit() {
let mut ie = InplaceEditor::new("Hello", Rect::new(0, 0, 200, 30));
ie.start_edit();
ie.set_text("Changed");
ie.handle_event(&Event::KeyPress {
key: 0x1B, modifiers: 0,
});
assert!(!ie.is_editing());
assert_eq!(ie.text(), "Hello");
}
#[test]
fn inplace_editor_enter_accepts_edit() {
let mut ie = InplaceEditor::new("Hello", Rect::new(0, 0, 200, 30));
ie.start_edit();
ie.set_text("Accepted");
ie.handle_event(&Event::KeyPress {
key: 0x0D, modifiers: 0,
});
assert!(!ie.is_editing());
assert_eq!(ie.text(), "Accepted");
}
#[test]
fn inplace_editor_set_font_size_and_padding() {
let mut ie = InplaceEditor::new("Test", Rect::new(0, 0, 200, 30));
ie.set_font_size(18.0);
assert!((ie.font_size() - 18.0).abs() < 0.01);
ie.set_font_size(0.0); assert!((ie.font_size() - 4.0).abs() < 0.01);
ie.set_padding(8);
assert_eq!(ie.padding(), 8);
ie.set_padding(-5); assert_eq!(ie.padding(), 0);
}
#[test]
fn inplace_editor_insert_characters() {
let mut ie = InplaceEditor::new("", Rect::new(0, 0, 200, 30));
ie.start_edit();
ie.insert_char('A');
ie.insert_char('B');
ie.insert_char('C');
assert_eq!(ie.text(), "ABC");
assert_eq!(ie.cursor_position, 3);
ie.insert_char('\u{7f}');
assert_eq!(ie.text(), "AB");
assert_eq!(ie.cursor_position, 2);
}
}