use super::types::{BreadcrumbItem, SeparatorStyle};
use crate::render::Cell;
use crate::style::Color;
use crate::utils::Selection;
use crate::widget::theme::{DARK_GRAY, LIGHT_GRAY};
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
pub struct Breadcrumb {
items: Vec<BreadcrumbItem>,
selection: Selection,
separator: SeparatorStyle,
item_color: Color,
selected_color: Color,
separator_color: Color,
show_home: bool,
home_icon: char,
max_width: u16,
collapse: bool,
props: WidgetProps,
}
impl Breadcrumb {
pub fn new() -> Self {
Self {
items: Vec::new(),
selection: Selection::new(0),
separator: SeparatorStyle::Chevron,
item_color: LIGHT_GRAY,
selected_color: Color::CYAN,
separator_color: DARK_GRAY,
show_home: true,
home_icon: '🏠',
max_width: 0,
collapse: true,
props: WidgetProps::new(),
}
}
pub fn item(mut self, item: BreadcrumbItem) -> Self {
self.items.push(item);
self.selection.set_len(self.items.len());
self.selection.last(); self
}
pub fn push(mut self, label: impl Into<String>) -> Self {
self.items.push(BreadcrumbItem::new(label));
self.selection.set_len(self.items.len());
self.selection.last(); self
}
pub fn path(mut self, path: &str) -> Self {
self.items = path
.split('/')
.filter(|s| !s.is_empty())
.map(BreadcrumbItem::new)
.collect();
self.selection.set_len(self.items.len());
self.selection.last(); self
}
pub fn separator(mut self, style: SeparatorStyle) -> Self {
self.separator = style;
self
}
pub fn item_color(mut self, color: Color) -> Self {
self.item_color = color;
self
}
pub fn selected_color(mut self, color: Color) -> Self {
self.selected_color = color;
self
}
pub fn separator_color(mut self, color: Color) -> Self {
self.separator_color = color;
self
}
pub fn home(mut self, show: bool) -> Self {
self.show_home = show;
self
}
pub fn home_icon(mut self, icon: char) -> Self {
self.home_icon = icon;
self
}
pub fn max_width(mut self, width: u16) -> Self {
self.max_width = width;
self
}
pub fn collapse(mut self, collapse: bool) -> Self {
self.collapse = collapse;
self
}
pub fn select_next(&mut self) {
self.selection.down();
}
pub fn select_prev(&mut self) {
self.selection.up();
}
pub fn selected(&self) -> usize {
self.selection.index
}
pub fn set_selected(&mut self, index: usize) {
self.selection.set(index);
}
pub fn selected_item(&self) -> Option<&BreadcrumbItem> {
self.items.get(self.selection.index)
}
pub fn path_string(&self) -> String {
self.items
.iter()
.map(|i| i.label.as_str())
.collect::<Vec<_>>()
.join("/")
}
pub fn len(&self) -> usize {
self.items.len()
}
#[doc(hidden)]
pub fn items(&self) -> &[BreadcrumbItem] {
&self.items
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
use crate::event::Key;
match key {
Key::Left | Key::Char('h') => {
self.select_prev();
true
}
Key::Right | Key::Char('l') => {
self.select_next();
true
}
_ => false,
}
}
pub fn pop(&mut self) -> Option<BreadcrumbItem> {
let item = self.items.pop();
self.selection.set_len(self.items.len());
item
}
pub fn navigate_to(&mut self, index: usize) {
if index < self.items.len() {
self.items.truncate(index + 1);
self.selection.set_len(self.items.len());
self.selection.set(index);
}
}
pub(crate) fn total_width(&self) -> u16 {
let mut width = 0u16;
if self.show_home {
width += 2; }
for (i, item) in self.items.iter().enumerate() {
if item.icon.is_some() {
width += 2; }
width += crate::utils::display_width(&item.label) as u16;
if i < self.items.len() - 1 {
width += 3; }
}
width
}
}
impl Default for Breadcrumb {
fn default() -> Self {
Self::new()
}
}
impl View for Breadcrumb {
crate::impl_view_meta!("Breadcrumb");
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width < 3 || area.height < 1 {
return;
}
let max_width = if self.max_width > 0 {
self.max_width.min(area.width)
} else {
area.width
};
let total = self.total_width();
let need_collapse = self.collapse && total > max_width;
let mut x: u16 = 0;
if self.show_home {
let mut home = Cell::new(self.home_icon);
home.fg = Some(self.item_color);
ctx.set(x, 0, home);
x += 2;
if !self.items.is_empty() {
let mut sep = Cell::new(self.separator.char());
sep.fg = Some(self.separator_color);
ctx.set(x, 0, sep);
x += 2;
}
}
let (start_idx, show_ellipsis) = if need_collapse && self.items.len() > 2 {
(self.items.len().saturating_sub(2), true)
} else {
(0, false)
};
if show_ellipsis && !self.items.is_empty() {
let item = &self.items[0];
let is_selected = self.selection.is_selected(0);
if let Some(icon) = item.icon {
let mut cell = Cell::new(icon);
cell.fg = Some(if is_selected {
self.selected_color
} else {
self.item_color
});
ctx.set(x, 0, cell);
x += 2;
}
let clip_width = max_width.saturating_sub(10).saturating_sub(x);
if is_selected {
ctx.draw_text_clipped_bold(x, 0, &item.label, self.selected_color, clip_width);
} else {
ctx.draw_text_clipped(x, 0, &item.label, self.item_color, clip_width);
}
x += (crate::utils::display_width(&item.label) as u16).min(clip_width);
x += 1;
let mut sep = Cell::new(self.separator.char());
sep.fg = Some(self.separator_color);
ctx.set(x, 0, sep);
x += 2;
for ch in "...".chars() {
let mut cell = Cell::new(ch);
cell.fg = Some(self.separator_color);
ctx.set(x, 0, cell);
x += 1;
}
x += 1;
let mut sep = Cell::new(self.separator.char());
sep.fg = Some(self.separator_color);
ctx.set(x, 0, sep);
x += 2;
}
let items_to_show = if show_ellipsis {
&self.items[start_idx..]
} else {
&self.items[..]
};
for (i, item) in items_to_show.iter().enumerate() {
let actual_idx = if show_ellipsis { start_idx + i } else { i };
let is_selected = self.selection.is_selected(actual_idx);
let is_last = actual_idx == self.items.len() - 1;
if x >= max_width {
break;
}
if let Some(icon) = item.icon {
let mut cell = Cell::new(icon);
cell.fg = Some(if is_selected {
self.selected_color
} else {
self.item_color
});
ctx.set(x, 0, cell);
x += 2;
}
let clip_width = max_width.saturating_sub(x);
if is_selected {
ctx.draw_text_clipped_bold(x, 0, &item.label, self.selected_color, clip_width);
} else {
ctx.draw_text_clipped(x, 0, &item.label, self.item_color, clip_width);
}
x += (crate::utils::display_width(&item.label) as u16).min(clip_width);
if !is_last && x + 2 < max_width {
x += 1;
let mut sep = Cell::new(self.separator.char());
sep.fg = Some(self.separator_color);
ctx.set(x, 0, sep);
x += 2;
}
}
}
}
impl_styled_view!(Breadcrumb);
impl_props_builders!(Breadcrumb);