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