1mod access;
4mod geometry;
5mod input;
6mod layout;
7mod tree;
8
9use densa_core::{Color, Point, Rect, Size};
10use densa_draw::Canvas;
11use densa_ui::{
12 AccessNode, AccessRequest, Event, EventCx, Handled, LayoutCx, Length, Overflow, PaintCx,
13 PointerButton, PointerKind, Style, Ui, UiError, Widget, WidgetId,
14};
15
16pub use self::layout::{
17 DockAxis, DockLayout, DockLayoutError, DockLayoutErrorKind, MAX_LAYOUT_DEPTH,
18};
19
20use self::geometry::{bar, beside, content_style, divider_rect, group_rect, hairline};
21use crate::paint::{centered, outline};
22use crate::text::Text;
23
24const TITLE_INSET: f32 = 6.0;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum DockAt {
31 First,
33 With(WidgetId),
35 Beside(WidgetId, DockSide),
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum DockSide {
43 Left,
45 Right,
47 Top,
49 Bottom,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub struct DockPanel {
60 pub title: String,
62 pub key: Option<String>,
64}
65
66impl DockPanel {
67 pub fn new(title: impl Into<String>) -> Self {
69 Self {
70 title: title.into(),
71 key: None,
72 }
73 }
74
75 #[must_use]
78 pub fn with_key(mut self, key: impl Into<String>) -> Self {
79 self.key = Some(key.into());
80 self
81 }
82}
83
84impl From<&str> for DockPanel {
85 fn from(title: &str) -> Self {
86 Self::new(title)
87 }
88}
89
90impl From<String> for DockPanel {
91 fn from(title: String) -> Self {
92 Self::new(title)
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum Axis {
99 Row,
101 Column,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq)]
107struct Frac {
108 x0: f32,
109 y0: f32,
110 x1: f32,
111 y1: f32,
112}
113
114impl Frac {
115 const WHOLE: Self = Self {
116 x0: 0.0,
117 y0: 0.0,
118 x1: 1.0,
119 y1: 1.0,
120 };
121}
122
123#[derive(Debug, Clone)]
124enum Kind {
125 Split {
127 axis: Axis,
128 ratio: f32,
129 first: usize,
130 second: usize,
131 },
132 Group {
135 tabs: Vec<usize>,
136 active: usize,
137 scroll: f32,
138 serial: u32,
140 },
141}
142
143#[derive(Debug, Clone)]
144struct Node {
145 kind: Kind,
146 parent: Option<usize>,
147}
148
149#[derive(Debug)]
151struct Tab {
152 widget: WidgetId,
153 title: Text,
154 key: Option<String>,
155 group: usize,
156 serial: u32,
158}
159
160#[derive(Debug, Clone, Copy)]
162struct Divider {
163 split: usize,
164 axis: Axis,
165 ratio: f32,
166 area: Frac,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172enum Target {
173 Into(usize, Option<usize>),
176 Beside(usize, DockSide),
178}
179
180#[derive(Debug, Clone, Copy)]
181enum Drag {
182 Pressed { tab: usize, from: Point },
184 Tab { tab: usize, target: Option<Target> },
186 Divider {
188 split: usize,
189 grab: f32,
190 moved: bool,
191 },
192}
193
194#[derive(Debug, Clone, Copy)]
196enum Found {
197 Tab(usize, Rect),
198 Divider(usize),
199 Group(usize, Rect),
200 ScrollTabs(usize, bool),
202 Nothing,
203}
204
205#[derive(Debug, Default)]
249pub struct DockArea {
250 nodes: Vec<Option<Node>>,
252 root: Option<usize>,
253 tabs: Vec<Tab>,
254 frames: Vec<(usize, Frac)>,
256 dividers: Vec<Divider>,
257 stack: Vec<(usize, Frac)>,
259 drag: Option<Drag>,
260 hovered: Option<usize>,
262 styled_scale: f32,
264 focus_group: Option<usize>,
266 last_serial: u32,
268}
269
270impl DockArea {
271 pub fn new() -> Self {
273 Self::default()
274 }
275
276 pub fn style() -> Style {
279 Style::column()
280 .grow(1.0)
281 .min_size(Length::Px(0.0), Length::Px(0.0))
282 .overflow(Overflow::Clip)
283 }
284
285 pub fn add_panel(
293 ui: &mut Ui,
294 dock: WidgetId,
295 widget: impl Widget,
296 panel: impl Into<DockPanel>,
297 at: DockAt,
298 ) -> Result<WidgetId, UiError> {
299 let area = ui
300 .widget::<DockArea>(dock)
301 .ok_or(UiError::NoSuchWidget(dock))?;
302 if let DockAt::With(other) | DockAt::Beside(other, _) = at {
303 if area.index_of(other).is_none() {
304 return Err(UiError::NoSuchWidget(other));
305 }
306 }
307 let id = ui.add(dock, widget, Style::default().hidden(true))?;
308 let panel = panel.into();
309 ui.update::<DockArea, _>(dock, |area, cx| area.attach(cx, id, panel, at));
310 Ok(id)
311 }
312
313 pub fn remove_panel(ui: &mut Ui, dock: WidgetId, panel: WidgetId) -> Result<(), UiError> {
320 let removed = ui
321 .update::<DockArea, _>(dock, |area, cx| {
322 let index = area.index_of(panel)?;
323 area.drop_tab(cx, index);
324 Some(())
325 })
326 .ok_or(UiError::NoSuchWidget(dock))?;
327 removed.ok_or(UiError::NoSuchWidget(panel))?;
328 ui.remove(panel);
329 Ok(())
330 }
331
332 pub fn show_panel(ui: &mut Ui, dock: WidgetId, panel: WidgetId) -> Result<(), UiError> {
338 let shown = ui
339 .update::<DockArea, _>(dock, |area, cx| area.show(cx, panel))
340 .ok_or(UiError::NoSuchWidget(dock))?;
341 shown.then_some(()).ok_or(UiError::NoSuchWidget(panel))
342 }
343
344 pub fn panels(&self) -> impl ExactSizeIterator<Item = WidgetId> + '_ {
346 self.tabs.iter().map(|t| t.widget)
347 }
348
349 pub fn is_shown(&self, panel: WidgetId) -> bool {
351 self.index_of(panel).is_some_and(|i| self.is_active(i))
352 }
353
354 pub fn show(&mut self, cx: &mut EventCx<'_>, panel: WidgetId) -> bool {
357 let Some(index) = self.index_of(panel) else {
358 return false;
359 };
360 self.activate(index);
361 self.reveal(self.tabs[index].group, cx.rect(), cx.scale());
362 self.restyle(cx);
363 true
364 }
365
366 pub fn set_title(
369 &mut self,
370 cx: &mut EventCx<'_>,
371 panel: WidgetId,
372 title: impl Into<String>,
373 ) -> bool {
374 let Some(index) = self.index_of(panel) else {
375 return false;
376 };
377 self.tabs[index].title.set(title);
378 cx.request_layout();
380 cx.request_paint();
381 true
382 }
383
384 pub fn title(&self, panel: WidgetId) -> Option<&str> {
386 self.index_of(panel).map(|i| self.tabs[i].title.as_str())
387 }
388
389 pub fn key_of(&self, panel: WidgetId) -> Option<&str> {
391 self.index_of(panel)
392 .and_then(|i| self.tabs[i].key.as_deref())
393 }
394
395 pub fn panel_with_key(&self, key: &str) -> Option<WidgetId> {
397 self.tabs
398 .iter()
399 .find(|t| t.key.as_deref() == Some(key))
400 .map(|t| t.widget)
401 }
402
403 pub fn group_count(&self) -> usize {
405 self.frames.len()
406 }
407
408 pub fn tabs_with(&self, panel: WidgetId) -> impl Iterator<Item = WidgetId> + '_ {
410 let tabs = self
411 .index_of(panel)
412 .map_or(&[][..], |i| self.group_tabs(self.tabs[i].group).0);
413 tabs.iter().map(|&i| self.tabs[i].widget)
414 }
415
416 pub fn tab_rect(&self, panel: WidgetId, dock: Rect, scale: f32) -> Option<Rect> {
419 self.tab_rect_of(self.index_of(panel)?, dock, scale)
420 }
421
422 fn index_of(&self, panel: WidgetId) -> Option<usize> {
423 self.tabs.iter().position(|t| t.widget == panel)
424 }
425
426 fn attach(&mut self, cx: &mut EventCx<'_>, widget: WidgetId, panel: DockPanel, at: DockAt) {
427 let index = self.tabs.len();
428 let serial = self.next_serial();
429 self.tabs.push(Tab {
430 widget,
431 title: Text::new(panel.title),
432 key: panel.key,
433 group: usize::MAX,
434 serial,
435 });
436 let first = self.frames.first().map(|&(group, _)| group);
437 let other = match at {
438 DockAt::With(other) | DockAt::Beside(other, _) => self.index_of(other),
439 DockAt::First => None,
440 };
441 match (at, other) {
442 (DockAt::Beside(_, side), Some(other)) => {
443 let group = self.tabs[other].group;
444 self.split(index, group, side);
445 }
446 (DockAt::With(_), Some(other)) => {
447 let group = self.tabs[other].group;
448 self.join(index, Some(group), None);
449 }
450 _ => self.join(index, first, None),
451 }
452 self.arrange();
453 self.restyle(cx);
454 cx.request_layout();
456 }
457
458 fn restyle(&mut self, cx: &mut EventCx<'_>) {
461 let scale = cx.scale();
462 self.styled_scale = scale;
463 for (index, tab) in self.tabs.iter().enumerate() {
464 let frame = self.frames.iter().find(|(g, _)| *g == tab.group);
465 let style = match frame {
466 Some(&(_, frac)) if self.is_active(index) => content_style(frac, scale),
467 _ => Style::default().hidden(true),
468 };
469 if cx.style_of(tab.widget) != Some(&style) {
470 cx.set_style(tab.widget, style);
471 }
472 }
473 cx.request_paint();
474 }
475
476 fn paint_group(&self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>, group: usize, area: Rect) {
479 let (theme, scale) = (cx.theme(), cx.scale());
480 let line = hairline(scale);
481 let bar = bar(area, scale);
482 canvas.fill_rect(area, theme.surface);
483 let strip = Rect::from_edges(area.left(), area.top(), area.right(), bar.bottom());
484 canvas.fill_rect(strip, theme.background);
485 outline(canvas, area, line, theme.border);
486 let under = Rect::from_edges(area.left(), bar.bottom() - line, area.right(), bar.bottom());
487 canvas.fill_rect(under, theme.border);
488 let strip = self.strip(group, bar, scale);
489 if let Some((back, forward)) = strip.buttons {
490 let can = |yes: bool| if yes { theme.text } else { theme.text_dim };
491 chevron(canvas, back, line, false, can(strip.offset > 0.0));
492 chevron(
493 canvas,
494 forward,
495 line,
496 true,
497 can(strip.offset < strip.max_offset),
498 );
499 }
500 let saved = canvas.state();
501 canvas.clip(strip.tabs);
502 let (_, active) = self.group_tabs(group);
503 for (position, (index, tab)) in self.tab_rects(group, bar, scale).enumerate() {
504 if tab.is_empty() || cx.rect_of(self.tabs[index].widget).is_none() {
506 continue;
507 }
508 if position == active {
509 canvas.fill_rect(tab, theme.surface);
510 if cx.is_focused() && self.focused_group() == Some(group) {
511 outline(canvas, tab, 2.0 * line, theme.focus);
512 }
513 let accent =
514 Rect::from_edges(tab.left(), tab.top(), tab.right(), tab.top() + 2.0 * line);
515 canvas.fill_rect(accent, theme.accent);
516 } else if self.hovered == Some(index) {
517 let above = Rect::from_edges(tab.left(), tab.top(), tab.right(), under.top());
518 canvas.fill_rect(above, theme.hover);
519 }
520 let title = &self.tabs[index].title;
521 let size = title.size();
522 let saved = canvas.state();
523 canvas.clip(tab);
524 let mut at = centered(tab, size.width, size.height);
525 let inset = (TITLE_INSET * scale).round();
527 if size.width > tab.width() - 2.0 * inset {
528 at.x = tab.left() + inset;
529 }
530 title.paint(canvas, at);
531 canvas.restore(saved);
532 }
533 canvas.restore(saved);
534 }
535
536 fn paint_target(&self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>, tab: usize, target: Target) {
539 if !self.changes(tab, target) {
540 return;
541 }
542 let (rect, scale) = (cx.rect(), cx.scale());
543 let (Target::Into(group, _) | Target::Beside(group, _)) = target;
544 let Some(&(_, frac)) = self.frames.iter().find(|(g, _)| *g == group) else {
545 return;
546 };
547 let area = group_rect(frac, rect, scale);
548 let accent = cx.theme().accent;
549 let line = hairline(scale);
550 match target {
551 Target::Beside(_, side) => {
552 let shown = beside(frac, side, rect, scale);
553 canvas.fill_rect(shown, accent.with_alpha(0.22));
554 outline(canvas, shown, 2.0 * line, accent);
555 }
556 Target::Into(_, position) => {
557 if group != self.tabs[tab].group {
558 canvas.fill_rect(area, accent.with_alpha(0.12));
559 }
560 let bar = bar(area, scale);
561 let mut x = bar.right();
562 if let Some(position) = position {
563 let slot = self.tab_rects(group, bar, scale).nth(position);
564 if let Some((_, slot)) = slot {
565 x = slot.left();
566 } else if let Some((_, last)) = self.tab_rects(group, bar, scale).last() {
567 x = last.right();
568 }
569 } else if let Some((_, last)) = self.tab_rects(group, bar, scale).last() {
570 x = last.right();
571 }
572 let x = x.clamp(bar.left(), bar.right() - 2.0 * line);
573 let marker = Rect::from_edges(x, bar.top(), x + 2.0 * line, bar.bottom());
574 canvas.fill_rect(marker, accent);
575 }
576 }
577 }
578}
579
580impl Widget for DockArea {
581 fn accessibility(&self, cx: &PaintCx<'_>, node: &mut AccessNode) {
582 self.describe(cx, node);
583 }
584
585 fn access_action(&mut self, cx: &mut EventCx<'_>, request: &AccessRequest<'_>) -> Handled {
586 self.act(cx, request)
587 }
588
589 fn accepts_focus(&self) -> bool {
591 !self.tabs.is_empty()
592 }
593
594 fn focus_on_press(&self) -> bool {
595 false
596 }
597
598 fn layout(&mut self, cx: &mut LayoutCx<'_>, size: Size) {
599 let color = cx.theme().text;
600 for tab in &mut self.tabs {
601 tab.title.fit(cx, f32::INFINITY, color);
602 }
603 let rect = Rect::new(0.0, 0.0, size.width, size.height);
606 for i in 0..self.frames.len() {
607 let group = self.frames[i].0;
608 self.reveal(group, rect, cx.scale());
609 }
610 }
611
612 fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
613 let (rect, scale) = (cx.rect(), cx.scale());
614 canvas.fill_rect(rect, cx.theme().background);
615 for &(group, frac) in &self.frames {
616 self.paint_group(cx, canvas, group, group_rect(frac, rect, scale));
617 }
618 if let Some(Drag::Divider { split, .. }) = self.drag {
619 if let Some(divider) = self.dividers.iter().find(|d| d.split == split) {
620 let strip = divider_rect(divider, rect, scale);
621 canvas.fill_rect(strip, cx.theme().accent.with_alpha(0.6));
622 }
623 }
624 }
625
626 fn paint_over(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
627 if let Some(Drag::Tab {
628 tab,
629 target: Some(target),
630 }) = self.drag
631 {
632 self.paint_target(cx, canvas, tab, target);
633 }
634 }
635
636 fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
637 self.prune(cx);
638 if cx.scale() != self.styled_scale {
640 self.restyle(cx);
641 }
642 let pointer = match event {
643 Event::Pointer(pointer) => pointer,
644 Event::Key(key) => return self.key(cx, key),
645 Event::FocusGained | Event::FocusLost => {
646 cx.request_paint();
647 return Handled::No;
648 }
649 _ => return Handled::No,
650 };
651 match pointer.kind {
652 PointerKind::Down(PointerButton::Primary) => self.press(cx, pointer.position),
653 PointerKind::Move => self.drag_to(cx, pointer.position),
654 PointerKind::Up(button) => {
657 let handled = self.finish(cx, button == PointerButton::Primary);
658 if let Found::Divider(i) = self.find(pointer.position, cx.rect(), cx.scale()) {
660 cx.set_cursor(Some(input::resize_cursor(self.dividers[i].axis)));
661 }
662 handled
663 }
664 PointerKind::Cancel => self.finish(cx, false),
665 PointerKind::Scroll(delta) => self.wheel(cx, pointer.position, delta),
666 PointerKind::Leave => {
667 if self.drag.is_none() {
668 cx.set_cursor(None);
669 }
670 self.hover(cx, None);
671 Handled::No
672 }
673 _ => Handled::No,
674 }
675 }
676}
677
678fn chevron(canvas: &mut Canvas<'_>, button: Rect, line: f32, forward: bool, color: Color) {
681 let rows = 7_u8;
682 let reach = f32::from(rows / 2) * line;
683 let cx = ((button.left() + button.right()) / 2.0).round();
684 let top = ((button.top() + button.bottom()) / 2.0 - reach - line / 2.0).round();
685 for row in 0..rows {
686 let step = f32::from(row.abs_diff(rows / 2)) * line;
687 let x = if forward {
689 cx - reach / 2.0 + (reach - step)
690 } else {
691 cx - reach / 2.0 + step
692 };
693 let y = top + f32::from(row) * line;
694 canvas.fill_rect(Rect::from_edges(x, y, x + 2.0 * line, y + line), color);
695 }
696}