use crate::console::{Console, ConsoleOptions, Renderable};
use crate::segment::Segment;
use crate::style::Style;
use crate::text::Text;
#[derive(Debug, Clone)]
pub struct Accordion {
pub title: String,
pub content: Text,
pub collapsed: bool,
pub style: Style,
pub title_style: Style,
pub icon_style: Style,
pub expand_icon: String,
pub collapse_icon: String,
pub indent: usize,
}
impl Accordion {
pub fn new(title: impl Into<String>, content: impl Into<Text>) -> Self {
Accordion {
title: title.into(),
content: content.into(),
collapsed: false,
style: Style::null(),
title_style: Style::null(),
icon_style: Style::null(),
expand_icon: "▶".to_string(),
collapse_icon: "▼".to_string(),
indent: 2,
}
}
#[must_use]
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
self
}
pub fn toggle(&mut self) {
self.collapsed = !self.collapsed;
}
pub fn expand(&mut self) {
self.collapsed = false;
}
pub fn collapse(&mut self) {
self.collapsed = true;
}
pub fn is_collapsed(&self) -> bool {
self.collapsed
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = style;
self
}
#[must_use]
pub fn icon_style(mut self, style: Style) -> Self {
self.icon_style = style;
self
}
#[must_use]
pub fn icons(mut self, expand: impl Into<String>, collapse: impl Into<String>) -> Self {
self.expand_icon = expand.into();
self.collapse_icon = collapse.into();
self
}
#[must_use]
pub fn indent(mut self, indent: usize) -> Self {
self.indent = indent;
self
}
fn current_icon(&self) -> &str {
if self.collapsed {
&self.expand_icon
} else {
&self.collapse_icon
}
}
}
#[derive(Debug, Clone)]
pub struct AccordionGroup {
pub items: Vec<Accordion>,
pub allow_multiple_open: bool,
}
impl AccordionGroup {
pub fn new(items: Vec<Accordion>) -> Self {
AccordionGroup {
items,
allow_multiple_open: true,
}
}
#[must_use]
pub fn allow_multiple_open(mut self, allow: bool) -> Self {
self.allow_multiple_open = allow;
self
}
pub fn expand_all(&mut self) {
for item in &mut self.items {
item.expand();
}
}
pub fn collapse_all(&mut self) {
for item in &mut self.items {
item.collapse();
}
}
pub fn push(&mut self, accordion: Accordion) {
self.items.push(accordion);
}
pub fn expand_item(&mut self, index: usize) {
if index >= self.items.len() {
return;
}
if !self.allow_multiple_open {
for (i, item) in self.items.iter_mut().enumerate() {
if i == index {
item.expand();
} else {
item.collapse();
}
}
} else {
self.items[index].expand();
}
}
}
impl Renderable for Accordion {
fn gilt_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let mut segments = Vec::new();
let max_width = options.max_width;
let icon_width = self.current_icon().chars().count() + 1; let content_width = max_width.saturating_sub(icon_width.max(self.indent));
let icon = self.current_icon();
if !self.icon_style.is_null() {
segments.push(Segment::styled(icon, self.icon_style.clone()));
} else {
segments.push(Segment::text(icon));
}
segments.push(Segment::text(" "));
let title_text = if !self.title_style.is_null() {
Segment::styled(&self.title, self.title_style.clone())
} else if !self.style.is_null() {
Segment::styled(&self.title, self.style.clone())
} else {
Segment::text(&self.title)
};
segments.push(title_text);
segments.push(Segment::line());
if !self.collapsed {
let mut content = self.content.clone();
content.end = String::new();
let content_opts = options.update_width(content_width);
let content_segments = content.gilt_console(console, &content_opts);
let lines = Segment::split_lines(&content_segments);
let indent_str = " ".repeat(self.indent);
for line in lines {
if !line.is_empty() {
segments.push(Segment::text(&indent_str));
segments.extend(line);
} else {
segments.push(Segment::line());
}
}
}
segments
}
}
impl Renderable for AccordionGroup {
fn gilt_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let mut segments = Vec::new();
for (i, item) in self.items.iter().enumerate() {
let item_segments = item.gilt_console(console, options);
segments.extend(item_segments);
if i < self.items.len() - 1 {
segments.push(Segment::line());
}
}
segments
}
}
impl std::fmt::Display for Accordion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut console = Console::builder()
.width(f.width().unwrap_or(80))
.force_terminal(true)
.no_color(true)
.build();
console.begin_capture();
console.print(self);
let output = console.end_capture();
write!(f, "{}", output.trim_end_matches('\n'))
}
}
impl std::fmt::Display for AccordionGroup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut console = Console::builder()
.width(f.width().unwrap_or(80))
.force_terminal(true)
.no_color(true)
.build();
console.begin_capture();
console.print(self);
let output = console.end_capture();
write!(f, "{}", output.trim_end_matches('\n'))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_console(width: usize) -> Console {
Console::builder()
.width(width)
.force_terminal(true)
.no_color(true)
.markup(false)
.build()
}
fn segments_to_text(segments: &[Segment]) -> String {
segments.iter().map(|s| s.text.as_str()).collect()
}
fn render_accordion(console: &Console, accordion: &Accordion) -> String {
let opts = console.options();
let segments = accordion.gilt_console(console, &opts);
segments_to_text(&segments)
}
#[test]
fn test_accordion_expanded() {
let console = make_console(80);
let accordion = Accordion::new(
"Section Title",
Text::new("Content line 1\nContent line 2", Style::null()),
);
let output = render_accordion(&console, &accordion);
assert!(output.contains("▼"));
assert!(output.contains("Section Title"));
assert!(output.contains("Content line 1"));
assert!(output.contains("Content line 2"));
}
#[test]
fn test_accordion_collapsed() {
let console = make_console(80);
let accordion = Accordion::new(
"Section Title",
Text::new("Content line 1\nContent line 2", Style::null()),
)
.collapsed(true);
let output = render_accordion(&console, &accordion);
assert!(output.contains("▶"));
assert!(output.contains("Section Title"));
assert!(!output.contains("Content line 1"));
assert!(!output.contains("Content line 2"));
}
#[test]
fn test_toggle() {
let mut accordion = Accordion::new("Test", Text::new("Content", Style::null()));
assert!(!accordion.is_collapsed());
accordion.toggle();
assert!(accordion.is_collapsed());
accordion.toggle();
assert!(!accordion.is_collapsed());
}
#[test]
fn test_expand_collapse() {
let mut accordion = Accordion::new("Test", Text::new("Content", Style::null()));
accordion.collapse();
assert!(accordion.is_collapsed());
accordion.expand();
assert!(!accordion.is_collapsed());
}
#[test]
fn test_custom_icons() {
let accordion = Accordion::new("Test", Text::new("Content", Style::null())).icons("+", "−");
assert_eq!(accordion.expand_icon, "+");
assert_eq!(accordion.collapse_icon, "−");
assert_eq!(accordion.current_icon(), "−");
let collapsed = Accordion::new("Test", Text::new("Content", Style::null()))
.icons("+", "−")
.collapsed(true);
assert_eq!(collapsed.current_icon(), "+");
}
#[test]
fn test_custom_icons_rendering() {
let console = make_console(80);
let expanded = Accordion::new("Test", Text::new("Content", Style::null())).icons("►", "▼");
let output = render_accordion(&console, &expanded);
assert!(output.contains("▼"));
let collapsed = Accordion::new("Test", Text::new("Content", Style::null()))
.icons("►", "▼")
.collapsed(true);
let output = render_accordion(&console, &collapsed);
assert!(output.contains("►"));
}
#[test]
fn test_indentation() {
let console = make_console(80);
let accordion =
Accordion::new("Title", Text::new("Line 1\nLine 2", Style::null())).indent(4);
let output = render_accordion(&console, &accordion);
let lines: Vec<&str> = output.lines().collect();
assert!(lines[1].starts_with(" Line 1"));
}
#[test]
fn test_accordion_group_new() {
let group = AccordionGroup::new(vec![
Accordion::new("A", Text::new("Content A", Style::null())),
Accordion::new("B", Text::new("Content B", Style::null())),
]);
assert_eq!(group.items.len(), 2);
assert!(group.allow_multiple_open);
}
#[test]
fn test_accordion_group_allow_multiple_open() {
let group = AccordionGroup::new(vec![]).allow_multiple_open(false);
assert!(!group.allow_multiple_open);
}
#[test]
fn test_accordion_group_expand_all() {
let mut group = AccordionGroup::new(vec![
Accordion::new("A", Text::new("...", Style::null())).collapsed(true),
Accordion::new("B", Text::new("...", Style::null())).collapsed(true),
]);
group.expand_all();
assert!(!group.items[0].is_collapsed());
assert!(!group.items[1].is_collapsed());
}
#[test]
fn test_accordion_group_collapse_all() {
let mut group = AccordionGroup::new(vec![
Accordion::new("A", Text::new("...", Style::null())),
Accordion::new("B", Text::new("...", Style::null())),
]);
group.collapse_all();
assert!(group.items[0].is_collapsed());
assert!(group.items[1].is_collapsed());
}
#[test]
fn test_accordion_group_push() {
let mut group = AccordionGroup::new(vec![]);
group.push(Accordion::new("New", Text::new("Content", Style::null())));
assert_eq!(group.items.len(), 1);
}
#[test]
fn test_accordion_group_expand_item_mutual_exclusion() {
let mut group = AccordionGroup::new(vec![
Accordion::new("A", Text::new("...", Style::null())),
Accordion::new("B", Text::new("...", Style::null())),
Accordion::new("C", Text::new("...", Style::null())),
])
.allow_multiple_open(false);
assert!(!group.items[0].is_collapsed());
assert!(!group.items[1].is_collapsed());
assert!(!group.items[2].is_collapsed());
group.expand_item(1);
assert!(group.items[0].is_collapsed());
assert!(!group.items[1].is_collapsed());
assert!(group.items[2].is_collapsed());
}
#[test]
fn test_accordion_group_render() {
let console = make_console(80);
let group = AccordionGroup::new(vec![
Accordion::new("First", Text::new("Content 1", Style::null())),
Accordion::new("Second", Text::new("Content 2", Style::null())),
]);
let opts = console.options();
let segments = group.gilt_console(&console, &opts);
let output = segments_to_text(&segments);
assert!(output.contains("First"));
assert!(output.contains("Second"));
assert!(output.contains("Content 1"));
assert!(output.contains("Content 2"));
}
#[test]
fn test_builder_chain() {
let accordion = Accordion::new("Title", Text::new("Content", Style::null()))
.collapsed(true)
.style(Style::parse("dim").unwrap())
.title_style(Style::parse("bold").unwrap())
.icon_style(Style::parse("cyan").unwrap())
.icons("+", "−")
.indent(4);
assert!(accordion.collapsed);
assert!(!accordion.style.is_null());
assert!(!accordion.title_style.is_null());
assert!(!accordion.icon_style.is_null());
assert_eq!(accordion.expand_icon, "+");
assert_eq!(accordion.collapse_icon, "−");
assert_eq!(accordion.indent, 4);
}
#[test]
fn test_accordion_display() {
let accordion = Accordion::new("Title", Text::new("Content", Style::null()));
let output = format!("{}", accordion);
assert!(output.contains("Title"));
assert!(output.contains("Content"));
}
#[test]
fn test_accordion_group_display() {
let group = AccordionGroup::new(vec![Accordion::new(
"A",
Text::new("Content A", Style::null()),
)]);
let output = format!("{}", group);
assert!(output.contains("A"));
assert!(output.contains("Content A"));
}
#[test]
fn test_empty_content() {
let console = make_console(80);
let accordion = Accordion::new("Title", Text::new("", Style::null()));
let output = render_accordion(&console, &accordion);
assert!(output.contains("Title"));
assert!(output.contains("▼"));
}
#[test]
fn test_empty_title() {
let console = make_console(80);
let accordion = Accordion::new("", Text::new("Content", Style::null()));
let output = render_accordion(&console, &accordion);
assert!(output.contains("▼"));
assert!(output.contains("Content"));
}
#[test]
fn test_long_content_wrapping() {
let console = make_console(40);
let long_text =
"This is a very long line that should wrap when rendered in a narrow console width.";
let accordion = Accordion::new("Title", Text::new(long_text, Style::null()));
let output = render_accordion(&console, &accordion);
let line_count = output.lines().count();
assert!(
line_count >= 2,
"Expected at least 2 lines, got {}",
line_count
);
}
#[test]
fn test_expand_item_out_of_bounds() {
let mut group =
AccordionGroup::new(vec![Accordion::new("A", Text::new("...", Style::null()))]);
group.expand_item(10);
assert!(!group.items[0].is_collapsed());
}
#[test]
fn test_accordion_group_empty() {
let console = make_console(80);
let group = AccordionGroup::new(vec![]);
let opts = console.options();
let segments = group.gilt_console(&console, &opts);
assert!(segments.is_empty());
}
}