use super::button::Button;
use super::dialog::Dialog;
use super::input_line::InputLine;
use super::label::Label;
use super::static_text::StaticText;
use crate::app::Application;
use crate::core::command::{CM_CANCEL, CM_NO, CM_OK, CM_YES, CommandId};
use crate::core::geometry::Rect;
use std::cell::RefCell;
use std::rc::Rc;
pub const MF_WARNING: u16 = 0x0000;
pub const MF_ERROR: u16 = 0x0001;
pub const MF_INFORMATION: u16 = 0x0002;
pub const MF_CONFIRMATION: u16 = 0x0003;
pub const MF_YES_BUTTON: u16 = 0x0100;
pub const MF_NO_BUTTON: u16 = 0x0200;
pub const MF_OK_BUTTON: u16 = 0x0400;
pub const MF_CANCEL_BUTTON: u16 = 0x0800;
pub const MF_YES_NO_CANCEL: u16 = MF_YES_BUTTON | MF_NO_BUTTON | MF_CANCEL_BUTTON;
pub const MF_OK_CANCEL: u16 = MF_OK_BUTTON | MF_CANCEL_BUTTON;
pub fn message_box(app: &mut Application, message: &str, options: u16) -> CommandId {
let (screen_w, screen_h) = app.terminal.size();
let target_w = 60usize;
let inner_w = target_w.saturating_sub(6).max(10);
let wrapped = wrap_message(message, inner_w);
let msg_width = wrapped.lines().map(|l| l.chars().count()).max().unwrap_or(20);
let msg_height = wrapped.lines().count().max(1);
let width = (msg_width + 6).min(target_w).max(30);
let max_height = (screen_h as usize).saturating_sub(2).max(7);
let height = (msg_height + 6).min(max_height).max(7);
let x = (screen_w - width as i16) / 2;
let y = (screen_h - height as i16) / 2;
let bounds = Rect::new(x, y, x + width as i16, y + height as i16);
message_box_rect(app, bounds, &wrapped, options)
}
#[allow(unused_assignments)] fn wrap_message(message: &str, max_width: usize) -> String {
let max_width = max_width.max(1);
let mut out = String::new();
for (i, paragraph) in message.split('\n').enumerate() {
if i > 0 {
out.push('\n');
}
if paragraph.chars().count() <= max_width {
out.push_str(paragraph);
continue;
}
let mut line_len = 0usize;
let mut first_word_in_line = true;
for word in paragraph.split_whitespace() {
let word_chars: Vec<char> = word.chars().collect();
let word_len = word_chars.len();
if word_len > max_width {
if !first_word_in_line {
out.push('\n');
line_len = 0;
first_word_in_line = true;
}
let mut start = 0;
while start < word_len {
let end = (start + max_width).min(word_len);
if start > 0 {
out.push('\n');
}
for ch in &word_chars[start..end] {
out.push(*ch);
}
start = end;
}
line_len = (word_len % max_width).max(if word_len % max_width == 0 {
max_width
} else {
0
});
first_word_in_line = false;
continue;
}
let needed = if first_word_in_line {
word_len
} else {
line_len + 1 + word_len
};
if needed > max_width {
out.push('\n');
out.push_str(word);
line_len = word_len;
} else {
if !first_word_in_line {
out.push(' ');
line_len += 1;
}
out.push_str(word);
line_len += word_len;
}
first_word_in_line = false;
}
}
out
}
pub fn message_box_rect(app: &mut Application, bounds: Rect, message: &str, options: u16) -> CommandId {
let title = match options & 0x03 {
MF_WARNING => "\u{26A0} Warning",
MF_ERROR => "\u{274C} Error",
MF_INFORMATION => "\u{2139}\u{FE0F} Information",
MF_CONFIRMATION => "\u{2753} Confirm",
_ => "Message",
};
let mut dialog = Dialog::new(bounds, title);
let text_bounds = Rect::new(3, 1, bounds.width() - 2, bounds.height() - 4);
dialog.add(Box::new(StaticText::new(text_bounds, message)));
let button_configs = [
(MF_YES_BUTTON, " ~Y~es", CM_YES),
(MF_NO_BUTTON, " ~N~o", CM_NO),
(MF_OK_BUTTON, " ~O~K", CM_OK),
(MF_CANCEL_BUTTON, " ~C~ancel", CM_CANCEL),
];
let mut buttons = Vec::new();
for (flag, label, cmd) in &button_configs {
if options & flag != 0 {
buttons.push((*label, *cmd));
}
}
let button_y = bounds.height() - 4;
let total_width: usize = buttons.iter().map(|(label, _)| label.len() + 2).sum();
let mut x = (bounds.width_clamped() as usize - total_width) / 2;
let is_default = buttons.len() == 1 || (options & MF_OK_BUTTON != 0);
for (i, (label, cmd)) in buttons.iter().enumerate() {
let button_width = label.len() as i16;
let button_bounds = Rect::new(x as i16, button_y, x as i16 + button_width, button_y + 2);
let is_this_default = is_default && (i == 0 || *cmd == CM_OK);
dialog.add(Box::new(Button::new(button_bounds, label, *cmd, is_this_default)));
x += button_width as usize + 2;
}
dialog.set_initial_focus();
dialog.execute(app)
}
pub fn message_box_ok(app: &mut Application, message: &str) -> CommandId {
message_box(app, message, MF_INFORMATION | MF_OK_BUTTON)
}
pub fn message_box_error(app: &mut Application, message: &str) -> CommandId {
message_box(app, message, MF_ERROR | MF_OK_BUTTON)
}
pub fn message_box_warning(app: &mut Application, message: &str) -> CommandId {
message_box(app, message, MF_WARNING | MF_OK_BUTTON)
}
pub fn confirmation_box(app: &mut Application, message: &str) -> CommandId {
message_box(app, message, MF_CONFIRMATION | MF_YES_NO_CANCEL)
}
pub fn confirmation_box_yes_no(app: &mut Application, message: &str) -> CommandId {
message_box(app, message, MF_CONFIRMATION | MF_YES_BUTTON | MF_NO_BUTTON)
}
pub fn confirmation_box_ok_cancel(app: &mut Application, message: &str) -> CommandId {
message_box(app, message, MF_CONFIRMATION | MF_OK_CANCEL)
}
pub fn input_box(app: &mut Application, title: &str, label: &str, initial: &str, max_length: usize) -> Option<String> {
let label_len = label.len();
let width = (label_len + max_length + 12).min(60).max(30);
let height = 8;
let (screen_w, screen_h) = app.terminal.size();
let x = (screen_w - width as i16) / 2;
let y = (screen_h - height as i16) / 2;
let bounds = Rect::new(x, y, x + width as i16, y + height as i16);
input_box_rect(app, bounds, title, label, initial, max_length)
}
pub fn input_box_rect(app: &mut Application, bounds: Rect, title: &str, label: &str, initial: &str, max_length: usize) -> Option<String> {
let mut dialog = Dialog::new(bounds, title);
let data = Rc::new(RefCell::new(initial.to_string()));
let label_x = 2;
let label_width = label.len() as i16;
let label_bounds = Rect::new(label_x, 2, label_x + label_width, 3);
dialog.add(Box::new(Label::new(label_bounds, label)));
let input_x = label_x + label_width + 1;
let input_width = (bounds.width() - input_x - 3).min(max_length as i16 + 2);
let input_bounds = Rect::new(input_x, 2, input_x + input_width, 3);
dialog.add(Box::new(InputLine::new(input_bounds, max_length, data.clone())));
let button_y = bounds.height() - 4;
let ok_x = bounds.width() / 2 - 11;
let ok_bounds = Rect::new(ok_x, button_y, ok_x + 10, button_y + 2);
dialog.add(Box::new(Button::new(ok_bounds, " ~O~K", CM_OK, true)));
let cancel_x = ok_x + 12;
let cancel_bounds = Rect::new(cancel_x, button_y, cancel_x + 10, button_y + 2);
dialog.add(Box::new(Button::new(cancel_bounds, " ~C~ancel", CM_CANCEL, false)));
dialog.set_initial_focus();
let result = dialog.execute(app);
if result == CM_OK { Some(data.borrow().clone()) } else { None }
}
pub fn search_box(app: &mut Application, title: &str) -> Option<String> {
let width = 50;
let height = 9;
let (screen_w, screen_h) = app.terminal.size();
let x = (screen_w - width) / 2;
let y = (screen_h - height) / 2;
let bounds = Rect::new(x, y, x + width, y + height);
let mut dialog = Dialog::new(bounds, title);
let data = Rc::new(RefCell::new(String::new()));
let label_bounds = Rect::new(2, 2, 20, 3);
dialog.add(Box::new(Label::new(label_bounds, "~F~ind:")));
let input_bounds = Rect::new(2, 3, width - 4, 4);
dialog.add(Box::new(InputLine::new(input_bounds, 100, data.clone())));
let ok_bounds = Rect::new(15, 5, 25, 7);
dialog.add(Box::new(Button::new(ok_bounds, " ~O~K", CM_OK, true)));
let cancel_bounds = Rect::new(27, 5, 37, 7);
dialog.add(Box::new(Button::new(cancel_bounds, " ~C~ancel", CM_CANCEL, false)));
dialog.set_initial_focus();
let result = dialog.execute(app);
if result == CM_OK {
let text = data.borrow().clone();
if !text.is_empty() { Some(text) } else { None }
} else {
None
}
}
pub fn search_replace_box(app: &mut Application, title: &str) -> Option<(String, String)> {
let width = 50;
let height = 13;
let (screen_w, screen_h) = app.terminal.size();
let x = (screen_w - width) / 2;
let y = (screen_h - height) / 2;
let bounds = Rect::new(x, y, x + width, y + height);
let mut dialog = Dialog::new(bounds, title);
let find_data = Rc::new(RefCell::new(String::new()));
let replace_data = Rc::new(RefCell::new(String::new()));
let label1_bounds = Rect::new(2, 2, 20, 3);
dialog.add(Box::new(Label::new(label1_bounds, "~F~ind:")));
let input1_bounds = Rect::new(2, 3, width - 4, 4);
dialog.add(Box::new(InputLine::new(input1_bounds, 100, find_data.clone())));
let label2_bounds = Rect::new(2, 5, 20, 6);
dialog.add(Box::new(Label::new(label2_bounds, "~R~eplace with:")));
let input2_bounds = Rect::new(2, 6, width - 4, 7);
dialog.add(Box::new(InputLine::new(input2_bounds, 100, replace_data.clone())));
let ok_bounds = Rect::new(15, 9, 25, 11);
dialog.add(Box::new(Button::new(ok_bounds, " ~O~K", CM_OK, true)));
let cancel_bounds = Rect::new(27, 9, 37, 11);
dialog.add(Box::new(Button::new(cancel_bounds, " ~C~ancel", CM_CANCEL, false)));
dialog.set_initial_focus();
let result = dialog.execute(app);
if result == CM_OK {
let find_text = find_data.borrow().clone();
if !find_text.is_empty() {
let replace_text = replace_data.borrow().clone();
Some((find_text, replace_text))
} else {
None
}
} else {
None
}
}
pub fn goto_line_box(app: &mut Application, title: &str) -> Option<usize> {
let width = 40;
let height = 8;
let (screen_w, screen_h) = app.terminal.size();
let x = (screen_w - width) / 2;
let y = (screen_h - height) / 2;
let bounds = Rect::new(x, y, x + width, y + height);
let mut dialog = Dialog::new(bounds, title);
let data = Rc::new(RefCell::new(String::new()));
let label_bounds = Rect::new(2, 2, 20, 3);
dialog.add(Box::new(Label::new(label_bounds, " ~L~ine number:")));
let input_bounds = Rect::new(2, 3, width - 4, 4);
dialog.add(Box::new(InputLine::new(input_bounds, 10, data.clone())));
let ok_bounds = Rect::new(10, 5, 20, 7);
dialog.add(Box::new(Button::new(ok_bounds, " ~O~K", CM_OK, true)));
let cancel_bounds = Rect::new(22, 5, 32, 7);
dialog.add(Box::new(Button::new(cancel_bounds, " ~C~ancel", CM_CANCEL, false)));
dialog.set_initial_focus();
let result = dialog.execute(app);
if result == CM_OK {
let text = data.borrow().clone();
text.parse::<usize>().ok()
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::wrap_message;
#[test]
fn wraps_long_lines_at_word_boundaries() {
let out = wrap_message(
"Parse error: line 1:1: expected 'program', found identifier 'hello'",
30,
);
for line in out.lines() {
assert!(line.chars().count() <= 30, "line too long: {line:?}");
}
let original_words: Vec<&str> = "Parse error: line 1:1: expected 'program', found identifier 'hello'"
.split_whitespace()
.collect();
let wrapped_words: Vec<&str> = out.split_whitespace().collect();
assert_eq!(original_words, wrapped_words);
}
#[test]
fn preserves_existing_newlines() {
let out = wrap_message("first paragraph\nsecond paragraph", 40);
assert_eq!(out, "first paragraph\nsecond paragraph");
}
#[test]
fn breaks_overlong_words_character_wise() {
let path = "averylongfilenameWithoutSpaces.txt";
let out = wrap_message(path, 10);
for line in out.lines() {
assert!(line.chars().count() <= 10);
}
let recombined: String = out.lines().collect();
assert_eq!(recombined, path);
}
}