use crate::charset::Charset;
use crate::theme::Theme;
use crate::video_buffer::{self, Cell, VideoBuffer};
use crossterm::style::Color;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PromptType {
#[allow(dead_code)]
Info, #[allow(dead_code)]
Success, #[allow(dead_code)]
Warning, Danger, }
impl PromptType {
pub fn background_color(&self, theme: &Theme) -> Color {
match self {
PromptType::Info => theme.prompt_info_bg,
PromptType::Success => theme.prompt_success_bg,
PromptType::Warning => theme.prompt_warning_bg,
PromptType::Danger => theme.prompt_danger_bg,
}
}
pub fn foreground_color(&self, theme: &Theme) -> Color {
match self {
PromptType::Info => theme.prompt_info_fg,
PromptType::Success => theme.prompt_success_fg,
PromptType::Warning => theme.prompt_warning_fg,
PromptType::Danger => theme.prompt_danger_fg,
}
}
}
#[derive(Clone, Debug)]
pub struct PromptButton {
pub text: String,
pub action: PromptAction,
pub is_primary: bool, }
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PromptAction {
Confirm,
Cancel,
#[allow(dead_code)]
Custom(u32),
}
impl PromptButton {
pub fn new(text: String, action: PromptAction, is_primary: bool) -> Self {
Self {
text,
action,
is_primary,
}
}
pub fn colors(&self, prompt_type: PromptType, theme: &Theme) -> (Color, Color) {
if self.is_primary {
match prompt_type {
PromptType::Info => (
theme.dialog_button_primary_info_fg,
theme.dialog_button_primary_info_bg,
),
PromptType::Success => (
theme.dialog_button_primary_success_fg,
theme.dialog_button_primary_success_bg,
),
PromptType::Warning => (
theme.dialog_button_primary_warning_fg,
theme.dialog_button_primary_warning_bg,
),
PromptType::Danger => (
theme.dialog_button_primary_danger_fg,
theme.dialog_button_primary_danger_bg,
),
}
} else {
(
theme.dialog_button_secondary_fg,
theme.dialog_button_secondary_bg,
)
}
}
pub fn width(&self) -> u16 {
self.text.len() as u16 + 4
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TextAlign {
#[allow(dead_code)]
Left,
Center,
}
pub struct Prompt {
pub prompt_type: PromptType,
pub message: String,
pub buttons: Vec<PromptButton>,
pub width: u16,
pub height: u16,
pub x: u16,
pub y: u16,
pub selected_button_index: usize, pub text_align: TextAlign, }
impl Prompt {
pub fn new(
prompt_type: PromptType,
message: String,
buttons: Vec<PromptButton>,
buffer_width: u16,
buffer_height: u16,
) -> Self {
Self::new_with_alignment(
prompt_type,
message,
buttons,
buffer_width,
buffer_height,
TextAlign::Center,
)
}
pub fn new_with_alignment(
prompt_type: PromptType,
message: String,
buttons: Vec<PromptButton>,
buffer_width: u16,
buffer_height: u16,
text_align: TextAlign,
) -> Self {
let message_lines: Vec<&str> = message.lines().collect();
let max_message_width = message_lines
.iter()
.map(|line| Self::strip_color_codes(line).len())
.max()
.unwrap_or(0) as u16;
let total_button_width: u16 = buttons.iter().map(|b| b.width()).sum::<u16>()
+ (buttons.len().saturating_sub(1)) as u16 * 2;
let content_width = max_message_width.max(total_button_width);
let width = content_width + 6;
let height = message_lines.len() as u16 + 6;
let x = (buffer_width.saturating_sub(width)) / 2;
let y = (buffer_height.saturating_sub(height)) / 2;
let selected_button_index = buttons.iter().position(|b| b.is_primary).unwrap_or(0);
Self {
prompt_type,
message,
buttons,
width,
height,
x,
y,
selected_button_index,
text_align,
}
}
fn strip_color_codes(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '{' {
for next in chars.by_ref() {
if next == '}' {
break;
}
}
} else {
result.push(ch);
}
}
result
}
fn parse_color_code(code: &str) -> Option<Color> {
match code {
"Y" | "y" => Some(Color::Yellow),
"C" | "c" => Some(Color::Cyan),
"W" | "w" => Some(Color::White),
"G" | "g" => Some(Color::Green),
"R" | "r" => Some(Color::Red),
"M" | "m" => Some(Color::Magenta),
"B" | "b" => Some(Color::Blue),
"DG" | "dg" => Some(Color::DarkGrey),
_ => None,
}
}
pub fn render(&self, buffer: &mut VideoBuffer, charset: &Charset, theme: &Theme) {
let bg_color = self.prompt_type.background_color(theme);
let default_fg_color = self.prompt_type.foreground_color(theme);
for y in 0..self.height {
for x in 0..self.width {
buffer.set(
self.x + x,
self.y + y,
Cell::new(' ', default_fg_color, bg_color),
);
}
}
let message_lines: Vec<&str> = self.message.lines().collect();
let message_start_y = self.y + 2;
for (i, line) in message_lines.iter().enumerate() {
let line_y = message_start_y + i as u16;
let stripped_line = Self::strip_color_codes(line);
let line_x = match self.text_align {
TextAlign::Center => {
self.x + (self.width.saturating_sub(stripped_line.len() as u16)) / 2
}
TextAlign::Left => self.x + 3, };
let mut current_x = line_x;
let mut current_color = default_fg_color;
let mut chars = line.chars();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut code = String::new();
for next in chars.by_ref() {
if next == '}' {
if let Some(color) = Self::parse_color_code(&code) {
current_color = color;
}
break;
}
code.push(next);
}
} else {
buffer.set(current_x, line_y, Cell::new(ch, current_color, bg_color));
current_x += 1;
}
}
}
let button_y = self.y + self.height - 2;
let total_button_width: u16 = self.buttons.iter().map(|b| b.width()).sum::<u16>()
+ (self.buttons.len().saturating_sub(1)) as u16 * 2;
let mut button_x = self.x + (self.width.saturating_sub(total_button_width)) / 2;
for (index, button) in self.buttons.iter().enumerate() {
let (button_fg, button_bg) = button.colors(self.prompt_type, theme);
let is_selected = index == self.selected_button_index;
if is_selected {
if button_x > self.x {
buffer.set(
button_x - 1,
button_y,
Cell::new('>', default_fg_color, bg_color),
);
}
}
buffer.set(button_x, button_y, Cell::new('[', button_fg, button_bg));
button_x += 1;
buffer.set(button_x, button_y, Cell::new(' ', button_fg, button_bg));
button_x += 1;
for ch in button.text.chars() {
buffer.set(button_x, button_y, Cell::new(ch, button_fg, button_bg));
button_x += 1;
}
buffer.set(button_x, button_y, Cell::new(' ', button_fg, button_bg));
button_x += 1;
buffer.set(button_x, button_y, Cell::new(']', button_fg, button_bg));
button_x += 1;
if is_selected {
buffer.set(
button_x,
button_y,
Cell::new('<', default_fg_color, bg_color),
);
}
button_x += 2;
}
video_buffer::render_shadow(
buffer,
self.x,
self.y,
self.width,
self.height,
charset,
theme,
);
}
pub fn handle_click(&self, x: u16, y: u16) -> Option<PromptAction> {
let button_y = self.y + self.height - 2;
if y != button_y {
return None;
}
let total_button_width: u16 = self.buttons.iter().map(|b| b.width()).sum::<u16>()
+ (self.buttons.len().saturating_sub(1)) as u16 * 2;
let mut button_x = self.x + (self.width.saturating_sub(total_button_width)) / 2;
for button in &self.buttons {
let button_width = button.width();
let button_end = button_x + button_width;
if x >= button_x && x < button_end {
return Some(button.action);
}
button_x = button_end + 2; }
None
}
pub fn contains_point(&self, x: u16, y: u16) -> bool {
x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
}
pub fn select_next_button(&mut self) {
if !self.buttons.is_empty() {
self.selected_button_index = (self.selected_button_index + 1) % self.buttons.len();
}
}
pub fn select_previous_button(&mut self) {
if !self.buttons.is_empty() {
self.selected_button_index = if self.selected_button_index == 0 {
self.buttons.len() - 1
} else {
self.selected_button_index - 1
};
}
}
pub fn get_selected_action(&self) -> Option<PromptAction> {
self.buttons
.get(self.selected_button_index)
.map(|b| b.action)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prompt_creation() {
let buttons = vec![
PromptButton::new("Yes".to_string(), PromptAction::Confirm, true),
PromptButton::new("No".to_string(), PromptAction::Cancel, false),
];
let prompt = Prompt::new(
PromptType::Danger,
"Are you sure?".to_string(),
buttons,
80,
24,
);
assert_eq!(prompt.prompt_type, PromptType::Danger);
assert_eq!(prompt.buttons.len(), 2);
}
#[test]
fn test_button_width() {
let button = PromptButton::new("OK".to_string(), PromptAction::Confirm, true);
assert_eq!(button.width(), 6); }
}