use crate::geometry::{Rect, clamp_u16};
use crate::text;
use super::super::cells;
use super::super::tab_model::TabHit;
use super::{ADD, ARROW, Arrow, CLOSE, FILL_MIN, GAP, Overflow, PAD, TabWidth, Tabs, close_mark};
#[derive(Debug, Default)]
pub(super) struct Strip {
pub(super) tabs: Vec<(usize, Rect)>,
pub(super) arrows: Option<(Rect, Rect)>,
pub(super) menu: Option<Rect>,
pub(super) add: Option<Rect>,
}
impl Strip {
pub(super) fn lane(&self, area: Rect) -> Rect {
let (mut left, mut right) = (area.x, area.right());
if let Some((back, forward)) = self.arrows {
left = back.right() + i32::from(GAP);
right = forward.x - i32::from(GAP);
}
if let Some(menu) = self.menu {
right = menu.x - i32::from(GAP);
}
if let Some(add) = self.add {
right = right.min(add.x - i32::from(GAP));
}
Rect::new(left, area.y, clamp_u16(right - left), area.height)
}
pub(super) fn arrow_at(&self, x: i32, y: i32) -> Option<Arrow> {
let (back, forward) = self.arrows?;
if back.contains(x, y) {
Some(Arrow::Back)
} else if forward.contains(x, y) {
Some(Arrow::Forward)
} else {
None
}
}
}
impl<Msg: 'static> Tabs<Msg> {
fn number_width(&self, position: usize) -> u16 {
if self.numbered { cells::sum([text::width(&(position + 1).to_string()), 1]) } else { 0 }
}
pub(super) fn close_x(rect: Rect) -> i32 {
rect.right() - i32::from(PAD + close_mark::WIDTH - 1)
}
pub(super) fn badge_text(&self, index: usize) -> Option<String> {
self.badges.get(index).filter(|count| **count > 0).map(|count| super::super::badge::count_label(*count))
}
pub(super) fn badge_width(&self, index: usize) -> u16 {
self.badge_text(index).map_or(0, |text| text::width(&text).saturating_add(1))
}
pub(super) fn close_width(&self, index: usize) -> u16 {
if self.model.closable(index) { CLOSE } else { 0 }
}
fn fit_width(&self, index: usize) -> u16 {
cells::sum([
self.number_width(index),
text::width(&self.labels[index]),
self.badge_width(index),
PAD * 2,
self.close_width(index),
1,
])
}
fn min_width(&self, index: usize) -> u16 {
cells::sum([self.number_width(index), 1, self.badge_width(index), PAD * 2, self.close_width(index)])
}
fn narrowest_room(&self) -> u16 {
(0..self.labels.len()).map(|i| self.min_width(i)).max().unwrap_or(0)
}
pub(super) fn widths(&self, available: u16) -> Vec<u16> {
let count = self.labels.len();
match self.width {
TabWidth::Fit => (0..count).map(|i| self.fit_width(i)).collect(),
TabWidth::Fixed(cells) => (0..count).map(|i| cells.max(self.min_width(i))).collect(),
TabWidth::Fill => {
let count16 = u16::try_from(count).unwrap_or(u16::MAX).max(1);
let room = available.saturating_sub(self.add_room()).saturating_sub(count16.saturating_sub(1) * GAP);
let (share, extra) = (room / count16, room % count16);
(0..count)
.map(|i| {
let bonus = u16::from(u16::try_from(i).unwrap_or(u16::MAX) < extra);
let floor = self.fit_width(i).min(FILL_MIN).max(self.min_width(i));
(share + bonus).max(floor)
})
.collect()
}
}
}
pub(super) fn total_width(widths: &[u16]) -> u16 {
let count = u16::try_from(widths.len()).unwrap_or(u16::MAX);
widths.iter().fold(0u16, |sum, w| sum.saturating_add(*w)).saturating_add(count.saturating_sub(1) * GAP)
}
pub(super) fn add_room(&self) -> u16 {
match (&self.on_add, self.labels.is_empty()) {
(None, _) => 0,
(Some(_), true) => ADD,
(Some(_), false) => ADD + GAP,
}
}
pub(super) fn strip(&self, whole: Rect, order: &[usize], offset: usize, widths: &[u16]) -> Strip {
let area = Rect::new(whole.x, whole.y, whole.width.saturating_sub(self.add_room()), whole.height);
let overflows = Self::total_width(widths) > area.width;
let mut strip = Strip::default();
let (mut x, mut limit) = (area.x, area.right());
match self.overflow {
Overflow::Arrows if overflows && area.width >= 2 * (ARROW + GAP) + self.narrowest_room() => {
strip.arrows = Some((
Rect::new(area.x, area.y, ARROW, 1),
Rect::new(area.right() - i32::from(ARROW), area.y, ARROW, 1),
));
x += i32::from(ARROW + GAP);
limit -= i32::from(ARROW + GAP);
}
Overflow::Menu if overflows => {
let digits = text::width(&self.labels.len().to_string());
let width = 4 + digits;
let rect = Rect::new(area.right() - i32::from(width), area.y, width, 1);
strip.menu = Some(rect);
limit = rect.x - i32::from(GAP);
}
Overflow::Arrows | Overflow::Menu => {}
}
for &index in order.iter().skip(offset) {
let width = widths[index];
let room = limit - x;
if i32::from(width) > room {
if strip.tabs.is_empty() && room >= i32::from(self.min_width(index)) {
strip.tabs.push((index, Rect::new(x, area.y, clamp_u16(room), 1)));
}
break;
}
strip.tabs.push((index, Rect::new(x, area.y, width, 1)));
x += i32::from(width) + i32::from(GAP);
}
if self.on_add.is_some() && whole.width >= ADD {
let end = whole.right() - i32::from(ADD);
let cut = strip.tabs.last().is_some_and(|(index, rect)| rect.width < widths[*index]);
let x = if strip.tabs.len() < order.len() || cut {
end
} else {
strip.tabs.last().map_or(whole.x, |(_, rect)| (rect.right() + i32::from(GAP)).min(end))
};
strip.add = Some(Rect::new(x, whole.y, ADD, 1));
}
strip
}
fn shows_the_rest(&self, area: Rect, order: &[usize], offset: usize, widths: &[u16]) -> bool {
let strip = self.strip(area, order, offset, widths);
strip.tabs.len() == order.len().saturating_sub(offset)
&& strip.tabs.last().is_none_or(|(index, rect)| rect.width == widths[*index])
}
pub(super) fn settle(&self, area: Rect, order: &[usize], mut offset: usize, widths: &[u16]) -> usize {
while offset > 0 && self.shows_the_rest(area, order, offset - 1, widths) {
offset -= 1;
}
offset
}
pub(super) fn follow(&self, area: Rect, order: &[usize], offset: usize, target: usize, widths: &[u16]) -> usize {
let mut offset = offset.min(target);
while offset < target && !self.strip(area, order, offset, widths).tabs.iter().any(|(i, _)| order[target] == *i)
{
offset += 1;
}
offset
}
pub(super) fn drop_slots(&self, strip: &Strip) -> Vec<(usize, Rect)> {
let mut slots = strip.tabs.clone();
if let Some(add) = strip.add
&& let Some(last) = self.labels.len().checked_sub(1)
{
slots.push((last, add));
}
slots
}
pub(super) fn identity(&self) -> Vec<usize> {
(0..self.labels.len()).collect()
}
pub(super) fn hit(&self, strip: &Strip, x: i32, y: i32) -> Option<TabHit> {
let (index, rect) = strip.tabs.iter().find(|(_, rect)| rect.contains(x, y)).copied()?;
if self.model.closable(index) && close_mark::rect(Self::close_x(rect), rect.y).contains(x, y) {
return Some(TabHit::Close(index));
}
Some(TabHit::Tab(index, rect))
}
}