use egui::accesskit::Role;
use egui::{Id, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, WidgetInfo, WidgetType};
use crate::layout::{EXTENT, usable};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TabFit {
pub shown: Vec<(usize, f32)>,
pub overflow: Vec<usize>,
pub overflow_width: f32,
}
const SQUEEZE_PASSES: usize = 32;
fn clean(v: f32) -> f32 {
if v.is_finite() { v.clamp(0.0, EXTENT) } else { 0.0 }
}
pub fn fit(natural: &[f32], selected: usize, available: f32, minimum_width: f32, overflow_width: f32) -> TabFit {
let widths: Vec<f32> = natural.iter().map(|&w| clean(w)).collect();
let available = clean(available);
let minimum = clean(minimum_width);
let Some(last) = widths.len().checked_sub(1) else { return TabFit::default() };
let total: f64 = widths.iter().map(|&w| f64::from(w)).sum();
if total <= f64::from(available) {
return TabFit { shown: widths.iter().copied().enumerate().collect(), overflow: Vec::new(), overflow_width: 0.0 };
}
if last == 0 {
return TabFit { shown: vec![(0, available)], overflow: Vec::new(), overflow_width: 0.0 };
}
let floor = |w: f32| w.min(minimum);
let floors: f64 = widths.iter().map(|&w| f64::from(floor(w))).sum();
if floors <= f64::from(available) {
let all: Vec<usize> = (0..widths.len()).collect();
return TabFit { shown: squeeze(&widths, &all, minimum, available), overflow: Vec::new(), overflow_width: 0.0 };
}
let overflow_width = clean(overflow_width).min(available);
let budget = f64::from(available - overflow_width).max(0.0);
let selected = selected.min(last);
let mut used = f64::from(floor(widths.get(selected).copied().unwrap_or(0.0)));
let keep: Vec<bool> = widths
.iter()
.enumerate()
.map(|(i, &w)| {
if i == selected {
return true;
}
let fits = used + f64::from(floor(w)) <= budget;
if fits {
used += f64::from(floor(w));
}
fits
})
.collect();
let (shown, overflow): (Vec<usize>, Vec<usize>) = (0..widths.len()).partition(|&i| keep.get(i).copied().unwrap_or(false));
TabFit { shown: squeeze(&widths, &shown, minimum, budget as f32), overflow, overflow_width }
}
fn squeeze(widths: &[f32], indices: &[usize], minimum: f32, total: f32) -> Vec<(usize, f32)> {
let width_at = |i: usize, cap: f64| {
let natural = f64::from(widths.get(i).copied().unwrap_or(0.0));
natural.min(cap.max(natural.min(f64::from(minimum))))
};
let sum_at = |cap: f64| indices.iter().map(|&i| width_at(i, cap)).sum::<f64>();
let total = f64::from(total);
let widest = indices.iter().filter_map(|&i| widths.get(i)).fold(0.0f32, |a, &b| a.max(b));
let (mut lo, mut hi) = (f64::from(minimum), f64::from(widest.max(minimum)));
let cap = if sum_at(hi) <= total {
hi
} else {
for _ in 0..SQUEEZE_PASSES {
let mid = (lo + hi) / 2.0;
if sum_at(mid) <= total {
lo = mid;
} else {
hi = mid;
}
}
lo
};
let mut remaining = total;
indices
.iter()
.map(|&i| {
let w = width_at(i, cap).min(remaining).max(0.0);
remaining = (remaining - w).max(0.0);
(i, w as f32)
})
.collect()
}
pub struct Tab<'a> {
id: Id,
label: &'a str,
selected: bool,
sense: Sense,
focus_stroke: Option<Stroke>,
}
impl<'a> Tab<'a> {
pub fn new(id: Id, label: &'a str, selected: bool) -> Self {
Self { id, label, selected, sense: Sense::click(), focus_stroke: None }
}
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
pub fn focus_stroke(mut self, stroke: Stroke) -> Self {
self.focus_stroke = Some(stroke);
self
}
pub fn show_at(self, ui: &mut Ui, rect: Rect, paint: impl FnOnce(&Ui, &Response)) -> Response {
if !usable(rect) {
let response = ui.interact(Rect::from_min_size(Pos2::ZERO, Vec2::ZERO), self.id, Sense::hover());
response.widget_info(|| WidgetInfo::selected(WidgetType::SelectableLabel, false, self.selected, self.label));
return response;
}
let response = ui.interact(rect, self.id, self.sense);
let enabled = ui.is_enabled();
response.widget_info(|| WidgetInfo::selected(WidgetType::SelectableLabel, enabled, self.selected, self.label));
ui.ctx().accesskit_node_builder(self.id, |node| {
node.set_role(Role::Tab);
node.clear_toggled();
node.set_selected(self.selected);
});
paint(ui, &response);
if response.has_focus() && rect.width() > 4.0 && rect.height() > 4.0 {
let stroke = self.focus_stroke.unwrap_or(ui.visuals().selection.stroke);
let inner = rect.shrink(2.0);
if stroke.width.is_finite() && stroke.width > 0.0 {
let width = stroke.width.min(inner.width().min(inner.height()) / 2.0);
ui.painter().rect_stroke(inner, 0.0, Stroke::new(width, stroke.color), StrokeKind::Inside);
}
}
response
}
}
#[cfg(test)]
mod tests;