use densa_core::{Point, Rect};
use densa_ui::{Edges, Length, Overflow, Style};
use super::{Divider, DockArea, DockSide, Found, Frac, Target};
const TAB_BAR: f32 = 28.0;
const TAB_PADDING: f32 = 12.0;
const TAB_MIN: f32 = 48.0;
const SCROLL_BUTTON: f32 = 20.0;
pub(super) const GAP: f32 = 6.0;
const EDGE: f32 = 0.25;
const EPSILON: f32 = 1e-4;
pub(super) fn hairline(scale: f32) -> f32 {
scale.round().max(1.0)
}
fn bar_height(scale: f32) -> f32 {
(TAB_BAR * scale).round()
}
fn inner(on_edge: bool, half: f32) -> f32 {
if on_edge {
0.0
} else {
half
}
}
pub(super) fn group_rect(frac: Frac, rect: Rect, scale: f32) -> Rect {
let half = GAP * scale / 2.0;
let (w, h) = (rect.width(), rect.height());
Rect::from_edges(
(rect.left() + frac.x0 * w + inner(frac.x0 < EPSILON, half)).round(),
(rect.top() + frac.y0 * h + inner(frac.y0 < EPSILON, half)).round(),
(rect.left() + frac.x1 * w - inner(frac.x1 > 1.0 - EPSILON, half)).round(),
(rect.top() + frac.y1 * h - inner(frac.y1 > 1.0 - EPSILON, half)).round(),
)
}
pub(super) fn bar(area: Rect, scale: f32) -> Rect {
let line = hairline(scale);
let bottom = (area.top() + bar_height(scale)).min(area.bottom());
Rect::from_edges(area.left() + line, area.top(), area.right() - line, bottom)
}
pub(super) fn content_style(frac: Frac, scale: f32) -> Style {
let half = GAP / 2.0;
let inset = Edges {
left: Length::Fraction(frac.x0),
top: Length::Fraction(frac.y0),
right: Length::Fraction(1.0 - frac.x1),
bottom: Length::Fraction(1.0 - frac.y1),
};
let margin = Edges {
left: Length::Px(inner(frac.x0 < EPSILON, half)),
top: Length::Px(bar_height(scale) / scale + inner(frac.y0 < EPSILON, half)),
right: Length::Px(inner(frac.x1 > 1.0 - EPSILON, half)),
bottom: Length::Px(inner(frac.y1 > 1.0 - EPSILON, half)),
};
Style::column()
.absolute(inset)
.margin(margin)
.min_size(Length::Px(0.0), Length::Px(0.0))
.overflow(Overflow::Clip)
}
pub(super) fn divider_rect(divider: &Divider, rect: Rect, scale: f32) -> Rect {
let (area, ratio) = (divider.area, divider.ratio);
let half = GAP * scale / 2.0;
let (w, h) = (rect.width(), rect.height());
if matches!(divider.axis, super::Axis::Row) {
let x = rect.left() + (area.x0 + ratio * (area.x1 - area.x0)) * w;
Rect::from_edges(
x - half,
rect.top() + area.y0 * h,
x + half,
rect.top() + area.y1 * h,
)
} else {
let y = rect.top() + (area.y0 + ratio * (area.y1 - area.y0)) * h;
Rect::from_edges(
rect.left() + area.x0 * w,
y - half,
rect.left() + area.x1 * w,
y + half,
)
}
}
pub(super) fn beside(frac: Frac, side: DockSide, rect: Rect, scale: f32) -> Rect {
let mx = (frac.x0 + frac.x1) / 2.0;
let my = (frac.y0 + frac.y1) / 2.0;
let half = match side {
DockSide::Left => Frac { x1: mx, ..frac },
DockSide::Right => Frac { x0: mx, ..frac },
DockSide::Top => Frac { y1: my, ..frac },
DockSide::Bottom => Frac { y0: my, ..frac },
};
group_rect(half, rect, scale)
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Strip {
pub(super) tabs: Rect,
pub(super) buttons: Option<(Rect, Rect)>,
pub(super) offset: f32,
pub(super) max_offset: f32,
shrink: f32,
pad: f32,
min: f32,
}
impl Strip {
fn width(&self, dock: &DockArea, index: usize) -> f32 {
let natural = dock.natural_width(index, self.pad);
if self.shrink < 1.0 {
(natural * self.shrink).floor().max(self.min)
} else {
natural
}
}
}
impl DockArea {
pub(super) fn strip(&self, group: usize, bar: Rect, scale: f32) -> Strip {
let (members, _) = self.group_tabs(group);
let pad = TAB_PADDING * scale;
let min = (TAB_MIN * scale).round();
let natural: f32 = members.iter().map(|&i| self.natural_width(i, pad)).sum();
let room = bar.width().max(0.0);
let shrink = if natural > room { room / natural } else { 1.0 };
let mut strip = Strip {
tabs: bar,
buttons: None,
offset: 0.0,
max_offset: 0.0,
shrink,
pad,
min,
};
let total: f32 = members.iter().map(|&i| strip.width(self, i)).sum();
if total > room {
let button = (SCROLL_BUTTON * scale).round();
let tabs_right = (bar.right() - 2.0 * button).max(bar.left());
strip.tabs = Rect::from_edges(bar.left(), bar.top(), tabs_right, bar.bottom());
let back = Rect::from_edges(tabs_right, bar.top(), tabs_right + button, bar.bottom());
let forward = Rect::from_edges(back.right(), bar.top(), bar.right(), bar.bottom());
strip.buttons = Some((back, forward));
strip.max_offset = (total - strip.tabs.width()).max(0.0);
strip.offset = (self.group_scroll(group) * scale).clamp(0.0, strip.max_offset);
}
strip
}
fn natural_width(&self, index: usize, pad: f32) -> f32 {
(self.tabs[index].title.size().width + 2.0 * pad).round()
}
pub(super) fn tab_rects(
&self,
group: usize,
bar: Rect,
scale: f32,
) -> impl Iterator<Item = (usize, Rect)> + '_ {
let (members, _) = self.group_tabs(group);
let strip = self.strip(group, bar, scale);
let area = strip.tabs;
let mut x = area.left() - strip.offset;
members.iter().map(move |&i| {
let w = strip.width(self, i);
let left = x.clamp(area.left(), area.right());
x += w;
let right = x.clamp(area.left(), area.right());
(i, Rect::from_edges(left, area.top(), right, area.bottom()))
})
}
pub(super) fn strip_of(&self, group: usize, rect: Rect, scale: f32) -> Option<Strip> {
let &(_, frac) = self.frames.iter().find(|(g, _)| *g == group)?;
Some(self.strip(group, bar(group_rect(frac, rect, scale), scale), scale))
}
pub(super) fn reveal(&mut self, group: usize, rect: Rect, scale: f32) {
let (_, active) = self.group_tabs(group);
self.reveal_at(group, active, rect, scale);
}
pub(super) fn reveal_at(&mut self, group: usize, position: usize, rect: Rect, scale: f32) {
let Some(strip) = self.strip_of(group, rect, scale) else {
return;
};
if strip.buttons.is_none() {
return;
}
let (members, _) = self.group_tabs(group);
let position = position.min(members.len());
let start: f32 = members[..position]
.iter()
.map(|&i| strip.width(self, i))
.sum();
let end = start + members.get(position).map_or(0.0, |&i| strip.width(self, i));
let room = strip.tabs.width();
let offset = if start < strip.offset {
start
} else if end > strip.offset + room {
end - room
} else {
strip.offset
};
self.set_group_scroll(group, offset.clamp(0.0, strip.max_offset) / scale);
}
pub(super) fn scroll_tabs(&mut self, group: usize, by: f32, rect: Rect, scale: f32) -> bool {
let Some(strip) = self.strip_of(group, rect, scale) else {
return false;
};
let offset = (strip.offset + by).clamp(0.0, strip.max_offset);
if offset == strip.offset {
return false;
}
self.set_group_scroll(group, offset / scale);
true
}
pub(super) fn find(&self, at: Point, rect: Rect, scale: f32) -> Found {
if let Some(i) = self
.dividers
.iter()
.position(|d| divider_rect(d, rect, scale).contains(at))
{
return Found::Divider(i);
}
for &(group, frac) in &self.frames {
let area = group_rect(frac, rect, scale);
if !area.contains(at) {
continue;
}
let bar = bar(area, scale);
if let Some((back, forward)) = self.strip(group, bar, scale).buttons {
if back.contains(at) || forward.contains(at) {
return Found::ScrollTabs(group, forward.contains(at));
}
}
let tab = self
.tab_rects(group, bar, scale)
.find(|(_, tab)| tab.contains(at));
return match tab {
Some((index, tab)) => Found::Tab(index, tab),
None => Found::Group(group, area),
};
}
Found::Nothing
}
pub(super) fn target_at(&self, at: Point, rect: Rect, scale: f32) -> Option<Target> {
let (group, area) = match self.find(at, rect, scale) {
Found::Group(group, area) => (group, area),
Found::ScrollTabs(group, _) => {
let (tabs, _) = self.group_tabs(group);
return Some(Target::Into(group, Some(tabs.len())));
}
Found::Tab(index, tab) => {
let group = self.tabs[index].group;
let position = self.position_in(group, index);
let after = at.x >= (tab.left() + tab.right()) / 2.0;
let index = position + usize::from(after);
return Some(Target::Into(group, Some(index)));
}
Found::Divider(_) | Found::Nothing => return None,
};
if bar(area, scale).contains(at) {
let (tabs, _) = self.group_tabs(group);
return Some(Target::Into(group, Some(tabs.len())));
}
let fx = (at.x - area.left()) / area.width().max(1.0);
let fy = (at.y - area.top()) / area.height().max(1.0);
let nearest = [
(fx, DockSide::Left),
(1.0 - fx, DockSide::Right),
(fy, DockSide::Top),
(1.0 - fy, DockSide::Bottom),
]
.into_iter()
.min_by(|a, b| a.0.total_cmp(&b.0))
.filter(|(distance, _)| *distance < EDGE);
Some(match nearest {
Some((_, side)) => Target::Beside(group, side),
None => Target::Into(group, None),
})
}
pub(super) fn tab_rect_of(&self, index: usize, rect: Rect, scale: f32) -> Option<Rect> {
let group = self.tabs.get(index)?.group;
let &(_, frac) = self.frames.iter().find(|(g, _)| *g == group)?;
let area = group_rect(frac, rect, scale);
self.tab_rects(group, bar(area, scale), scale)
.find(|(i, _)| *i == index)
.map(|(_, tab)| tab)
.filter(|tab| !tab.is_empty())
}
}