mod chunking;
mod editing;
mod history;
mod types;
mod update;
mod view_helpers;
pub use types::{LineInputMessage, LineInputOutput};
#[cfg(test)]
mod handle_event_tests;
#[cfg(test)]
mod property_tests;
#[cfg(test)]
mod tests;
use crate::component::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
use crate::undo::UndoStack;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum InputMode {
Desktop,
Readline,
}
use chunking::{chunk_buffer, cursor_to_visual};
use history::History;
#[derive(Debug, Clone)]
struct LineInputSnapshot {
buffer: String,
cursor: usize,
}
pub struct LineInput;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct LineInputState {
buffer: String,
cursor: usize,
placeholder: String,
selection_anchor: Option<usize>,
clipboard: String,
max_length: Option<usize>,
#[cfg_attr(feature = "serialization", serde(skip))]
history: History,
#[cfg_attr(feature = "serialization", serde(skip))]
undo_stack: UndoStack<LineInputSnapshot>,
#[cfg_attr(feature = "serialization", serde(skip))]
last_display_width: usize,
input_mode: InputMode,
}
impl Default for LineInputState {
fn default() -> Self {
Self {
buffer: String::new(),
cursor: 0,
placeholder: String::new(),
selection_anchor: None,
clipboard: String::new(),
max_length: None,
history: History::default(),
undo_stack: UndoStack::default(),
last_display_width: 80,
input_mode: InputMode::Desktop,
}
}
}
impl LineInputState {
pub fn new() -> Self {
Self::default()
}
pub fn with_value(value: impl Into<String>) -> Self {
let buffer: String = value.into();
let cursor = buffer.len();
Self {
buffer,
cursor,
..Self::default()
}
}
pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn with_max_history(mut self, max: usize) -> Self {
self.history = History::new(max);
self
}
pub fn with_max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
pub fn with_input_mode(mut self, mode: InputMode) -> Self {
self.input_mode = mode;
self
}
pub fn value(&self) -> &str {
&self.buffer
}
pub fn set_value(&mut self, value: impl Into<String>) {
self.buffer = value.into();
self.cursor = self.buffer.len();
self.clear_selection();
}
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub fn len(&self) -> usize {
self.buffer.chars().count()
}
pub fn cursor_byte_offset(&self) -> usize {
self.cursor
}
pub fn cursor_visual_position(&self) -> (usize, usize) {
cursor_to_visual(&self.buffer, self.cursor, self.last_display_width)
}
pub fn set_display_width(&mut self, width: usize) {
self.last_display_width = width;
}
pub fn display_width(&self) -> usize {
self.last_display_width
}
pub fn visual_rows_at_width(&self, width: usize) -> usize {
chunk_buffer(&self.buffer, width).len()
}
pub fn max_length(&self) -> Option<usize> {
self.max_length
}
pub fn set_max_length(&mut self, max: Option<usize>) {
self.max_length = max;
}
pub fn placeholder(&self) -> &str {
&self.placeholder
}
pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
self.placeholder = placeholder.into();
}
pub fn input_mode(&self) -> &InputMode {
&self.input_mode
}
pub fn set_input_mode(&mut self, mode: InputMode) {
self.input_mode = mode;
}
pub fn max_history(&self) -> usize {
self.history.max_entries()
}
pub fn set_max_history(&mut self, max: usize) {
self.history.set_max_entries(max);
}
pub fn history_entries(&self) -> &[String] {
self.history.entries()
}
pub fn history_count(&self) -> usize {
self.history.count()
}
pub fn is_browsing_history(&self) -> bool {
self.history.is_browsing()
}
pub fn can_undo(&self) -> bool {
self.undo_stack.can_undo()
}
pub fn can_redo(&self) -> bool {
self.undo_stack.can_redo()
}
pub fn has_selection(&self) -> bool {
self.selection_anchor.is_some()
}
pub fn selection_range(&self) -> Option<(usize, usize)> {
let anchor = self.selection_anchor?;
let start = anchor.min(self.cursor);
let end = anchor.max(self.cursor);
if start == end {
None
} else {
Some((start, end))
}
}
pub fn selected_text(&self) -> Option<&str> {
let (start, end) = self.selection_range()?;
Some(&self.buffer[start..end])
}
pub fn clipboard(&self) -> &str {
&self.clipboard
}
pub fn update(&mut self, msg: LineInputMessage) -> Option<LineInputOutput> {
LineInput::update(self, msg)
}
fn remaining_capacity(&self) -> usize {
match self.max_length {
Some(max) => max.saturating_sub(self.buffer.chars().count()),
None => usize::MAX,
}
}
fn snapshot(&self) -> LineInputSnapshot {
LineInputSnapshot {
buffer: self.buffer.clone(),
cursor: self.cursor,
}
}
fn restore(&mut self, snapshot: LineInputSnapshot) {
self.buffer = snapshot.buffer;
self.cursor = snapshot.cursor;
self.clear_selection();
}
fn clear_selection(&mut self) {
self.selection_anchor = None;
}
fn ensure_selection_anchor(&mut self) {
if self.selection_anchor.is_none() {
self.selection_anchor = Some(self.cursor);
}
}
fn delete_selection(&mut self) -> Option<String> {
let (start, end) = self.selection_range()?;
let deleted = self.buffer[start..end].to_string();
let mut new_buffer = String::with_capacity(self.buffer.len() - (end - start));
new_buffer.push_str(&self.buffer[..start]);
new_buffer.push_str(&self.buffer[end..]);
self.buffer = new_buffer;
self.cursor = start;
self.clear_selection();
Some(deleted)
}
}
impl Component for LineInput {
type State = LineInputState;
type Message = LineInputMessage;
type Output = LineInputOutput;
fn init() -> Self::State {
LineInputState::default()
}
fn handle_event(
state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
if let Event::Paste(text) = event {
return Some(LineInputMessage::Paste(text.clone()));
}
let key = event.as_key()?;
let ctrl = key.modifiers.ctrl();
let shift = key.modifiers.shift();
match key.code {
Key::Char('z') if ctrl => Some(LineInputMessage::Undo),
Key::Char('y') if ctrl => Some(LineInputMessage::Redo),
Key::Char('a') if ctrl => match state.input_mode {
InputMode::Desktop => Some(LineInputMessage::SelectAll),
InputMode::Readline => Some(LineInputMessage::Home),
},
Key::Char('e') if ctrl => match state.input_mode {
InputMode::Desktop => Some(LineInputMessage::End),
InputMode::Readline => Some(LineInputMessage::End),
},
Key::Char('w') if ctrl => Some(LineInputMessage::DeleteWordBack),
Key::Char('k') if ctrl => match state.input_mode {
InputMode::Desktop => None,
InputMode::Readline => Some(LineInputMessage::DeleteToEnd),
},
Key::Char('u') if ctrl => Some(LineInputMessage::Clear),
Key::Char('c') if ctrl && state.input_mode == InputMode::Desktop => {
Some(LineInputMessage::Copy)
}
Key::Char('x') if ctrl && state.input_mode == InputMode::Desktop => {
Some(LineInputMessage::Cut)
}
Key::Char('v') if ctrl && state.input_mode == InputMode::Desktop => {
if !state.clipboard.is_empty() {
Some(LineInputMessage::Paste(state.clipboard.clone()))
} else {
None
}
}
Key::Char(_) if !ctrl => key.raw_char.map(LineInputMessage::Insert),
Key::Left if ctrl && shift => Some(LineInputMessage::SelectWordLeft),
Key::Right if ctrl && shift => Some(LineInputMessage::SelectWordRight),
Key::Left if shift => Some(LineInputMessage::SelectLeft),
Key::Right if shift => Some(LineInputMessage::SelectRight),
Key::Home if shift => Some(LineInputMessage::SelectHome),
Key::End if shift => Some(LineInputMessage::SelectEnd),
Key::Backspace if ctrl => Some(LineInputMessage::DeleteWordBack),
Key::Delete if ctrl => Some(LineInputMessage::DeleteWordForward),
Key::Backspace => Some(LineInputMessage::Backspace),
Key::Delete => Some(LineInputMessage::Delete),
Key::Left if ctrl => Some(LineInputMessage::WordLeft),
Key::Right if ctrl => Some(LineInputMessage::WordRight),
Key::Left => Some(LineInputMessage::Left),
Key::Right => Some(LineInputMessage::Right),
Key::Home => Some(LineInputMessage::Home),
Key::End => Some(LineInputMessage::End),
Key::Up => {
let (row, _) =
cursor_to_visual(&state.buffer, state.cursor, state.last_display_width);
if row == 0 {
Some(LineInputMessage::HistoryPrev)
} else {
Some(LineInputMessage::VisualUp)
}
}
Key::Down => {
let (row, _) =
cursor_to_visual(&state.buffer, state.cursor, state.last_display_width);
let chunks = chunk_buffer(&state.buffer, state.last_display_width);
let last_row = chunks.len().saturating_sub(1);
let buffer_on_phantom = row >= chunks.len();
if row >= last_row || buffer_on_phantom {
Some(LineInputMessage::HistoryNext)
} else {
Some(LineInputMessage::VisualDown)
}
}
Key::Enter => Some(LineInputMessage::Submit),
_ => None,
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
update::update(state, msg)
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
view_helpers::render(state, ctx);
}
}