use crate::draw_utils::print_with_color_and_background_at;
use crate::model::choice::Choice;
use crate::model::common::{Bounds, ScreenBuffer};
#[derive(Debug, Clone, PartialEq)]
pub enum SelectionStyle {
ColorHighlight,
InvertColors,
BorderHighlight,
PointerIndicator,
UnderlineHighlight,
BoldHighlight,
Combined(Vec<SelectionStyle>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum FocusStyle {
None,
Blinking,
Intensity,
DottedBorder,
AnimatedCursor,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FeedbackStyle {
ColorFlash,
ColorTransition,
RippleEffect,
SlideAnimation,
None,
}
#[derive(Debug, Clone)]
pub struct SelectionStyleConfig {
pub selection_style: SelectionStyle,
pub focus_style: FocusStyle,
pub feedback_style: FeedbackStyle,
pub animation_duration_ms: u32,
pub intensity_factor: f32,
pub custom_indicators: Option<SelectionIndicators>,
}
#[derive(Debug, Clone)]
pub struct SelectionIndicators {
pub pointer_symbol: String,
pub selection_prefix: String,
pub selection_suffix: String,
pub focus_indicator: String,
pub border_chars: BorderChars,
}
#[derive(Debug, Clone)]
pub struct BorderChars {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
}
impl Default for SelectionStyleConfig {
fn default() -> Self {
Self {
selection_style: SelectionStyle::ColorHighlight,
focus_style: FocusStyle::None,
feedback_style: FeedbackStyle::None,
animation_duration_ms: 200,
intensity_factor: 1.5,
custom_indicators: None,
}
}
}
impl Default for SelectionIndicators {
fn default() -> Self {
Self {
pointer_symbol: "▶".to_string(),
selection_prefix: "".to_string(),
selection_suffix: "".to_string(),
focus_indicator: "◀".to_string(),
border_chars: BorderChars::default(),
}
}
}
impl Default for BorderChars {
fn default() -> Self {
Self {
top_left: '┌',
top_right: '┐',
bottom_left: '└',
bottom_right: '┘',
horizontal: '─',
vertical: '│',
}
}
}
pub struct SelectionStyleRenderer {
pub id: String,
pub config: SelectionStyleConfig,
}
impl SelectionStyleRenderer {
pub fn new(id: String, config: SelectionStyleConfig) -> Self {
Self { id, config }
}
pub fn with_defaults(id: String) -> Self {
Self {
id,
config: SelectionStyleConfig::default(),
}
}
pub fn render_choice(
&self,
choice: &Choice,
bounds: &Bounds,
y_position: usize,
x_position: usize,
base_fg_color: &Option<String>,
base_bg_color: &Option<String>,
selected_fg_color: &Option<String>,
selected_bg_color: &Option<String>,
is_focused: bool,
buffer: &mut ScreenBuffer,
) {
let (final_fg_color, final_bg_color, display_text) = self.calculate_style_colors_and_text(
choice,
base_fg_color,
base_bg_color,
selected_fg_color,
selected_bg_color,
is_focused,
);
print_with_color_and_background_at(
y_position,
x_position,
&final_fg_color,
&final_bg_color,
&display_text,
buffer,
);
if choice.selected {
self.apply_selection_style(
choice,
bounds,
y_position,
x_position,
&display_text,
buffer,
);
}
if is_focused {
self.apply_focus_style(bounds, y_position, x_position, &display_text, buffer);
}
}
pub fn calculate_style_colors_and_text(
&self,
choice: &Choice,
base_fg_color: &Option<String>,
base_bg_color: &Option<String>,
selected_fg_color: &Option<String>,
selected_bg_color: &Option<String>,
is_focused: bool,
) -> (Option<String>, Option<String>, String) {
let mut fg_color = if choice.selected {
selected_fg_color.clone()
} else {
base_fg_color.clone()
};
let mut bg_color = if choice.selected {
selected_bg_color.clone()
} else {
base_bg_color.clone()
};
let mut display_text = choice.content.as_ref().unwrap_or(&String::new()).clone();
if choice.selected {
match &self.config.selection_style {
SelectionStyle::InvertColors => {
std::mem::swap(&mut fg_color, &mut bg_color);
}
SelectionStyle::BoldHighlight => {
fg_color = self.make_color_bright(&fg_color);
}
SelectionStyle::PointerIndicator => {
let default_indicators = SelectionIndicators::default();
let indicators = self
.config
.custom_indicators
.as_ref()
.unwrap_or(&default_indicators);
display_text = format!("{} {}", indicators.pointer_symbol, display_text);
}
SelectionStyle::Combined(styles) => {
for style in styles {
match style {
SelectionStyle::InvertColors => {
std::mem::swap(&mut fg_color, &mut bg_color);
}
SelectionStyle::BoldHighlight => {
fg_color = self.make_color_bright(&fg_color);
}
SelectionStyle::PointerIndicator => {
let default_indicators = SelectionIndicators::default();
let indicators = self
.config
.custom_indicators
.as_ref()
.unwrap_or(&default_indicators);
display_text =
format!("{} {}", indicators.pointer_symbol, display_text);
}
_ => {} }
}
}
_ => {} }
}
if is_focused && matches!(self.config.focus_style, FocusStyle::Intensity) {
fg_color = self.apply_intensity(&fg_color);
}
if choice.waiting {
display_text = format!("{}...", display_text);
}
(fg_color, bg_color, display_text)
}
fn apply_selection_style(
&self,
_choice: &Choice,
bounds: &Bounds,
y_position: usize,
x_position: usize,
display_text: &str,
buffer: &mut ScreenBuffer,
) {
match &self.config.selection_style {
SelectionStyle::BorderHighlight => {
self.draw_selection_border(
bounds,
y_position,
x_position,
display_text.len(),
buffer,
);
}
SelectionStyle::UnderlineHighlight => {
self.draw_selection_underline(
bounds,
y_position,
x_position,
display_text.len(),
buffer,
);
}
SelectionStyle::Combined(styles) => {
if styles.contains(&SelectionStyle::BorderHighlight) {
self.draw_selection_border(
bounds,
y_position,
x_position,
display_text.len(),
buffer,
);
}
if styles.contains(&SelectionStyle::UnderlineHighlight) {
self.draw_selection_underline(
bounds,
y_position,
x_position,
display_text.len(),
buffer,
);
}
}
_ => {}
}
}
fn apply_focus_style(
&self,
bounds: &Bounds,
y_position: usize,
x_position: usize,
display_text: &str,
buffer: &mut ScreenBuffer,
) {
match &self.config.focus_style {
FocusStyle::DottedBorder => {
self.draw_dotted_focus_border(
bounds,
y_position,
x_position,
display_text.len(),
buffer,
);
}
FocusStyle::AnimatedCursor => {
self.draw_animated_cursor(bounds, y_position, x_position, buffer);
}
FocusStyle::Blinking => {
self.draw_focus_indicator(bounds, y_position, x_position, buffer);
}
_ => {}
}
}
fn draw_selection_border(
&self,
bounds: &Bounds,
y_position: usize,
x_position: usize,
text_length: usize,
buffer: &mut ScreenBuffer,
) {
let default_indicators = SelectionIndicators::default();
let indicators = self
.config
.custom_indicators
.as_ref()
.unwrap_or(&default_indicators);
let border = &indicators.border_chars;
if y_position > bounds.top() && y_position < bounds.bottom() && x_position > bounds.left() {
if x_position > 0 {
print_with_color_and_background_at(
y_position,
x_position - 1,
&Some("bright_yellow".to_string()),
&Some("black".to_string()),
&border.vertical.to_string(),
buffer,
);
}
if x_position + text_length < bounds.right() {
print_with_color_and_background_at(
y_position,
x_position + text_length,
&Some("bright_yellow".to_string()),
&Some("black".to_string()),
&border.vertical.to_string(),
buffer,
);
}
}
}
fn draw_selection_underline(
&self,
bounds: &Bounds,
y_position: usize,
x_position: usize,
text_length: usize,
buffer: &mut ScreenBuffer,
) {
if y_position < bounds.bottom() {
let underline = "─".repeat(text_length);
print_with_color_and_background_at(
y_position + 1,
x_position,
&Some("bright_yellow".to_string()),
&Some("black".to_string()),
&underline,
buffer,
);
}
}
fn draw_dotted_focus_border(
&self,
bounds: &Bounds,
y_position: usize,
x_position: usize,
text_length: usize,
buffer: &mut ScreenBuffer,
) {
if y_position >= bounds.top()
&& y_position <= bounds.bottom()
&& x_position >= bounds.left()
{
if x_position > 0 {
print_with_color_and_background_at(
y_position,
x_position - 1,
&Some("bright_cyan".to_string()),
&Some("black".to_string()),
":",
buffer,
);
}
if x_position + text_length < bounds.right() {
print_with_color_and_background_at(
y_position,
x_position + text_length,
&Some("bright_cyan".to_string()),
&Some("black".to_string()),
":",
buffer,
);
}
}
}
fn draw_animated_cursor(
&self,
bounds: &Bounds,
y_position: usize,
x_position: usize,
buffer: &mut ScreenBuffer,
) {
if y_position >= bounds.top() && y_position <= bounds.bottom() && x_position > bounds.left()
{
let default_indicators = SelectionIndicators::default();
let indicators = self
.config
.custom_indicators
.as_ref()
.unwrap_or(&default_indicators);
print_with_color_and_background_at(
y_position,
x_position - 1,
&Some("bright_magenta".to_string()),
&Some("black".to_string()),
&indicators.focus_indicator,
buffer,
);
}
}
fn draw_focus_indicator(
&self,
bounds: &Bounds,
y_position: usize,
x_position: usize,
buffer: &mut ScreenBuffer,
) {
if y_position >= bounds.top() && y_position <= bounds.bottom() && x_position > bounds.left()
{
print_with_color_and_background_at(
y_position,
x_position - 1,
&Some("bright_white".to_string()),
&Some("black".to_string()),
"●",
buffer,
);
}
}
fn make_color_bright(&self, color: &Option<String>) -> Option<String> {
match color {
Some(color_str) => match color_str.as_str() {
"red" => Some("bright_red".to_string()),
"green" => Some("bright_green".to_string()),
"blue" => Some("bright_blue".to_string()),
"yellow" => Some("bright_yellow".to_string()),
"magenta" => Some("bright_magenta".to_string()),
"cyan" => Some("bright_cyan".to_string()),
"white" => Some("bright_white".to_string()),
"black" => Some("bright_black".to_string()),
_ => Some(color_str.clone()), },
None => None, }
}
fn apply_intensity(&self, color: &Option<String>) -> Option<String> {
self.make_color_bright(color)
}
pub fn get_legacy_style_config() -> SelectionStyleConfig {
SelectionStyleConfig {
selection_style: SelectionStyle::ColorHighlight,
focus_style: FocusStyle::None,
feedback_style: FeedbackStyle::None,
animation_duration_ms: 0,
intensity_factor: 1.0,
custom_indicators: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::common::ScreenBuffer;
#[test]
fn test_selection_style_renderer_creation() {
let renderer = SelectionStyleRenderer::with_defaults("test".to_string());
assert_eq!(renderer.id, "test");
assert_eq!(
renderer.config.selection_style,
SelectionStyle::ColorHighlight
);
}
#[test]
fn test_custom_selection_style_config() {
let config = SelectionStyleConfig {
selection_style: SelectionStyle::PointerIndicator,
focus_style: FocusStyle::Intensity,
feedback_style: FeedbackStyle::ColorFlash,
animation_duration_ms: 300,
intensity_factor: 2.0,
custom_indicators: Some(SelectionIndicators::default()),
};
let renderer = SelectionStyleRenderer::new("custom".to_string(), config);
assert!(matches!(
renderer.config.selection_style,
SelectionStyle::PointerIndicator
));
assert!(matches!(renderer.config.focus_style, FocusStyle::Intensity));
}
#[test]
fn test_color_bright_conversion() {
let renderer = SelectionStyleRenderer::with_defaults("test".to_string());
assert_eq!(
renderer.make_color_bright(&Some("red".to_string())),
Some("bright_red".to_string())
);
assert_eq!(
renderer.make_color_bright(&Some("green".to_string())),
Some("bright_green".to_string())
);
assert_eq!(
renderer.make_color_bright(&Some("bright_blue".to_string())),
Some("bright_blue".to_string())
); assert_eq!(renderer.make_color_bright(&None), None); }
#[test]
fn test_selection_indicators_default() {
let indicators = SelectionIndicators::default();
assert_eq!(indicators.pointer_symbol, "▶");
assert_eq!(indicators.focus_indicator, "◀");
assert_eq!(indicators.border_chars.top_left, '┌');
}
#[test]
fn test_combined_selection_styles() {
let combined = SelectionStyle::Combined(vec![
SelectionStyle::PointerIndicator,
SelectionStyle::BoldHighlight,
SelectionStyle::BorderHighlight,
]);
if let SelectionStyle::Combined(styles) = combined {
assert_eq!(styles.len(), 3);
assert!(styles.contains(&SelectionStyle::PointerIndicator));
assert!(styles.contains(&SelectionStyle::BoldHighlight));
assert!(styles.contains(&SelectionStyle::BorderHighlight));
} else {
panic!("Expected Combined selection style");
}
}
#[test]
fn test_calculate_style_colors_and_text_basic() {
let renderer = SelectionStyleRenderer::with_defaults("test".to_string());
let mut choice = Choice {
id: "test_choice".to_string(),
content: Some("Test Choice".to_string()),
selected: true,
hovered: false,
waiting: false,
script: None,
execution_mode: crate::model::common::ExecutionMode::Immediate,
redirect_output: None,
append_output: None,
};
let (fg, bg, text) = renderer.calculate_style_colors_and_text(
&choice,
&Some("white".to_string()),
&Some("black".to_string()),
&Some("bright_white".to_string()),
&Some("blue".to_string()),
false,
);
assert_eq!(fg, Some("bright_white".to_string()));
assert_eq!(bg, Some("blue".to_string()));
assert_eq!(text, "Test Choice");
}
#[test]
fn test_calculate_style_colors_and_text_with_pointer() {
let config = SelectionStyleConfig {
selection_style: SelectionStyle::PointerIndicator,
focus_style: FocusStyle::None,
feedback_style: FeedbackStyle::None,
animation_duration_ms: 0,
intensity_factor: 1.0,
custom_indicators: Some(SelectionIndicators::default()),
};
let renderer = SelectionStyleRenderer::new("test".to_string(), config);
let choice = Choice {
id: "menu_item".to_string(),
content: Some("Menu Item".to_string()),
selected: true,
hovered: false,
waiting: false,
script: None,
execution_mode: crate::model::common::ExecutionMode::Immediate,
redirect_output: None,
append_output: None,
};
let (fg, bg, text) = renderer.calculate_style_colors_and_text(
&choice,
&Some("white".to_string()),
&Some("black".to_string()),
&Some("bright_white".to_string()),
&Some("blue".to_string()),
false,
);
assert_eq!(fg, Some("bright_white".to_string()));
assert_eq!(bg, Some("blue".to_string()));
assert_eq!(text, "▶ Menu Item");
}
#[test]
fn test_inverted_colors() {
let config = SelectionStyleConfig {
selection_style: SelectionStyle::InvertColors,
..SelectionStyleConfig::default()
};
let renderer = SelectionStyleRenderer::new("test".to_string(), config);
let choice = Choice {
id: "inverted".to_string(),
content: Some("Inverted".to_string()),
selected: true,
hovered: false,
waiting: false,
script: None,
execution_mode: crate::model::common::ExecutionMode::Immediate,
redirect_output: None,
append_output: None,
};
let (fg, bg, text) = renderer.calculate_style_colors_and_text(
&choice,
&Some("white".to_string()),
&Some("black".to_string()),
&Some("bright_white".to_string()),
&Some("blue".to_string()),
false,
);
assert_eq!(fg, Some("blue".to_string()));
assert_eq!(bg, Some("bright_white".to_string()));
assert_eq!(text, "Inverted");
}
#[test]
fn test_waiting_state_indicator() {
let renderer = SelectionStyleRenderer::with_defaults("test".to_string());
let choice = Choice {
id: "loading".to_string(),
content: Some("Loading".to_string()),
selected: false,
hovered: false,
waiting: true,
script: None,
execution_mode: crate::model::common::ExecutionMode::Immediate,
redirect_output: None,
append_output: None,
};
let (fg, bg, text) = renderer.calculate_style_colors_and_text(
&choice,
&Some("white".to_string()),
&Some("black".to_string()),
&Some("bright_white".to_string()),
&Some("blue".to_string()),
false,
);
assert_eq!(text, "Loading...");
}
#[test]
fn test_focus_intensity() {
let config = SelectionStyleConfig {
selection_style: SelectionStyle::ColorHighlight,
focus_style: FocusStyle::Intensity,
..SelectionStyleConfig::default()
};
let renderer = SelectionStyleRenderer::new("test".to_string(), config);
let choice = Choice {
id: "focused".to_string(),
content: Some("Focused".to_string()),
selected: true,
hovered: false,
waiting: false,
script: None,
execution_mode: crate::model::common::ExecutionMode::Immediate,
redirect_output: None,
append_output: None,
};
let (fg, bg, text) = renderer.calculate_style_colors_and_text(
&choice,
&Some("white".to_string()),
&Some("black".to_string()),
&Some("red".to_string()),
&Some("blue".to_string()),
true, );
assert_eq!(fg, Some("bright_red".to_string()));
assert_eq!(bg, Some("blue".to_string()));
}
}