mod access;
mod geometry;
mod input;
mod layout;
mod tree;
use densa_core::{Color, Point, Rect, Size};
use densa_draw::Canvas;
use densa_ui::{
AccessNode, AccessRequest, Event, EventCx, Handled, LayoutCx, Length, Overflow, PaintCx,
PointerButton, PointerKind, Style, Ui, UiError, Widget, WidgetId,
};
pub use self::layout::{
DockAxis, DockLayout, DockLayoutError, DockLayoutErrorKind, MAX_LAYOUT_DEPTH,
};
use self::geometry::{bar, beside, content_style, divider_rect, group_rect, hairline};
use crate::paint::{centered, outline};
use crate::text::Text;
const TITLE_INSET: f32 = 6.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DockAt {
First,
With(WidgetId),
Beside(WidgetId, DockSide),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DockSide {
Left,
Right,
Top,
Bottom,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DockPanel {
pub title: String,
pub key: Option<String>,
}
impl DockPanel {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
key: None,
}
}
#[must_use]
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
}
impl From<&str> for DockPanel {
fn from(title: &str) -> Self {
Self::new(title)
}
}
impl From<String> for DockPanel {
fn from(title: String) -> Self {
Self::new(title)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Axis {
Row,
Column,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Frac {
x0: f32,
y0: f32,
x1: f32,
y1: f32,
}
impl Frac {
const WHOLE: Self = Self {
x0: 0.0,
y0: 0.0,
x1: 1.0,
y1: 1.0,
};
}
#[derive(Debug, Clone)]
enum Kind {
Split {
axis: Axis,
ratio: f32,
first: usize,
second: usize,
},
Group {
tabs: Vec<usize>,
active: usize,
scroll: f32,
serial: u32,
},
}
#[derive(Debug, Clone)]
struct Node {
kind: Kind,
parent: Option<usize>,
}
#[derive(Debug)]
struct Tab {
widget: WidgetId,
title: Text,
key: Option<String>,
group: usize,
serial: u32,
}
#[derive(Debug, Clone, Copy)]
struct Divider {
split: usize,
axis: Axis,
ratio: f32,
area: Frac,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Target {
Into(usize, Option<usize>),
Beside(usize, DockSide),
}
#[derive(Debug, Clone, Copy)]
enum Drag {
Pressed { tab: usize, from: Point },
Tab { tab: usize, target: Option<Target> },
Divider {
split: usize,
grab: f32,
moved: bool,
},
}
#[derive(Debug, Clone, Copy)]
enum Found {
Tab(usize, Rect),
Divider(usize),
Group(usize, Rect),
ScrollTabs(usize, bool),
Nothing,
}
#[derive(Debug, Default)]
pub struct DockArea {
nodes: Vec<Option<Node>>,
root: Option<usize>,
tabs: Vec<Tab>,
frames: Vec<(usize, Frac)>,
dividers: Vec<Divider>,
stack: Vec<(usize, Frac)>,
drag: Option<Drag>,
hovered: Option<usize>,
styled_scale: f32,
focus_group: Option<usize>,
last_serial: u32,
}
impl DockArea {
pub fn new() -> Self {
Self::default()
}
pub fn style() -> Style {
Style::column()
.grow(1.0)
.min_size(Length::Px(0.0), Length::Px(0.0))
.overflow(Overflow::Clip)
}
pub fn add_panel(
ui: &mut Ui,
dock: WidgetId,
widget: impl Widget,
panel: impl Into<DockPanel>,
at: DockAt,
) -> Result<WidgetId, UiError> {
let area = ui
.widget::<DockArea>(dock)
.ok_or(UiError::NoSuchWidget(dock))?;
if let DockAt::With(other) | DockAt::Beside(other, _) = at {
if area.index_of(other).is_none() {
return Err(UiError::NoSuchWidget(other));
}
}
let id = ui.add(dock, widget, Style::default().hidden(true))?;
let panel = panel.into();
ui.update::<DockArea, _>(dock, |area, cx| area.attach(cx, id, panel, at));
Ok(id)
}
pub fn remove_panel(ui: &mut Ui, dock: WidgetId, panel: WidgetId) -> Result<(), UiError> {
let removed = ui
.update::<DockArea, _>(dock, |area, cx| {
let index = area.index_of(panel)?;
area.drop_tab(cx, index);
Some(())
})
.ok_or(UiError::NoSuchWidget(dock))?;
removed.ok_or(UiError::NoSuchWidget(panel))?;
ui.remove(panel);
Ok(())
}
pub fn show_panel(ui: &mut Ui, dock: WidgetId, panel: WidgetId) -> Result<(), UiError> {
let shown = ui
.update::<DockArea, _>(dock, |area, cx| area.show(cx, panel))
.ok_or(UiError::NoSuchWidget(dock))?;
shown.then_some(()).ok_or(UiError::NoSuchWidget(panel))
}
pub fn panels(&self) -> impl ExactSizeIterator<Item = WidgetId> + '_ {
self.tabs.iter().map(|t| t.widget)
}
pub fn is_shown(&self, panel: WidgetId) -> bool {
self.index_of(panel).is_some_and(|i| self.is_active(i))
}
pub fn show(&mut self, cx: &mut EventCx<'_>, panel: WidgetId) -> bool {
let Some(index) = self.index_of(panel) else {
return false;
};
self.activate(index);
self.reveal(self.tabs[index].group, cx.rect(), cx.scale());
self.restyle(cx);
true
}
pub fn set_title(
&mut self,
cx: &mut EventCx<'_>,
panel: WidgetId,
title: impl Into<String>,
) -> bool {
let Some(index) = self.index_of(panel) else {
return false;
};
self.tabs[index].title.set(title);
cx.request_layout();
cx.request_paint();
true
}
pub fn title(&self, panel: WidgetId) -> Option<&str> {
self.index_of(panel).map(|i| self.tabs[i].title.as_str())
}
pub fn key_of(&self, panel: WidgetId) -> Option<&str> {
self.index_of(panel)
.and_then(|i| self.tabs[i].key.as_deref())
}
pub fn panel_with_key(&self, key: &str) -> Option<WidgetId> {
self.tabs
.iter()
.find(|t| t.key.as_deref() == Some(key))
.map(|t| t.widget)
}
pub fn group_count(&self) -> usize {
self.frames.len()
}
pub fn tabs_with(&self, panel: WidgetId) -> impl Iterator<Item = WidgetId> + '_ {
let tabs = self
.index_of(panel)
.map_or(&[][..], |i| self.group_tabs(self.tabs[i].group).0);
tabs.iter().map(|&i| self.tabs[i].widget)
}
pub fn tab_rect(&self, panel: WidgetId, dock: Rect, scale: f32) -> Option<Rect> {
self.tab_rect_of(self.index_of(panel)?, dock, scale)
}
fn index_of(&self, panel: WidgetId) -> Option<usize> {
self.tabs.iter().position(|t| t.widget == panel)
}
fn attach(&mut self, cx: &mut EventCx<'_>, widget: WidgetId, panel: DockPanel, at: DockAt) {
let index = self.tabs.len();
let serial = self.next_serial();
self.tabs.push(Tab {
widget,
title: Text::new(panel.title),
key: panel.key,
group: usize::MAX,
serial,
});
let first = self.frames.first().map(|&(group, _)| group);
let other = match at {
DockAt::With(other) | DockAt::Beside(other, _) => self.index_of(other),
DockAt::First => None,
};
match (at, other) {
(DockAt::Beside(_, side), Some(other)) => {
let group = self.tabs[other].group;
self.split(index, group, side);
}
(DockAt::With(_), Some(other)) => {
let group = self.tabs[other].group;
self.join(index, Some(group), None);
}
_ => self.join(index, first, None),
}
self.arrange();
self.restyle(cx);
cx.request_layout();
}
fn restyle(&mut self, cx: &mut EventCx<'_>) {
let scale = cx.scale();
self.styled_scale = scale;
for (index, tab) in self.tabs.iter().enumerate() {
let frame = self.frames.iter().find(|(g, _)| *g == tab.group);
let style = match frame {
Some(&(_, frac)) if self.is_active(index) => content_style(frac, scale),
_ => Style::default().hidden(true),
};
if cx.style_of(tab.widget) != Some(&style) {
cx.set_style(tab.widget, style);
}
}
cx.request_paint();
}
fn paint_group(&self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>, group: usize, area: Rect) {
let (theme, scale) = (cx.theme(), cx.scale());
let line = hairline(scale);
let bar = bar(area, scale);
canvas.fill_rect(area, theme.surface);
let strip = Rect::from_edges(area.left(), area.top(), area.right(), bar.bottom());
canvas.fill_rect(strip, theme.background);
outline(canvas, area, line, theme.border);
let under = Rect::from_edges(area.left(), bar.bottom() - line, area.right(), bar.bottom());
canvas.fill_rect(under, theme.border);
let strip = self.strip(group, bar, scale);
if let Some((back, forward)) = strip.buttons {
let can = |yes: bool| if yes { theme.text } else { theme.text_dim };
chevron(canvas, back, line, false, can(strip.offset > 0.0));
chevron(
canvas,
forward,
line,
true,
can(strip.offset < strip.max_offset),
);
}
let saved = canvas.state();
canvas.clip(strip.tabs);
let (_, active) = self.group_tabs(group);
for (position, (index, tab)) in self.tab_rects(group, bar, scale).enumerate() {
if tab.is_empty() || cx.rect_of(self.tabs[index].widget).is_none() {
continue;
}
if position == active {
canvas.fill_rect(tab, theme.surface);
if cx.is_focused() && self.focused_group() == Some(group) {
outline(canvas, tab, 2.0 * line, theme.focus);
}
let accent =
Rect::from_edges(tab.left(), tab.top(), tab.right(), tab.top() + 2.0 * line);
canvas.fill_rect(accent, theme.accent);
} else if self.hovered == Some(index) {
let above = Rect::from_edges(tab.left(), tab.top(), tab.right(), under.top());
canvas.fill_rect(above, theme.hover);
}
let title = &self.tabs[index].title;
let size = title.size();
let saved = canvas.state();
canvas.clip(tab);
let mut at = centered(tab, size.width, size.height);
let inset = (TITLE_INSET * scale).round();
if size.width > tab.width() - 2.0 * inset {
at.x = tab.left() + inset;
}
title.paint(canvas, at);
canvas.restore(saved);
}
canvas.restore(saved);
}
fn paint_target(&self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>, tab: usize, target: Target) {
if !self.changes(tab, target) {
return;
}
let (rect, scale) = (cx.rect(), cx.scale());
let (Target::Into(group, _) | Target::Beside(group, _)) = target;
let Some(&(_, frac)) = self.frames.iter().find(|(g, _)| *g == group) else {
return;
};
let area = group_rect(frac, rect, scale);
let accent = cx.theme().accent;
let line = hairline(scale);
match target {
Target::Beside(_, side) => {
let shown = beside(frac, side, rect, scale);
canvas.fill_rect(shown, accent.with_alpha(0.22));
outline(canvas, shown, 2.0 * line, accent);
}
Target::Into(_, position) => {
if group != self.tabs[tab].group {
canvas.fill_rect(area, accent.with_alpha(0.12));
}
let bar = bar(area, scale);
let mut x = bar.right();
if let Some(position) = position {
let slot = self.tab_rects(group, bar, scale).nth(position);
if let Some((_, slot)) = slot {
x = slot.left();
} else if let Some((_, last)) = self.tab_rects(group, bar, scale).last() {
x = last.right();
}
} else if let Some((_, last)) = self.tab_rects(group, bar, scale).last() {
x = last.right();
}
let x = x.clamp(bar.left(), bar.right() - 2.0 * line);
let marker = Rect::from_edges(x, bar.top(), x + 2.0 * line, bar.bottom());
canvas.fill_rect(marker, accent);
}
}
}
}
impl Widget for DockArea {
fn accessibility(&self, cx: &PaintCx<'_>, node: &mut AccessNode) {
self.describe(cx, node);
}
fn access_action(&mut self, cx: &mut EventCx<'_>, request: &AccessRequest<'_>) -> Handled {
self.act(cx, request)
}
fn accepts_focus(&self) -> bool {
!self.tabs.is_empty()
}
fn focus_on_press(&self) -> bool {
false
}
fn layout(&mut self, cx: &mut LayoutCx<'_>, size: Size) {
let color = cx.theme().text;
for tab in &mut self.tabs {
tab.title.fit(cx, f32::INFINITY, color);
}
let rect = Rect::new(0.0, 0.0, size.width, size.height);
for i in 0..self.frames.len() {
let group = self.frames[i].0;
self.reveal(group, rect, cx.scale());
}
}
fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
let (rect, scale) = (cx.rect(), cx.scale());
canvas.fill_rect(rect, cx.theme().background);
for &(group, frac) in &self.frames {
self.paint_group(cx, canvas, group, group_rect(frac, rect, scale));
}
if let Some(Drag::Divider { split, .. }) = self.drag {
if let Some(divider) = self.dividers.iter().find(|d| d.split == split) {
let strip = divider_rect(divider, rect, scale);
canvas.fill_rect(strip, cx.theme().accent.with_alpha(0.6));
}
}
}
fn paint_over(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
if let Some(Drag::Tab {
tab,
target: Some(target),
}) = self.drag
{
self.paint_target(cx, canvas, tab, target);
}
}
fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
self.prune(cx);
if cx.scale() != self.styled_scale {
self.restyle(cx);
}
let pointer = match event {
Event::Pointer(pointer) => pointer,
Event::Key(key) => return self.key(cx, key),
Event::FocusGained | Event::FocusLost => {
cx.request_paint();
return Handled::No;
}
_ => return Handled::No,
};
match pointer.kind {
PointerKind::Down(PointerButton::Primary) => self.press(cx, pointer.position),
PointerKind::Move => self.drag_to(cx, pointer.position),
PointerKind::Up(button) => {
let handled = self.finish(cx, button == PointerButton::Primary);
if let Found::Divider(i) = self.find(pointer.position, cx.rect(), cx.scale()) {
cx.set_cursor(Some(input::resize_cursor(self.dividers[i].axis)));
}
handled
}
PointerKind::Cancel => self.finish(cx, false),
PointerKind::Scroll(delta) => self.wheel(cx, pointer.position, delta),
PointerKind::Leave => {
if self.drag.is_none() {
cx.set_cursor(None);
}
self.hover(cx, None);
Handled::No
}
_ => Handled::No,
}
}
}
fn chevron(canvas: &mut Canvas<'_>, button: Rect, line: f32, forward: bool, color: Color) {
let rows = 7_u8;
let reach = f32::from(rows / 2) * line;
let cx = ((button.left() + button.right()) / 2.0).round();
let top = ((button.top() + button.bottom()) / 2.0 - reach - line / 2.0).round();
for row in 0..rows {
let step = f32::from(row.abs_diff(rows / 2)) * line;
let x = if forward {
cx - reach / 2.0 + (reach - step)
} else {
cx - reach / 2.0 + step
};
let y = top + f32::from(row) * line;
canvas.fill_rect(Rect::from_edges(x, y, x + 2.0 * line, y + line), color);
}
}