use crate::{
direction::Direction,
event::{Callback, Event, EventResult, Key, MouseEvent},
rect::Rect,
style::{PaletteStyle, StyleType},
utils::lines::simple::{simple_prefix, simple_suffix},
view::{CannotFocus, View},
Cursive, Printer, Vec2, With,
};
use std::sync::{Arc, Mutex};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub type OnEdit = dyn Fn(&mut Cursive, &str, usize) + Send + Sync;
pub type OnSubmit = dyn Fn(&mut Cursive, &str) + Send + Sync;
pub struct EditView {
#[allow(clippy::rc_buffer)] content: Arc<String>,
cursor: usize,
offset: usize,
max_content_width: Option<usize>,
last_length: usize,
on_edit: Option<Arc<OnEdit>>,
on_submit: Option<Arc<OnSubmit>>,
secret: bool,
filler: String,
enabled: bool,
regular_style: StyleType,
inactive_style: StyleType,
cursor_style: StyleType,
}
new_default!(EditView);
impl EditView {
impl_enabled!(self.enabled);
pub fn new() -> Self {
EditView {
content: Arc::new(String::new()),
cursor: 0,
offset: 0,
last_length: 0, on_edit: None,
on_submit: None,
max_content_width: None,
secret: false,
filler: "_".to_string(),
enabled: true,
regular_style: PaletteStyle::EditableText.into(),
inactive_style: PaletteStyle::EditableTextInactive.into(),
cursor_style: PaletteStyle::EditableTextCursor.into(),
}
}
pub fn set_max_content_width(&mut self, width: Option<usize>) {
self.max_content_width = width;
}
#[must_use]
pub fn max_content_width(self, width: usize) -> Self {
self.with(|s| s.set_max_content_width(Some(width)))
}
pub fn set_secret(&mut self, secret: bool) {
self.secret = secret;
}
#[must_use]
pub fn secret(self) -> Self {
self.with(|s| s.set_secret(true))
}
pub fn set_filler<S: Into<String>>(&mut self, filler: S) {
self.filler = filler.into();
}
#[must_use]
pub fn filler<S: Into<String>>(self, filler: S) -> Self {
self.with(|s| s.set_filler(filler))
}
pub fn set_style<S: Into<StyleType>>(&mut self, style: S) {
let style = style.into();
self.regular_style = style;
}
#[must_use]
pub fn style<S: Into<StyleType>>(self, style: S) -> Self {
self.with(|s| s.set_style(style))
}
pub fn set_on_edit_mut<F>(&mut self, callback: F)
where
F: FnMut(&mut Cursive, &str, usize) + 'static + Send + Sync,
{
self.set_on_edit(immut3!(callback));
}
#[crate::callback_helpers]
pub fn set_on_edit<F>(&mut self, callback: F)
where
F: Fn(&mut Cursive, &str, usize) + 'static + Send + Sync,
{
self.on_edit = Some(Arc::new(callback));
}
#[must_use]
pub fn on_edit_mut<F>(self, callback: F) -> Self
where
F: FnMut(&mut Cursive, &str, usize) + 'static + Send + Sync,
{
self.with(|v| v.set_on_edit_mut(callback))
}
#[must_use]
pub fn on_edit<F>(self, callback: F) -> Self
where
F: Fn(&mut Cursive, &str, usize) + 'static + Send + Sync,
{
self.with(|v| v.set_on_edit(callback))
}
pub fn set_on_submit_mut<F>(&mut self, callback: F)
where
F: FnMut(&mut Cursive, &str) + 'static + Send + Sync,
{
let callback = Mutex::new(callback);
self.set_on_submit(move |s, text| {
if let Ok(mut f) = callback.try_lock() {
(*f)(s, text);
}
});
}
#[crate::callback_helpers]
pub fn set_on_submit<F>(&mut self, callback: F)
where
F: Fn(&mut Cursive, &str) + 'static + Send + Sync,
{
self.on_submit = Some(Arc::new(callback));
}
#[must_use]
pub fn on_submit_mut<F>(self, callback: F) -> Self
where
F: FnMut(&mut Cursive, &str) + 'static + Send + Sync,
{
self.with(|v| v.set_on_submit_mut(callback))
}
#[must_use]
pub fn on_submit<F>(self, callback: F) -> Self
where
F: Fn(&mut Cursive, &str) + 'static + Send + Sync,
{
self.with(|v| v.set_on_submit(callback))
}
pub fn set_content<S: Into<String>>(&mut self, content: S) -> Callback {
let content = content.into();
let len = content.len();
self.content = Arc::new(content);
self.offset = 0;
self.set_cursor(len);
self.make_edit_cb().unwrap_or_else(Callback::dummy)
}
#[allow(clippy::rc_buffer)]
pub fn get_content(&self) -> Arc<String> {
Arc::clone(&self.content)
}
#[must_use]
pub fn content<S: Into<String>>(mut self, content: S) -> Self {
self.set_content(content);
self
}
pub fn get_cursor(&self) -> usize {
self.cursor
}
pub fn set_cursor(&mut self, cursor: usize) {
self.cursor = cursor;
self.keep_cursor_in_view();
}
pub fn insert(&mut self, ch: char) -> Callback {
if let Some(width) = self.max_content_width {
if ch.width().unwrap_or(0) + self.content.width() > width {
return Callback::dummy();
}
}
Arc::make_mut(&mut self.content).insert(self.cursor, ch);
self.cursor += ch.len_utf8();
self.keep_cursor_in_view();
self.make_edit_cb().unwrap_or_else(Callback::dummy)
}
pub fn remove(&mut self, len: usize) -> Callback {
let start = self.cursor;
let end = self.cursor + len.min(self.content.len() - self.cursor);
for _ in Arc::make_mut(&mut self.content).drain(start..end) {}
self.keep_cursor_in_view();
self.make_edit_cb().unwrap_or_else(Callback::dummy)
}
fn make_edit_cb(&self) -> Option<Callback> {
self.on_edit.clone().map(|cb| {
let content = Arc::clone(&self.content);
let cursor = self.cursor;
Callback::from_fn(move |s| {
cb(s, &content, cursor);
})
})
}
fn keep_cursor_in_view(&mut self) {
if self.cursor < self.offset {
self.offset = self.cursor;
} else {
let c_len = self.content[self.cursor..]
.graphemes(true)
.map(UnicodeWidthStr::width)
.next()
.unwrap_or(1);
let available = match self.last_length.checked_sub(c_len) {
Some(s) => s,
None => return,
};
let suffix_length =
simple_suffix(&self.content[self.offset..self.cursor], available).length;
assert!(suffix_length <= self.cursor);
self.offset = self.cursor - suffix_length;
assert!(self.cursor >= self.offset);
}
if self.content[self.offset..].width() < self.last_length {
assert!(self.last_length >= 1);
let suffix_length = simple_suffix(&self.content, self.last_length - 1).length;
assert!(self.content.len() >= suffix_length);
self.offset = self.content.len() - suffix_length;
}
}
}
fn make_small_stars(length: usize) -> &'static str {
assert!(
length <= 4,
"Can only generate stars for one grapheme at a time."
);
&"****"[..length]
}
impl View for EditView {
fn draw(&self, printer: &Printer) {
assert_eq!(
printer.size.x, self.last_length,
"Was promised {}, received {}",
self.last_length, printer.size.x
);
let (style, cursor_style) = if self.enabled && printer.enabled {
(self.regular_style, self.cursor_style)
} else {
(self.inactive_style, self.inactive_style)
};
let width = self.content.width();
printer.with_style(style, |printer| {
if width < self.last_length {
assert!(printer.size.x >= width);
if self.secret {
printer.print_hline((0, 0), width, "*");
} else {
printer.print((0, 0), &self.content);
}
let filler_len = (printer.size.x - width) / self.filler.width();
printer.print_hline((width, 0), filler_len, self.filler.as_str());
} else {
let content = &self.content[self.offset..];
let display_bytes = content
.graphemes(true)
.scan(0, |w, g| {
*w += g.width();
if *w > self.last_length {
None
} else {
Some(g)
}
})
.map(str::len)
.sum();
let content = &content[..display_bytes];
let width = content.width();
if self.secret {
printer.print_hline((0, 0), width, "*");
} else {
printer.print((0, 0), content);
}
if width < self.last_length {
let filler_len = (self.last_length - width) / self.filler.width();
printer.print_hline((width, 0), filler_len, self.filler.as_str());
}
}
});
if printer.focused {
let c: &str = if self.cursor == self.content.len() {
&self.filler
} else {
let selected = self.content[self.cursor..]
.graphemes(true)
.next()
.unwrap_or_else(|| {
panic!(
"Found no char at cursor {} in {}",
self.cursor, &self.content
)
});
if self.secret {
make_small_stars(selected.width())
} else {
selected
}
};
let offset = self.content[self.offset..self.cursor].width();
printer.with_style(cursor_style, |printer| {
printer.print((offset, 0), c);
});
}
}
fn layout(&mut self, size: Vec2) {
self.last_length = size.x;
}
fn take_focus(&mut self, _: Direction) -> Result<EventResult, CannotFocus> {
self.enabled.then(EventResult::consumed).ok_or(CannotFocus)
}
fn on_event(&mut self, event: Event) -> EventResult {
if !self.enabled {
return EventResult::Ignored;
}
match event {
Event::Char(ch) => {
return EventResult::Consumed(Some(self.insert(ch)));
}
Event::CtrlChar('u') => {
let content = self.content[self.cursor..].to_owned();
let callback = self.set_content(content);
self.set_cursor(0);
return EventResult::Consumed(Some(callback));
}
Event::CtrlChar('k') => {
let content = self.content[..self.cursor].to_owned();
return EventResult::Consumed(Some(self.set_content(content)));
}
Event::Key(Key::Home) | Event::CtrlChar('a') => self.set_cursor(0),
Event::Key(Key::End) | Event::CtrlChar('e') => {
let len = self.content.len();
self.set_cursor(len);
}
Event::Key(Key::Left) | Event::CtrlChar('b') if self.cursor > 0 => {
let len = self.content[..self.cursor]
.graphemes(true)
.last()
.unwrap()
.len();
let cursor = self.cursor - len;
self.set_cursor(cursor);
}
Event::Key(Key::Right) | Event::CtrlChar('f') if self.cursor < self.content.len() => {
let len = self.content[self.cursor..]
.graphemes(true)
.next()
.unwrap()
.len();
let cursor = self.cursor + len;
self.set_cursor(cursor);
}
Event::Key(Key::Backspace) if self.cursor > 0 => {
let len = self.content[..self.cursor]
.graphemes(true)
.last()
.unwrap()
.len();
self.cursor -= len;
return EventResult::Consumed(Some(self.remove(len)));
}
Event::Key(Key::Del) if self.cursor < self.content.len() => {
let len = self.content[self.cursor..]
.graphemes(true)
.next()
.unwrap()
.len();
return EventResult::Consumed(Some(self.remove(len)));
}
Event::Key(Key::Enter) if self.on_submit.is_some() => {
let cb = self.on_submit.clone().unwrap();
let content = Arc::clone(&self.content);
return EventResult::with_cb(move |s| {
cb(s, &content);
});
}
Event::Mouse {
event: MouseEvent::Press(_),
position,
offset,
} if position.fits_in_rect(offset, (self.last_length, 1)) => {
if let Some(position) = position.checked_sub(offset) {
self.cursor = self.offset
+ simple_prefix(&self.content[self.offset..], position.x).length;
}
}
_ => return EventResult::Ignored,
}
EventResult::Consumed(self.make_edit_cb())
}
fn important_area(&self, _: Vec2) -> Rect {
let char_width = if self.cursor >= self.content.len() {
1
} else {
self.content[self.cursor..]
.graphemes(true)
.next()
.unwrap()
.width()
};
let x = self.content[..self.cursor].width();
Rect::from_size((x, 0), (char_width, 1))
}
}
#[crate::blueprint(EditView::new())]
struct Blueprint {
content: Option<String>,
on_edit: Option<_>,
on_submit: Option<_>,
}
crate::fn_blueprint!("EditView.with_content", |config, context| {
let name: String = context.resolve(&config["name"])?;
let callback = config["callback"].clone();
let context = context.clone();
let result: Arc<dyn Fn(&mut Cursive) + Send + Sync> = Arc::new(move |s| {
let content: String = s
.call_on_name(&name, |view: &mut EditView| view.get_content())
.unwrap()
.as_ref()
.clone();
let context = context.sub_context(|c| {
c.store("content", content);
});
let callback: Arc<dyn Fn(&mut Cursive) + Send + Sync> = match context.resolve(&callback) {
Ok(callback) => callback,
Err(err) => {
log::error!("Could not resolve callback: {err:?}");
return;
}
};
(*callback)(s);
});
Ok(result)
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ctrl_key_events() {
let mut view = EditView::new().content("foobarbaz");
view.set_cursor(0);
view.on_event(Event::CtrlChar('f'));
assert_eq!(view.get_cursor(), 1);
view.on_event(Event::CtrlChar('b'));
assert_eq!(view.get_cursor(), 0);
view.on_event(Event::CtrlChar('e'));
assert_eq!(view.get_cursor(), view.get_content().len());
view.on_event(Event::CtrlChar('a'));
assert_eq!(view.get_cursor(), 0);
view.set_cursor(3);
view.on_event(Event::CtrlChar('u'));
assert_eq!(view.get_cursor(), 0);
assert_eq!(*view.get_content(), "barbaz");
view.set_cursor(3);
view.on_event(Event::CtrlChar('k'));
assert_eq!(view.get_cursor(), 3);
assert_eq!(*view.get_content(), "bar");
}
}