use std::cmp::min;
use cursive::{
Cursive, Printer, Vec2, View,
align::HAlign,
event::{Event, EventResult, MouseButton, MouseEvent},
theme::ColorStyle,
view::Nameable,
views::NamedView,
};
use unicode_width::UnicodeWidthStr;
use crate::{
command::{Command, MoveAmount, MoveMode},
commands::CommandResult,
traits::{BoxedViewExt, IntoBoxedViewExt, ViewExt},
};
#[derive(Default)]
pub struct TabbedView {
tabs: Vec<NamedView<BoxedViewExt>>,
selected: usize,
last_layout_size: Vec2,
}
impl TabbedView {
pub fn new() -> Self {
Default::default()
}
pub fn add_tab(&mut self, title: impl Into<String>, view: impl IntoBoxedViewExt) {
let tab = BoxedViewExt::new(view.into_boxed_view_ext()).with_name(title);
self.tabs.push(tab);
}
pub fn tab_mut(&mut self, index: usize) -> Option<&mut NamedView<BoxedViewExt>> {
self.tabs.get_mut(index)
}
pub fn selected_tab_mut(&mut self) -> Option<&mut NamedView<BoxedViewExt>> {
self.tab_mut(self.selected)
}
pub fn len(&self) -> usize {
self.tabs.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn set_selected(&mut self, index: usize) {
self.selected = min(self.len().saturating_sub(1), index);
}
pub fn move_selected(&mut self, amount: isize) {
self.selected = min(
self.selected.saturating_add_signed(amount),
self.len().saturating_sub(1),
);
}
pub fn move_left(&mut self) {
self.move_selected(-1);
}
pub fn move_right(&mut self) {
self.move_selected(1);
}
pub fn select_first(&mut self) {
self.selected = 0;
}
pub fn select_last(&mut self) {
self.selected = self.len() - 1;
}
pub fn on_first_tab(&mut self) -> bool {
self.selected == 0
}
pub fn on_last_tab(&mut self) -> bool {
self.selected == self.len() - 1
}
pub fn tab_width(&self) -> usize {
self.last_layout_size.x / self.len()
}
}
impl View for TabbedView {
fn draw(&self, printer: &Printer<'_, '_>) {
if self.is_empty() {
return;
}
let tabwidth = self.tab_width();
for (i, tab) in self.tabs.iter().enumerate() {
let style = if self.selected == i {
ColorStyle::highlight()
} else {
ColorStyle::primary()
};
let mut width = tabwidth;
if i == self.tabs.len() - 1 {
width += printer.size.x % self.tabs.len();
}
let title = tab.name();
let offset = HAlign::Center.get_offset(title.width(), width);
printer.with_color(style, |printer| {
printer.print_hline((i * tabwidth, 0), width, " ");
printer.print((i * tabwidth + offset, 0), title);
});
}
if let Some(tab) = self.tabs.get(self.selected) {
let printer = printer
.offset((0, 1))
.cropped((printer.size.x, printer.size.y - 1));
tab.draw(&printer);
}
}
fn layout(&mut self, size: Vec2) {
self.last_layout_size = size;
if let Some(tab) = self.tab_mut(self.selected) {
tab.layout((size.x, size.y - 1).into())
}
}
fn on_event(&mut self, event: Event) -> EventResult {
if let Event::Mouse {
offset,
position,
event,
} = event
{
let position = position.checked_sub(offset);
if let Some(0) = position.map(|p| p.y) {
match event {
MouseEvent::WheelUp => self.move_left(),
MouseEvent::WheelDown => self.move_right(),
MouseEvent::Press(MouseButton::Left) => {
let tabwidth = self.tab_width();
if let Some(selected_tab) = position.and_then(|p| p.x.checked_div(tabwidth))
{
self.set_selected(selected_tab);
}
}
_ => {}
};
return EventResult::consumed();
}
}
if let Some(tab) = self.tab_mut(self.selected) {
tab.on_event(event.relativized((0, 1)))
} else {
EventResult::Ignored
}
}
}
impl ViewExt for TabbedView {
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
match cmd {
Command::Move(mode, amount) if matches!(mode, MoveMode::Left | MoveMode::Right) => {
if matches!(mode, MoveMode::Left) && !self.on_first_tab() {
match amount {
MoveAmount::Extreme => self.select_first(),
MoveAmount::Integer(amount) => self.move_selected(-(*amount) as isize),
_ => (),
}
} else if matches!(mode, MoveMode::Right) && !self.on_last_tab() {
match amount {
MoveAmount::Extreme => self.select_last(),
MoveAmount::Integer(amount) => self.move_selected(*amount as isize),
_ => (),
}
}
Ok(CommandResult::Consumed(None))
}
_ => {
if let Some(tab) = self.selected_tab_mut() {
tab.on_command(s, cmd)
} else {
Ok(CommandResult::Ignored)
}
}
}
}
}