use log::{debug, warn};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::config::config::ConfigRef;
use crate::config::theme::Theme;
use crate::cursor::cursor_set::CursorSet;
use crate::experiments::clipboard::ClipboardRef;
use crate::experiments::screenspace::Screenspace;
use crate::io::input_event::InputEvent;
use crate::io::input_event::InputEvent::KeyInput;
use crate::io::keys::Keycode;
use crate::io::output::Output;
use crate::primitives::common_edit_msgs::{key_to_edit_msg, CommonEditMsg};
use crate::primitives::helpers;
use crate::primitives::xy::XY;
use crate::text::buffer_state::BufferState;
use crate::text::text_buffer::TextBuffer;
use crate::widget::any_msg::AnyMsg;
use crate::widget::fill_policy::SizePolicy;
use crate::widget::widget::{get_new_widget_id, Widget, WidgetAction, WID};
use crate::{unpack_or_e, unpack_unit_e};
pub struct EditBoxWidget {
id: WID,
enabled: bool,
on_hit: Option<WidgetAction<EditBoxWidget>>,
on_change: Option<WidgetAction<EditBoxWidget>>,
on_miss: Option<WidgetAction<EditBoxWidget>>,
buffer: BufferState,
min_width_op: Option<u16>,
max_width_op: Option<u16>,
clipboard_op: Option<ClipboardRef>,
last_size_x: Option<u16>,
size_policy: SizePolicy,
config: ConfigRef,
}
impl EditBoxWidget {
const MIN_WIDTH: u16 = 2;
pub const TYPENAME: &'static str = "edit_box";
pub fn new(config: ConfigRef) -> Self {
let widget_id = get_new_widget_id();
let mut buffer = BufferState::simplified_single_line();
buffer.initialize_for_widget(widget_id, None);
let mut res = EditBoxWidget {
id: widget_id,
enabled: true,
buffer,
on_hit: None,
on_change: None,
on_miss: None,
max_width_op: None,
clipboard_op: None,
last_size_x: None,
min_width_op: None,
size_policy: SizePolicy::SELF_DETERMINED,
config,
};
res
}
pub fn with_size_policy(self, size_policy: SizePolicy) -> Self {
Self { size_policy, ..self }
}
pub fn with_clipboard(self, clipboard: ClipboardRef) -> Self {
Self {
clipboard_op: Some(clipboard),
..self
}
}
pub fn with_max_width(self, max_width: u16) -> Self {
EditBoxWidget {
max_width_op: Some(max_width),
..self
}
}
pub fn with_min_width(self, min_width: u16) -> Self {
EditBoxWidget {
min_width_op: Some(min_width),
..self
}
}
pub fn with_on_hit(self, on_hit: WidgetAction<EditBoxWidget>) -> Self {
EditBoxWidget {
on_hit: Some(on_hit),
..self
}
}
pub fn with_on_change(self, on_change: WidgetAction<EditBoxWidget>) -> Self {
EditBoxWidget {
on_change: Some(on_change),
..self
}
}
pub fn with_on_miss(self, on_miss: WidgetAction<EditBoxWidget>) -> Self {
EditBoxWidget {
on_miss: Some(on_miss),
..self
}
}
pub fn with_enabled(self, enabled: bool) -> Self {
EditBoxWidget { enabled, ..self }
}
pub fn with_text<'a, T: AsRef<str>>(self, text: T) -> Self {
let mut res = EditBoxWidget {
buffer: BufferState::simplified_single_line().with_text(text),
..self
};
res.buffer.initialize_for_widget(self.id, None);
res
}
pub fn get_buffer(&self) -> &BufferState {
&self.buffer
}
pub fn get_text(&self) -> String {
self.buffer.text().to_string()
}
pub fn is_empty(&self) -> bool {
self.buffer.len_bytes() == 0 }
pub fn set_text<'a, T: AsRef<str>>(&mut self, text: T) {
self.buffer = BufferState::simplified_single_line().with_text(text);
self.buffer.initialize_for_widget(self.id, None);
}
pub fn set_cursor_end(&mut self) {
let mut cursor_set = CursorSet::single();
cursor_set.move_end(&self.buffer, false);
self.buffer.text_mut().set_cursor_set(self.id, cursor_set);
}
pub fn clear(&mut self) {
self.buffer = BufferState::simplified_single_line();
self.buffer.initialize_for_widget(self.id, None);
}
fn event_changed(&self) -> Option<Box<dyn AnyMsg>> {
if self.on_change.is_some() {
self.on_change.as_ref().unwrap()(self)
} else {
None
}
}
fn event_miss(&self) -> Option<Box<dyn AnyMsg>> {
if self.on_miss.is_some() {
self.on_miss.as_ref().unwrap()(self)
} else {
None
}
}
fn event_hit(&self) -> Option<Box<dyn AnyMsg>> {
if self.on_hit.is_some() {
self.on_hit.as_ref().unwrap()(self)
} else {
None
}
}
}
impl Widget for EditBoxWidget {
fn id(&self) -> WID {
self.id
}
fn static_typename() -> &'static str
where
Self: Sized,
{
Self::TYPENAME
}
fn typename(&self) -> &'static str {
Self::TYPENAME
}
fn full_size(&self) -> XY {
XY::new(self.min_width_op.unwrap_or(Self::MIN_WIDTH), 1)
}
fn size_policy(&self) -> SizePolicy {
self.size_policy
}
fn layout(&mut self, screenspace: Screenspace) {
self.last_size_x = Some(screenspace.output_size().x);
}
fn on_input(&self, input_event: InputEvent) -> Option<Box<dyn AnyMsg>> {
debug_assert!(self.enabled, "EditBoxWidgetMsg: received input to disabled component!");
let cursor_set_copy = unpack_or_e!(self.buffer.text().get_cursor_set(self.id), None, "failed to get cursor_set").clone();
return match input_event {
KeyInput(key_event) => {
if key_event.keycode == Keycode::Enter {
Some(Box::new(EditBoxWidgetMsg::Hit))
} else {
match key_to_edit_msg(key_event, &self.config.keyboard_config.edit_msgs) {
Some(cem) => match cem {
CommonEditMsg::CursorUp { selecting: _ } | CommonEditMsg::CursorDown { selecting: _ } => None,
CommonEditMsg::CursorLeft { selecting: _ }
if cursor_set_copy.as_single().map(|c| c.a == 0).unwrap_or(false) =>
{
None
}
CommonEditMsg::CursorRight { selecting: _ }
if cursor_set_copy.as_single().map(|c| c.a > self.buffer.len_chars()).unwrap_or(false) =>
{
None
}
_ => Some(Box::new(EditBoxWidgetMsg::CommonEditMsg(cem))),
},
None => None,
}
}
}
_ => None,
};
}
fn update(&mut self, msg: Box<dyn AnyMsg>) -> Option<Box<dyn AnyMsg>> {
let our_msg = msg.as_msg::<EditBoxWidgetMsg>();
if our_msg.is_none() {
warn!("expecetd EditBoxWidgetMsg, got {:?}", msg);
return None;
}
debug!("EditBox got {:?}", msg);
return match our_msg.unwrap() {
EditBoxWidgetMsg::Hit => self.event_hit(),
EditBoxWidgetMsg::CommonEditMsg(cem) => {
if self
.buffer
.apply_common_edit_message(cem.clone(), self.id, 1, self.clipboard_op.as_ref(), false)
.modified_buffer
{
self.event_changed()
} else {
None
}
}
};
}
fn render(&self, theme: &Theme, focused: bool, output: &mut dyn Output) {
let size = XY::new(unpack_unit_e!(self.last_size_x, "render before layout",), 1);
#[cfg(any(test, feature = "fuzztest"))]
{
output.emit_metadata(crate::io::output::Metadata {
id: self.id(),
typename: self.typename().to_string(),
rect: crate::primitives::rect::Rect::from_zero(size),
focused,
});
}
let primary_style = theme.highlighted(focused);
helpers::fill_output(primary_style.background, output);
let cursor_set_copy = unpack_unit_e!(self.buffer.text().get_cursor_set(self.id), "failed to get cursor_set",).clone();
let mut x: usize = 0;
for (char_idx, g) in self.buffer.to_string().graphemes(true).enumerate() {
if x + g.width() > size.x as usize {
break;
}
let style = match theme.cursor_background(cursor_set_copy.get_cursor_status_for_char(char_idx)) {
Some(bg) => primary_style.with_background(if focused { bg } else { bg.half() }),
None => primary_style,
};
output.print_at(
XY::new(x as u16, 0), style,
g,
);
x += g.width();
}
if x < size.x as usize {
let style = match theme.cursor_background(cursor_set_copy.get_cursor_status_for_char(self.buffer.len_chars())) {
Some(bg) => primary_style.with_background(if focused { bg } else { bg.half() }),
None => primary_style,
};
output.print_at(XY::new(x as u16, 0), style, " ");
}
let cursor_offset: u16 = cursor_set_copy.max_cursor_pos() as u16 + 1; let text_width = self.buffer.to_string().width() as u16; let end_of_text = cursor_offset.max(text_width);
if size.x > end_of_text {
let background_length = size.x - end_of_text;
for i in 0..background_length {
let pos = XY::new(end_of_text + i as u16, 0);
output.print_at(pos, primary_style, " ");
}
}
}
fn kite(&self) -> XY {
XY::ZERO
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum EditBoxWidgetMsg {
Hit,
CommonEditMsg(CommonEditMsg),
}
impl AnyMsg for EditBoxWidgetMsg {}