use densa_core::Rect;
use densa_draw::Canvas;
use densa_ui::{
AccessNode, Cursor, Event, EventCx, Handled, Length, PaintCx, PointerButton, PointerKind, Role,
Style, Widget,
};
const HANDLE: f32 = 6.0;
const MIN_PANE: f32 = 48.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Split {
Row,
Column,
}
#[derive(Debug)]
pub struct Splitter {
direction: Split,
grab: Option<f32>,
}
impl Splitter {
pub fn row() -> Self {
Self {
direction: Split::Row,
grab: None,
}
}
pub fn column() -> Self {
Self {
direction: Split::Column,
..Self::row()
}
}
pub fn style(&self) -> Style {
let base = match self.direction {
Split::Row => Style::row(),
Split::Column => Style::column(),
};
base.gap(HANDLE)
.grow(1.0)
.min_size(Length::Px(0.0), Length::Px(0.0))
}
fn cursor(&self) -> Cursor {
match self.direction {
Split::Row => Cursor::ResizeColumn,
Split::Column => Cursor::ResizeRow,
}
}
fn span(&self, rect: Rect) -> (f32, f32) {
match self.direction {
Split::Row => (rect.left(), rect.right()),
Split::Column => (rect.top(), rect.bottom()),
}
}
}
impl Widget for Splitter {
fn accessibility(&self, _: &PaintCx<'_>, node: &mut AccessNode) {
node.set_role(Role::Splitter);
}
fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
let Some(pane) = cx.children().first().and_then(|&first| cx.rect_of(first)) else {
return;
};
let content = cx.content_rect();
let line = cx.px(1.0).round().max(1.0);
let middle = (self.span(pane).1 + cx.px(HANDLE) / 2.0 - line / 2.0).round();
let divider = match self.direction {
Split::Row => Rect::new(middle, content.top(), line, content.height()),
Split::Column => Rect::new(content.left(), middle, content.width(), line),
};
let theme = cx.theme();
let color = if cx.is_pressed() || cx.is_hover_target() {
theme.accent
} else {
theme.border
};
canvas.fill_rect(divider, color);
}
fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
let Event::Pointer(pointer) = event else {
return Handled::No;
};
let Some(&first) = cx.children().first() else {
return Handled::No;
};
let Some(pane) = cx.rect_of(first) else {
return Handled::No;
};
let at = match self.direction {
Split::Row => pointer.position.x,
Split::Column => pointer.position.y,
};
match pointer.kind {
PointerKind::Enter => {
cx.set_cursor(Some(self.cursor()));
cx.request_paint();
Handled::Yes
}
PointerKind::Leave => {
cx.set_cursor(None);
cx.request_paint();
Handled::Yes
}
PointerKind::Down(PointerButton::Primary) => {
let position = pointer.position;
let on_pane = cx
.children()
.iter()
.any(|&child| cx.rect_of(child).is_some_and(|r| r.contains(position)));
if on_pane {
return Handled::No;
}
self.grab = Some(at - self.span(pane).1);
cx.request_paint();
Handled::Yes
}
PointerKind::Cancel => {
self.grab = None;
cx.request_paint();
Handled::Yes
}
PointerKind::Move => {
let Some(grab) = self.grab else {
return Handled::No;
};
let (start, end) = self.span(cx.content_rect());
let (pane_start, _) = self.span(pane);
let most = (end - start - cx.px(HANDLE) - cx.px(MIN_PANE)).max(cx.px(MIN_PANE));
let size = (at - grab - pane_start).clamp(cx.px(MIN_PANE), most);
let logical = Length::Px(size / cx.scale());
let style = cx.style_of(first).cloned().unwrap_or_default().shrink(0.0);
let style = match self.direction {
Split::Row => style.width(logical),
Split::Column => style.height(logical),
};
cx.set_style(first, style);
Handled::Yes
}
PointerKind::Up(PointerButton::Primary) => {
self.grab = None;
cx.request_paint();
Handled::Yes
}
_ => Handled::No,
}
}
}