use super::*;
use crate::canvas::Canvas;
use std::num::NonZero;
use std::rc::Rc;
use std::time::{Duration, Instant};
use crossterm::event::KeyCode;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::*;
pub type TextChangedCallback = dyn Fn(&mut Textbox);
pub type TextboxKeyHandler = fn(&mut Textbox, event: &mut WindowEvent);
pub struct Textbox {
text: String,
grapheme_count: usize,
cursor: usize,
offset: usize,
has_focus: bool,
width: usize,
max_length: usize,
base: WidgetBase,
readonly: bool,
focus_color: Option<Color>,
fat_cursor: bool,
last_change: Instant,
delay: Duration,
callback: Rc<TextChangedCallback>,
key_handler: TextboxKeyHandler,
}
impl Default for Textbox {
fn default() -> Self {
let mut s = Self {
text: String::new(),
width: usize::MIN,
max_length: 128,
base: WidgetBase::default(),
has_focus: false,
cursor: usize::MIN,
offset: usize::MIN,
grapheme_count: usize::MIN,
readonly: false,
focus_color: None,
fat_cursor: true,
last_change: Instant::now(),
delay: Self::DELAY_DEFAULT,
callback: Rc::new(Self::default_callback),
key_handler: Self::default_key_handler,
};
s.base.constraints.y.min = NonZero::<TSize>::MIN;
s.base.constraints.y.max = Size::Fixed(NonZero::<TSize>::MIN);
s
}
}
impl Textbox {
pub const DELAY_DEFAULT: Duration = Duration::from_millis(250);
pub const CURSOR: Grapheme = Grapheme::new_unchecked("_", GlyphWidth::Half);
pub const CURSOR_WIDE: Grapheme = Grapheme::new_unchecked("_", GlyphWidth::Half);
pub fn default_callback(_: &mut Self) {}
pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
if let Event::Key(k) = event.raw() {
match k.code {
KeyCode::Left => {
self.move_cursor_left();
event.handled = true;
}
KeyCode::Right => {
self.move_cursor_right();
event.handled = true;
}
KeyCode::Backspace => {
if !self.readonly {
self.remove_glyph_before_cursor();
}
event.handled = true;
}
KeyCode::Char(ch) => {
if !self.readonly {
self.insert_char_at_cursor(ch);
}
event.handled = true;
}
_ => {}
}
}
}
pub fn set_text_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
self.callback = Rc::new(callback);
}
pub fn set_callback_delay(&mut self, duration: Duration) {
self.delay = duration;
}
pub fn set_key_handler(&mut self, f: TextboxKeyHandler) {
self.key_handler = f;
}
pub fn set_cursor_type(&mut self, fat_cursor: bool) {
self.fat_cursor = fat_cursor;
}
pub fn set_focus_color(&mut self, focus_color: Option<Color>) {
self.focus_color = focus_color;
}
pub fn is_readonly(&self) -> bool {
self.readonly
}
pub fn set_readonly(&mut self, readonly: bool) {
self.readonly = readonly;
}
pub fn get_max_length(&self) -> usize {
self.max_length
}
pub fn set_max_length(&mut self, length: usize) {
self.max_length = length;
}
pub fn get_text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: &str) {
self.text.clear();
self.text.push_str(text);
self.grapheme_count = self.text.graphemes(true).count();
self.cursor = self.grapheme_count % self.width;
self.offset = self.grapheme_count - self.cursor;
self.text_changed();
}
pub fn clear(&mut self) {
self.text.clear();
self.grapheme_count = usize::MIN;
self.cursor = usize::MIN;
self.offset = usize::MIN;
self.text_changed();
}
pub fn move_cursor_left(&mut self) {
if self.actual_cursor_pos() > usize::MIN {
self.decrease_cursor();
}
}
pub fn move_cursor_right(&mut self) {
if self.actual_cursor_pos() < self.grapheme_count {
self.increase_cursor();
self.check_cursor();
}
}
pub fn remove_glyph_before_cursor(&mut self) {
let cursor = self.actual_cursor_pos();
if cursor > usize::MIN {
let mut iter = self.text.grapheme_indices(true).skip(cursor - 1);
if let Some((idx, str)) = iter.next() {
(0..str.chars().count()).for_each(|_| {
self.text.remove(idx);
});
self.decrease_cursor();
}
self.grapheme_count = self.text.graphemes(true).count();
}
}
pub fn insert_char_at_cursor(&mut self, ch: char) {
let cursor = self.actual_cursor_pos();
let ch_width = match ch.width() {
Some(w) => w,
None => return,
};
if self.grapheme_count < self.max_length {
if cursor == self.grapheme_count {
if self.get_current_visible_width() == self.width - ch_width {
self.cursor -= 1;
self.offset += 1;
}
self.cursor += 1;
}
else {
self.increase_cursor();
}
let mut iter = self.text.grapheme_indices(true).skip(cursor);
match iter.next().map(|(idx, _)| idx) {
Some(idx) => self.text.insert(idx, ch),
None => self.text.push(ch),
}
self.grapheme_count = self.text.graphemes(true).count();
self.check_cursor();
self.text_changed();
}
}
fn check_cursor(&mut self) {
let len = self.text.width();
if self.actual_cursor_pos() > len {
let diff = self.actual_cursor_pos() - len;
self.cursor -= diff;
}
}
fn actual_cursor_pos(&self) -> usize {
self.cursor + self.offset
}
fn get_current_visible_width(&self) -> usize {
let mut iter = self.text.grapheme_indices(true).skip(self.offset);
let start = iter.next().map(|x| x.0).unwrap_or(usize::MIN);
let mut iter = self
.text
.grapheme_indices(true)
.skip(self.offset + self.cursor + 1);
let end = iter.next().map(|x| x.0).unwrap_or(self.text.len());
self.text[start..end].width()
}
fn is_cursor_at_max_width(&self) -> bool {
self.get_current_visible_width() == self.width
}
fn increase_cursor(&mut self) {
if self.is_cursor_at_max_width() {
self.offset += 1;
}
else {
self.cursor += 1;
}
}
fn decrease_cursor(&mut self) {
if self.offset > usize::MIN && self.cursor == usize::MIN {
self.offset -= 1;
}
else {
self.cursor -= 1;
}
}
fn text_changed(&mut self) {
if self.last_change.elapsed() > self.delay {
let cb = self.callback.clone();
cb(self);
}
self.last_change = Instant::now();
}
}
impl Window for Textbox {
fn render(&self, canvas: &mut Canvas) {
let mut row = canvas.get_row_variable_width(TSize::MIN).unwrap();
let mut graphemes = self.text.graphemes(true).skip(self.offset);
let mut counter = usize::MIN;
while row.cursor() < row.width() {
let gr = graphemes.next();
let str;
let mut style = Style::None;
let mut fg = None;
if !self.base.enabled {
fg = self.base.colors.disabled;
}
else if self.has_focus {
fg = self.focus_color;
}
fg = fg.or(self.base.colors.base_fg);
if self.has_focus && counter == self.cursor {
if self.fat_cursor {
str = gr
.map(|v| Grapheme::from(v).unwrap())
.unwrap_or(Grapheme::PLACEHOLDER);
style = Style::Reverse | Style::ResetAfter;
}
else {
let width = gr.map(|x| x.width()).unwrap_or(usize::MIN);
str = match width {
2 => Self::CURSOR_WIDE,
_ => Self::CURSOR,
};
}
}
else {
str = gr
.map(|v| Grapheme::from(v).unwrap())
.unwrap_or(Grapheme::PLACEHOLDER);
}
row.add_grapheme(str, fg, self.base.colors.base_bg, style)
.ok();
counter += 1;
}
}
fn handle_event(&mut self, event: &mut WindowEvent) {
match event.raw() {
Event::Resize(w, _) => self.width = *w as usize,
Event::FocusGained => self.has_focus = true,
Event::FocusLost => self.has_focus = false,
_ => (self.key_handler)(self, event),
}
}
fn is_enabled(&self) -> bool {
self.base.enabled
}
}
impl WindowLayout for Textbox {
fn desired_size(&self, available_size: TPoint) -> TPoint {
self.base.desired_size(available_size)
}
fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
self.base.alignment
}
fn margin(&self) -> Thickness {
self.base.margin
}
fn border(&self) -> BorderStyle {
self.base.border
}
fn is_visible(&self) -> bool {
self.base.visibility
}
}
impl HasWindowUID for Textbox {
fn uid(&self) -> WindowUID {
self.base.uid
}
}
impl Widget for Textbox {
fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
self.base.alignment = (horizontal, vertical);
self.provoke_changed_property(WindowProperty::Alignment);
}
fn set_visibility(&mut self, visibility: bool) {
self.base.visibility = visibility;
self.provoke_changed_property(WindowProperty::IsVisible);
}
fn set_width(&mut self, width: Size) {
self.base.size.x = width;
self.provoke_changed_property(WindowProperty::Size);
}
fn set_height(&mut self, _: Size) {}
fn set_margin(&mut self, margin: Thickness) {
self.base.margin = margin;
self.provoke_changed_property(WindowProperty::Margin);
}
fn set_border(&mut self, border: BorderStyle) {
self.base.border = border;
self.provoke_changed_property(WindowProperty::Border);
}
fn set_enabled_state(&mut self, is_enabled: bool) {
self.base.enabled = is_enabled;
}
fn set_width_constraint(&mut self, width: SizeConstraint) {
self.base.constraints.x = width;
self.provoke_changed_property(WindowProperty::Size);
}
fn set_height_constraint(&mut self, _: SizeConstraint) {}
}
impl WidgetColors for Textbox {
fn set_disabled_color(&mut self, color: Option<Color>) {
self.base.colors.disabled = color;
}
fn set_base_fg_color(&mut self, color: Option<Color>) {
self.base.colors.base_fg = color;
}
fn set_base_bg_color(&mut self, color: Option<Color>) {
self.base.colors.base_bg = color;
}
}