#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
use std::mem;
use crossterm::event::KeyCode;
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui_core::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::Widget,
};
pub const BOTTOM_TITLE: &str = "Press Enter to submit or Esc to abort";
#[derive(PartialEq, Default, Clone)]
pub struct Dialog {
pub open: bool,
pub submitted: bool,
pub working_input: String,
pub submitted_input: String,
cursor_position: usize,
title_top: Option<String>,
title_bottom: Option<String>,
borders: Option<Borders>,
style: Option<Style>,
}
impl Dialog {
pub fn key_action(&mut self, key_code: &KeyCode) {
self.submitted = false;
match key_code {
KeyCode::Char(to_insert) => self.insert_char(*to_insert),
KeyCode::Backspace => self.backspace(),
KeyCode::Delete => self.delete(),
KeyCode::End => self.end(),
KeyCode::Home => self.home(),
KeyCode::Left => self.move_cursor_left(),
KeyCode::Right => self.move_cursor_right(),
KeyCode::Enter => {
self.submitted_input = mem::take(&mut self.working_input);
self.submitted_input = self.submitted_input.trim().to_string();
self.submitted = true;
self.open = false;
self.cursor_position = 0;
}
KeyCode::Esc => {
self.working_input.clear();
self.open = false;
self.cursor_position = 0;
}
_ => (),
}
}
pub fn title_top(&mut self, title: &str) -> Self {
self.title_top = Some(title.to_owned());
self.clone()
}
pub fn title_bottom(&mut self, title: &str) -> Self {
self.title_bottom = Some(title.to_owned());
self.clone()
}
pub fn borders(&mut self, borders: Borders) -> Self {
self.borders = Some(borders);
self.clone()
}
pub fn style(&mut self, style: Style) -> Self {
self.style = Some(style);
self.clone()
}
fn render_working_input(&self) -> Line<'_> {
let text = format!("{} ", self.working_input);
let text_len = text.chars().count();
let text = text.chars();
let before_cursor = Span::raw(text.clone().take(self.cursor_position).collect::<String>());
let under_cursor = Span::raw(
text.clone()
.skip(self.cursor_position)
.take(1)
.collect::<String>(),
)
.reversed();
let after_cursor = if self.cursor_position != text_len {
Span::raw(
text.clone()
.skip(self.cursor_position + 1)
.collect::<String>(),
)
} else {
Span::raw("")
};
Line::from(vec![before_cursor, under_cursor, after_cursor])
}
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.cursor_position.saturating_sub(1);
self.cursor_position = self.clamp_cursor(cursor_moved_left);
}
fn move_cursor_right(&mut self) {
let cursor_moved_right = self.cursor_position.saturating_add(1);
self.cursor_position = self.clamp_cursor(cursor_moved_right);
}
fn insert_char(&mut self, new_char: char) {
self.working_input.insert(self.cursor_position, new_char);
self.move_cursor_right();
}
fn backspace(&mut self) {
if self.cursor_position != 0 {
let current_index = self.cursor_position;
let from_left_to_current_index = current_index - 1;
let before_char_to_delete = self.working_input.chars().take(from_left_to_current_index);
let after_char_to_delete = self.working_input.chars().skip(current_index);
self.working_input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left();
}
}
fn delete(&mut self) {
let current_index = self.cursor_position;
let before_char_to_delete = self.working_input.chars().take(current_index);
let after_char_to_delete = self.working_input.chars().skip(current_index + 1);
self.working_input = before_char_to_delete.chain(after_char_to_delete).collect();
}
fn end(&mut self) {
self.cursor_position = self.working_input.chars().count();
}
fn home(&mut self) {
self.cursor_position = 0;
}
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.working_input.len())
}
}
impl Widget for Dialog {
fn render(mut self, area: Rect, buf: &mut Buffer) {
if self.open {
Clear.render(area, buf);
let mut dialog_block = Block::default().title_alignment(Alignment::Center);
if let Some(ref mut v) = self.title_top {
dialog_block = dialog_block.title_top(mem::take(v))
}
dialog_block = if let Some(ref mut v) = self.title_bottom {
dialog_block.title_bottom(mem::take(v))
} else {
dialog_block.title_bottom(BOTTOM_TITLE)
};
dialog_block = if let Some(ref mut v) = self.borders {
dialog_block.borders(mem::take(v))
} else {
dialog_block.borders(Borders::ALL)
};
dialog_block = if let Some(ref mut v) = self.style {
dialog_block.style(mem::take(v))
} else {
dialog_block.style(Style::default().bg(Color::DarkGray))
};
Paragraph::new(self.render_working_input())
.block(dialog_block)
.render(area, buf)
}
}
}
pub fn centered_rect(
r: Rect,
width: u16,
height: u16,
horizontal_offset: i16,
vertical_offset: i16,
) -> Rect {
let (dialog_layout, index) = if vertical_offset.is_negative() {
(
Layout::vertical([
Constraint::Fill(1),
Constraint::Length(height),
Constraint::Fill(1),
Constraint::Length(vertical_offset.unsigned_abs()),
])
.split(r),
1,
)
} else {
(
Layout::vertical([
Constraint::Length(vertical_offset as u16),
Constraint::Fill(1),
Constraint::Length(height),
Constraint::Fill(1),
])
.split(r),
2,
)
};
if horizontal_offset.is_negative() {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(width),
Constraint::Fill(1),
Constraint::Length(horizontal_offset.unsigned_abs()),
])
.split(dialog_layout[index])[1]
} else {
Layout::horizontal([
Constraint::Length(horizontal_offset as u16),
Constraint::Fill(1),
Constraint::Length(width),
Constraint::Fill(1),
])
.split(dialog_layout[index])[2]
}
}