1use std::cell::{Cell, RefCell};
5use std::collections::HashMap;
6use std::fmt;
7use std::rc::Rc;
8
9use ravenrs_core::{Border, Color, CursorIcon, Insets, Key, Modifiers, Point, Quad, Rect, Role, Size};
10use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
11use ravenrs_reactive::{Binding, State};
12use ravenrs_style::{Semantic, TextStyle};
13
14use super::geometry::{self, Arranged, BarKind, BarPlace, DropAim, GroupPlace, Metrics};
15use super::layout::{DockDrop, DockEdge, DockLayout, DockSide};
16use crate::commands::menu_trigger;
17use crate::context::{BuildCx, DragPhase, NodeId, Paint, QuadPaint, Semantics};
18use crate::lists::reorder;
19use crate::overlay::Entry;
20use crate::stack::ScrollEdgeArrow;
21use crate::util::{self, FocusTarget, Roving};
22use crate::view::{AnyView, Keyed, View};
23use crate::{
24 Button, ButtonSize, ButtonStyle, ContextMenu, IconButton, IconName, Labels, MenuItem, SplitHandle,
25};
26
27const TAB_HEIGHT: f32 = 32.0;
28const TITLE_MAX_WIDTH: f32 = 180.0;
29const TOGGLE: f32 = 12.0;
31const KEY_PANEL: u8 = 1;
33const KEY_STRIP: u8 = 2;
34const KEY_BAR: u8 = 3;
35const KEY_TOGGLE: u8 = 4;
36
37type OnEnd = Rc<dyn Fn(&DockLayout)>;
39
40#[derive(Clone, Debug, PartialEq)]
43#[non_exhaustive]
44#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
45pub struct DockPanel {
46 pub id: String,
48 pub title: String,
50 pub icon: Option<IconName>,
52 pub closable: bool,
54}
55
56#[cfg_attr(feature = "live", ravenrs_live::expose)]
57impl DockPanel {
58 pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
60 Self { id: id.into(), title: title.into(), icon: None, closable: true }
61 }
62
63 pub fn icon(mut self, icon: IconName) -> Self {
65 self.icon = Some(icon);
66 self
67 }
68
69 pub fn closable(mut self, closable: bool) -> Self {
71 self.closable = closable;
72 self
73 }
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
78#[non_exhaustive]
79#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
80pub enum DockTabBar {
81 #[default]
83 Always,
84 WhenSeveral,
86 Never,
88}
89
90#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
128pub struct DockArea {
129 layout: Binding<DockLayout>,
130 panels: Vec<DockPanel>,
131 content: Rc<dyn Fn(&str) -> AnyView>,
132 height: Option<f32>,
133 metrics: Metrics,
134 movable: bool,
135 tab_bar: DockTabBar,
136 on_end: Option<OnEnd>,
137 label: Option<String>,
138}
139
140impl fmt::Debug for DockArea {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 f.debug_struct("DockArea").field("panels", &self.panels).finish_non_exhaustive()
143 }
144}
145
146#[cfg_attr(feature = "live", ravenrs_live::expose)]
147impl DockArea {
148 pub fn new(
150 layout: Binding<DockLayout>,
151 panels: Vec<DockPanel>,
152 content: impl Fn(&str) -> AnyView + 'static,
153 ) -> Self {
154 Self {
155 layout,
156 panels,
157 content: Rc::new(content),
158 height: None,
159 metrics: Metrics {
160 bar: crate::extras::SPLIT_BAR,
161 toggle: TOGGLE,
162 left: (140.0, 640.0),
163 right: (140.0, 640.0),
164 bottom: (90.0, 560.0),
165 center_min: (200.0, 120.0),
166 pane_min: 100.0,
167 },
168 movable: true,
169 tab_bar: DockTabBar::Always,
170 on_end: None,
171 label: None,
172 }
173 }
174
175 pub fn height(mut self, height: f32) -> Self {
177 self.height = Some(height);
178 self
179 }
180
181 pub fn dock_limits(mut self, side: DockSide, min: f32, max: f32) -> Self {
183 let limits = (min, max.max(min));
184 match side {
185 DockSide::Left => self.metrics.left = limits,
186 DockSide::Right => self.metrics.right = limits,
187 DockSide::Bottom => self.metrics.bottom = limits,
188 _ => {}
189 }
190 self
191 }
192
193 pub fn center_min(mut self, width: f32, height: f32) -> Self {
195 self.metrics.center_min = (width, height);
196 self
197 }
198
199 pub fn pane_min(mut self, min: f32) -> Self {
201 self.metrics.pane_min = min;
202 self
203 }
204
205 pub fn movable(mut self, movable: bool) -> Self {
208 self.movable = movable;
209 self
210 }
211
212 pub fn tab_bar(mut self, tab_bar: DockTabBar) -> Self {
214 self.tab_bar = tab_bar;
215 self
216 }
217
218 pub fn edge_toggles(mut self, on: bool) -> Self {
221 self.metrics.toggle = if on { TOGGLE } else { 0.0 };
222 self
223 }
224
225 pub fn on_end(mut self, handler: impl Fn(&DockLayout) + 'static) -> Self {
228 self.on_end = Some(Rc::new(handler));
229 self
230 }
231
232 pub fn label(mut self, label: impl Into<String>) -> Self {
234 self.label = Some(label.into());
235 self
236 }
237
238 fn panel(&self, id: &str) -> Option<&DockPanel> {
239 self.panels.iter().find(|panel| panel.id == id)
240 }
241
242 fn title_of<'a>(&'a self, id: &'a str) -> &'a str {
243 self.panel(id).map_or(id, |panel| panel.title.as_str())
244 }
245
246 fn strip_height(&self, panels: usize) -> f32 {
247 strip_height(self.tab_bar, panels)
248 }
249}
250
251fn strip_height(tab_bar: DockTabBar, panels: usize) -> f32 {
252 match tab_bar {
253 DockTabBar::Always => TAB_HEIGHT,
254 DockTabBar::WhenSeveral if panels > 1 => TAB_HEIGHT,
255 _ => 0.0,
256 }
257}
258
259fn key_of(kind: u8, text: &str) -> u64 {
261 let mut hash = 0xcbf2_9ce4_8422_2325_u64 ^ u64::from(kind);
262 for byte in text.bytes() {
263 hash = (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3);
264 }
265 hash
266}
267
268fn placed(rect: Rect) -> LayoutStyle {
270 LayoutStyle {
271 absolute: Some((rect.x(), rect.y())),
272 width: Some(rect.width().max(0.0)),
273 height: Some(rect.height().max(0.0)),
274 min_width: Some(0.0),
275 min_height: Some(0.0),
276 align: Align::Stretch,
277 ..LayoutStyle::stack(Axis::Vertical)
278 }
279}
280
281#[derive(Debug, Default)]
284struct Frames {
285 root: Cell<Rect>,
286 tabs: RefCell<HashMap<String, Rect>>,
287}
288
289#[derive(Clone, Debug, PartialEq)]
291struct TabDrag {
292 panel: String,
293 pressed_at: Point,
294 moving: bool,
295 aim: Option<DropAim>,
296}
297
298#[derive(Clone)]
300struct Actions {
301 layout: Binding<DockLayout>,
302 on_end: Option<OnEnd>,
303 spoken: State<String>,
304 focus: Rc<RefCell<Option<String>>>,
306 labels: Rc<Labels>,
307}
308
309#[cfg_attr(feature = "live", ravenrs_live::expose)]
310impl Actions {
311 fn apply(&self, change: impl FnOnce(&mut DockLayout) -> bool) -> bool {
312 let mut next = self.layout.get();
313 if !change(&mut next) {
314 return false;
315 }
316 self.layout.set(next.clone());
317 if let Some(on_end) = &self.on_end {
318 on_end(&next);
319 }
320 true
321 }
322
323 fn settled(&self) {
324 if let Some(on_end) = &self.on_end {
325 on_end(&self.layout.get());
326 }
327 }
328
329 fn send(&self, id: &str, target: DockDrop) -> bool {
331 let moved = self.apply(|layout| layout.move_panel(id, target));
332 if moved {
333 *self.focus.borrow_mut() = Some(id.to_owned());
334 if let Some(side) = self.layout.get().side_of(id) {
335 self.spoken.set(self.labels.dock_moved_to(side));
336 }
337 }
338 moved
339 }
340
341 fn close(&self, id: &str, next: Option<String>, title: &str) {
342 if self.apply(|layout| layout.close(id)) {
343 *self.focus.borrow_mut() = next;
344 self.spoken.set(self.labels.dock_closed_name(title));
345 }
346 }
347
348 fn reopen(&self, id: &str) {
349 if self.apply(|layout| layout.reopen(id)) {
350 *self.focus.borrow_mut() = Some(id.to_owned());
351 }
352 }
353}
354
355fn panel_items(
357 area: &DockArea,
358 layout: &DockLayout,
359 arranged: &Arranged,
360 actions: &Actions,
361 id: &str,
362) -> Vec<MenuItem> {
363 let labels = &actions.labels;
364 let mut items = Vec::new();
365 if area.movable {
366 let own_group = arranged.groups.iter().find(|group| group.panels.iter().any(|panel| panel == id));
369 let mut places = Vec::new();
370 for side in DockSide::ALL_IN_MENU {
371 if layout.side_of(id) != Some(side) {
372 places.push((labels.dock_place(side).to_owned(), DockDrop::Side(side)));
373 }
374 }
375 for group in &arranged.groups {
376 if own_group.is_some_and(|own| std::ptr::eq(own, group)) {
377 continue;
378 }
379 let Some(first) = group.panels.first() else { continue };
380 let shown = group.panels.get(group.active).unwrap_or(first);
381 let target = DockDrop::Tab { beside: first.clone(), at: group.panels.len() };
382 places.push((labels.dock_with_name(area.title_of(shown)), target));
383 }
384 if own_group.is_some_and(|own| own.panels.len() > 1) {
385 let split = |edge| DockDrop::Split { beside: id.to_owned(), edge };
386 places.push((labels.dock_split_right.clone(), split(DockEdge::Right)));
387 places.push((labels.dock_split_below.clone(), split(DockEdge::Bottom)));
388 }
389 if !places.is_empty() {
390 let mut item = MenuItem::new(labels.dock_move_to.clone());
391 for (name, target) in places {
392 let (actions, id) = (actions.clone(), id.to_owned());
393 item = item.item(MenuItem::new(name).on_select(move || {
394 actions.send(&id, target.clone());
395 }));
396 }
397 items.push(item);
398 }
399 }
400 if area.panel(id).is_some_and(|panel| panel.closable) {
401 let (actions, id, title) = (actions.clone(), id.to_owned(), area.title_of(id).to_owned());
402 items.push(
403 MenuItem::new(labels.dock_close.clone()).on_select(move || actions.close(&id, None, &title)),
404 );
405 }
406 if let Some(reopen) = reopen_item(area, layout, actions) {
407 items.push(reopen);
408 }
409 items
410}
411
412fn reopen_item(area: &DockArea, layout: &DockLayout, actions: &Actions) -> Option<MenuItem> {
414 let closed = layout.closed_panels();
415 if closed.is_empty() {
416 return None;
417 }
418 let mut item = MenuItem::new(actions.labels.dock_reopen.clone());
419 for id in closed {
420 let (actions, owned) = (actions.clone(), id.to_owned());
421 item =
422 item.item(MenuItem::new(area.title_of(id).to_owned()).on_select(move || actions.reopen(&owned)));
423 }
424 Some(item)
425}
426
427struct DragContext {
429 actions: Actions,
430 state: State<Option<TabDrag>>,
431 frames: Rc<Frames>,
432 arranged: Rc<Arranged>,
433 tab_bar: DockTabBar,
434}
435
436#[cfg_attr(feature = "live", ravenrs_live::expose)]
437impl DragContext {
438 fn aim_at(&self, id: &str, position: Point) -> Option<DropAim> {
439 let root = self.frames.root.get();
440 let at = Point::new(position.x - root.x(), position.y - root.y());
441 let tabs = self.frames.tabs.borrow();
442 let tab_bar = self.tab_bar;
443 let strip = move |group: &GroupPlace| strip_height(tab_bar, group.panels.len());
444 let tab = |panel: &str| {
445 tabs.get(panel).map(|f| Rect::new(f.x() - root.x(), f.y() - root.y(), f.width(), f.height()))
446 };
447 let found = geometry::aim(&self.arranged, at, &strip, &tab)?;
448 self.actions.layout.get().move_panel(id, found.target.clone()).then_some(found)
450 }
451
452 fn handler(self, id: String) -> crate::context::DragHandler {
453 Rc::new(move |drag| match drag.phase {
454 DragPhase::Start => self.state.set(Some(TabDrag {
455 panel: id.clone(),
456 pressed_at: drag.position,
457 moving: false,
458 aim: None,
459 })),
460 DragPhase::Move => {
461 let Some(mut now) = self.state.get() else { return };
463 let (dx, dy) = (drag.position.x - now.pressed_at.x, drag.position.y - now.pressed_at.y);
464 now.moving |= dx.hypot(dy) > reorder::DRAG_THRESHOLD;
465 if now.moving {
466 now.aim = self.aim_at(&id, drag.position);
467 }
468 self.state.set_if_changed(Some(now));
469 }
470 DragPhase::End => {
471 if let Some(done) = self.state.get()
472 && let Some(aim) = done.aim
473 {
474 self.actions.send(&id, aim.target);
475 }
476 self.state.set(None);
477 }
478 })
479 }
480}
481
482struct TabView<'a> {
484 area: &'a DockArea,
485 panel: &'a DockPanel,
486 group: &'a GroupPlace,
487 index: usize,
488 on: bool,
489 actions: &'a Actions,
490 frames: &'a Rc<Frames>,
491 drag: &'a State<Option<TabDrag>>,
492 arranged: &'a Rc<Arranged>,
493 roving: Roving,
494}
495
496impl View for TabView<'_> {
497 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
498 let theme = *cx.theme();
499 let labels = cx.labels();
500 let (panel, on) = (self.panel, self.on);
501 let hovered = cx.is_hover_within();
502 let dragged = self.drag.get().is_some_and(|d| d.moving && d.panel == panel.id);
503 let ink = theme.color(if on { Semantic::Ink } else { Semantic::InkMuted });
504 let attrs = util::attrs(&theme, TextStyle::Callout, None);
505 let title_width = (cx.measure_text(&panel.title, &attrs).width.ceil() + 1.0).min(TITLE_MAX_WIDTH);
506 let gap = theme.spacing.s2;
507 let right_pad = if panel.closable { theme.spacing.s1 } else { theme.spacing.s3 };
508 let mut width = theme.spacing.s3 + right_pad + title_width;
509 let mut parts = Vec::with_capacity(3);
510 if let Some(icon) = panel.icon {
511 parts.push(util::icon(cx, icon, 14.0, ink));
512 width += 14.0 + gap;
513 }
514 let title = cx.text_truncated(
515 &panel.title,
516 &attrs,
517 ink,
518 LayoutStyle { width: Some(title_width), min_width: Some(0.0), ..LayoutStyle::leaf() },
519 );
520 cx.semantics(title, Semantics::hidden());
521 parts.push(title);
522 let (id, group_panels) = (panel.id.clone(), &self.group.panels);
523 let next = group_panels
525 .get(self.index + 1)
526 .or_else(|| self.index.checked_sub(1).and_then(|i| group_panels.get(i)))
527 .cloned();
528 if panel.closable {
529 let (actions, id, title) = (self.actions.clone(), id.clone(), panel.title.clone());
530 width += ButtonSize::Mini.height() + gap;
531 let button = IconButton::new(IconName::X)
532 .label(labels.dock_close_name(&panel.title))
533 .size(ButtonSize::Mini)
534 .on_click(move || actions.close(&id, None, &title));
535 parts.push(cx.child(&button));
536 }
537 let fill = if dragged {
538 theme.color(Semantic::Accent).with_alpha(0.16)
539 } else if on {
540 theme.color(Semantic::Surface)
541 } else if hovered {
542 theme.color(Semantic::Ink).with_alpha(0.05)
543 } else {
544 Color::TRANSPARENT
545 };
546 let radius = theme.radii.sm;
547 let (frames, recorded) = (self.frames.clone(), panel.id.clone());
548 let node = cx.container(
549 LayoutStyle {
550 height: Some(TAB_HEIGHT),
551 width: Some(width),
552 min_width: Some(width),
553 spacing: gap,
554 padding: Insets { top: 0.0, bottom: 0.0, left: theme.spacing.s3, right: right_pad },
555 align: Align::Center,
556 ..LayoutStyle::stack(Axis::Horizontal)
557 },
558 &parts,
559 Paint::Custom(Rc::new(move |frame, scene| {
560 frames.tabs.borrow_mut().insert(recorded.clone(), frame);
561 if fill.a > 0.0 {
562 scene.push_quad(Quad::new(frame, fill).radius(radius));
563 }
564 })),
565 );
566 let (actions, to_show) = (self.actions.clone(), id.clone());
567 cx.on_click(
568 node,
569 Rc::new(move || {
570 actions.apply(|layout| !layout.is_active(&to_show) && layout.activate(&to_show));
571 }),
572 );
573 cx.cursor(node, CursorIcon::Pointer);
574 cx.focusable(node, radius);
575 cx.semantics(node, Semantics::new(Role::Tab).name(panel.title.clone()).selected(on));
576 if self.actions.focus.borrow().as_deref() == Some(id.as_str()) {
577 self.actions.focus.borrow_mut().take();
578 cx.request_focus_node(node);
579 }
580 let movable = self.area.movable;
581 if movable {
582 let context = DragContext {
583 actions: self.actions.clone(),
584 state: self.drag.clone(),
585 frames: self.frames.clone(),
586 arranged: self.arranged.clone(),
587 tab_bar: self.area.tab_bar,
588 };
589 cx.on_drag(node, context.handler(id.clone()));
590 }
591 let (index, count) = (self.index, group_panels.len());
592 let first = group_panels.first().cloned().unwrap_or_default();
593 let (actions, called_off, closable) = (self.actions.clone(), self.drag.clone(), panel.closable);
594 let title = panel.title.clone();
595 let key_id = id.clone();
596 let before: crate::KeyHandler = Rc::new(move |key, modifiers| {
597 if *key == Key::Escape && called_off.peek().is_some() {
598 called_off.set(None);
599 return true;
600 }
601 if *key == Key::Delete && closable {
602 actions.close(&key_id, next.clone(), &title);
603 return true;
604 }
605 if !movable {
606 return false;
607 }
608 if let Some(side) = dock_key(key, modifiers) {
609 actions.send(&key_id, DockDrop::Side(side));
610 return true;
611 }
612 if let Some(to) = reorder::key_target(key, modifiers, index, count, false) {
613 let at = if to > index { to + 1 } else { to };
615 if actions.send(&key_id, DockDrop::Tab { beside: first.clone(), at }) {
616 actions.spoken.set(actions.labels.moved_to_position(to + 1, count));
617 }
618 }
619 reorder::is_move_key(key, modifiers)
620 });
621 let (actions, panels) = (self.actions.clone(), group_panels.clone());
622 let opens: Rc<dyn Fn(usize)> = Rc::new(move |to| {
623 if let Some(id) = panels.get(to) {
624 actions.apply(|layout| layout.activate(id));
625 }
626 });
627 self.roving.wire_with(cx, node, Some(opens), Some(before));
628 node
629 }
630}
631
632fn dock_key(key: &Key, modifiers: Modifiers) -> Option<DockSide> {
634 if !(modifiers.alt && modifiers.shift) {
635 return None;
636 }
637 match key {
638 Key::ArrowLeft => Some(DockSide::Left),
639 Key::ArrowRight => Some(DockSide::Right),
640 Key::ArrowDown => Some(DockSide::Bottom),
641 Key::ArrowUp => Some(DockSide::Center),
642 _ => None,
643 }
644}
645
646struct TabWithMenu<'a> {
648 strip: &'a Strip<'a>,
649 index: usize,
650 focus_to: FocusTarget,
651}
652
653impl View for TabWithMenu<'_> {
654 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
655 let strip = self.strip;
656 let Some(panel) = strip.group.panels.get(self.index).and_then(|id| strip.area.panel(id)) else {
657 return cx.empty();
658 };
659 let view = TabView {
660 area: strip.area,
661 panel,
662 group: strip.group,
663 index: self.index,
664 on: self.index == strip.group.active,
665 actions: strip.actions,
666 frames: strip.frames,
667 drag: strip.drag,
668 arranged: strip.arranged,
669 roving: Roving {
670 index: self.index,
671 count: strip.group.panels.len(),
672 focus_to: self.focus_to.clone(),
673 },
674 };
675 let mut menu = ContextMenu::new(view);
676 for item in panel_items(strip.area, strip.layout, strip.arranged, strip.actions, &panel.id) {
677 menu = menu.item(item);
678 }
679 menu.build(cx)
680 }
681}
682
683struct Strip<'a> {
685 area: &'a DockArea,
686 group: &'a GroupPlace,
687 layout: &'a DockLayout,
688 actions: &'a Actions,
689 frames: &'a Rc<Frames>,
690 drag: &'a State<Option<TabDrag>>,
691 arranged: &'a Rc<Arranged>,
692}
693
694impl View for Strip<'_> {
695 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
696 let theme = *cx.theme();
697 let labels = cx.labels();
698 let focus_to = util::roving_cell(cx, None);
699 let tabs: Vec<Keyed<TabWithMenu<'_>>> = self
700 .group
701 .panels
702 .iter()
703 .enumerate()
704 .map(|(index, id)| {
705 Keyed::new(
706 key_of(KEY_PANEL, id),
707 TabWithMenu { strip: self, index, focus_to: focus_to.clone() },
708 )
709 })
710 .collect();
711 let tabs =
712 cx.child(&ScrollEdgeArrow::new(tabs).spacing(2.0).fade(Semantic::Canvas).height(TAB_HEIGHT));
713 let tabs = cx.container(
714 LayoutStyle {
715 grow: 1.0,
716 min_width: Some(0.0),
717 align: Align::Stretch,
718 ..LayoutStyle::stack(Axis::Vertical)
719 },
720 &[tabs],
721 Paint::None,
722 );
723 cx.semantics(tabs, Semantics::new(Role::TabList).name(labels.dock_tabs_name.clone()));
724 let mut nodes = vec![tabs];
725 let shown = self.group.panels.get(self.group.active).map(String::as_str).unwrap_or_default();
726 let items = panel_items(self.area, self.layout, self.arranged, self.actions, shown);
727 if !items.is_empty() {
728 let entries: Vec<Entry> = items.into_iter().map(Entry::Item).collect();
729 nodes.push(
730 cx.child(&Options { entries, name: labels.dock_options_name(self.area.title_of(shown)) }),
731 );
732 }
733 cx.container(
734 LayoutStyle {
735 height: Some(TAB_HEIGHT),
736 min_width: Some(0.0),
737 align: Align::Center,
738 spacing: theme.spacing.s1,
739 padding: Insets { top: 0.0, bottom: 0.0, left: 0.0, right: theme.spacing.s1 },
740 align_self: Some(Align::Stretch),
741 ..LayoutStyle::stack(Axis::Horizontal)
742 },
743 &nodes,
744 Paint::Quad(QuadPaint::new(theme.color(Semantic::Canvas))),
745 )
746 }
747}
748
749struct Options {
751 entries: Vec<Entry>,
752 name: String,
753}
754
755impl View for Options {
756 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
757 menu_trigger(
758 cx,
759 &self.entries,
760 IconName::Ellipsis,
761 ButtonStyle::Ghost,
762 ButtonSize::Mini,
763 self.name.clone(),
764 )
765 }
766}
767
768struct EdgeToggle {
770 rect: Rect,
771 side: DockSide,
772 open: bool,
773 actions: Actions,
774}
775
776impl View for EdgeToggle {
777 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
778 let theme = *cx.theme();
779 let labels = cx.labels();
780 let lit = cx.is_hovered() || cx.is_focus_visible();
781 let icon = match (self.side, self.open) {
783 (DockSide::Left, true) | (DockSide::Right, false) => IconName::ChevronLeft,
784 (DockSide::Left, false) | (DockSide::Right, true) => IconName::ChevronRight,
785 (_, true) => IconName::ChevronDown,
786 (_, false) => IconName::ChevronUp,
787 };
788 let ink = theme.color(if lit { Semantic::Ink } else { Semantic::InkMuted });
789 let glyph = util::icon(cx, icon, 10.0, ink);
790 cx.semantics(glyph, Semantics::hidden());
791 let fill = if lit { theme.color(Semantic::Ink).with_alpha(0.06) } else { Color::TRANSPARENT };
792 let node = cx.container(
793 LayoutStyle { align: Align::Center, justify: Justify::Center, ..placed(self.rect) },
794 &[glyph],
795 Paint::Quad(QuadPaint::new(fill)),
796 );
797 let (actions, side, open) = (self.actions.clone(), self.side, self.open);
798 cx.on_click(
799 node,
800 Rc::new(move || {
801 actions.apply(|layout| {
802 layout.set_open(side, !open);
803 true
804 });
805 }),
806 );
807 cx.cursor(node, CursorIcon::Pointer);
808 cx.focusable(node, 2.0);
809 let name = if open { labels.dock_hide_name(self.side) } else { labels.dock_show_name(self.side) };
810 cx.semantics(node, Semantics::new(Role::Button).name(name).expanded(open));
811 node
812 }
813}
814
815struct EmptyCenter<'a> {
817 area: &'a DockArea,
818 layout: &'a DockLayout,
819 actions: &'a Actions,
820 rect: Rect,
821}
822
823impl View for EmptyCenter<'_> {
824 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
825 let theme = *cx.theme();
826 let labels = cx.labels();
827 let attrs = util::attrs(&theme, TextStyle::Callout, None);
828 let words = cx.text(&labels.dock_empty, &attrs, theme.color(Semantic::InkMuted), LayoutStyle::leaf());
829 let mut buttons = Vec::new();
830 for id in self.layout.closed_panels() {
831 let (actions, owned) = (self.actions.clone(), id.to_owned());
832 let button = Button::new(self.area.title_of(id).to_owned())
833 .style(ButtonStyle::Secondary)
834 .size(ButtonSize::Small)
835 .on_click(move || actions.reopen(&owned));
836 buttons.push(cx.keyed_child(key_of(KEY_PANEL, id), &button));
837 }
838 let row = cx.container(
839 LayoutStyle {
840 wrap: true,
841 spacing: theme.spacing.s2,
842 justify: Justify::Center,
843 align_self: Some(Align::Stretch),
844 ..LayoutStyle::stack(Axis::Horizontal)
845 },
846 &buttons,
847 Paint::None,
848 );
849 cx.container(
850 LayoutStyle {
851 align: Align::Center,
852 justify: Justify::Center,
853 spacing: theme.spacing.s3,
854 padding: Insets::uniform(theme.spacing.s5),
855 ..placed(self.rect)
856 },
857 &[words, row],
858 Paint::None,
859 )
860 }
861}
862
863impl View for DockArea {
864 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
865 let theme = *cx.theme();
866 let labels = cx.labels();
867 let drag: State<Option<TabDrag>> = cx.use_state(|| None);
868 let spoken: State<String> = cx.use_state(String::new);
869 let frames: Rc<Frames> = cx.use_state(|| Rc::new(Frames::default())).peek().clone();
870 let focus: Rc<RefCell<Option<String>>> = cx.use_state(|| Rc::new(RefCell::new(None))).peek().clone();
871 let known: Vec<&str> = self.panels.iter().map(|panel| panel.id.as_str()).collect();
872 let mut layout = self.layout.get();
873 if layout.reconcile(&known) {
875 self.layout.set(layout.clone());
876 }
877 let actions = Actions {
878 layout: self.layout.clone(),
879 on_end: self.on_end.clone(),
880 spoken: spoken.clone(),
881 focus,
882 labels: labels.clone(),
883 };
884 let size = cx.scroll_viewport().unwrap_or_else(|| {
886 let window = cx.viewport();
887 Size::new(window.width, self.height.unwrap_or(window.height))
888 });
889 let arranged = Rc::new(geometry::arrange(&layout, size, &self.metrics));
890 let mut nodes = Vec::with_capacity(arranged.groups.len() * 2 + arranged.bars.len() + 5);
891 for group in &arranged.groups {
892 nodes.push(self.build_group(cx, group, &layout, &actions, &frames, &drag, &arranged));
893 }
894 for bar in &arranged.bars {
895 nodes.push(self.build_bar(cx, bar, &actions));
896 }
897 for (side, rect) in &arranged.toggles {
898 let toggle = EdgeToggle {
899 rect: *rect,
900 side: *side,
901 open: layout.is_open(*side),
902 actions: actions.clone(),
903 };
904 nodes.push(cx.keyed_child(key_of(KEY_TOGGLE, labels.dock_place(*side)), &toggle));
905 }
906 if layout.node(DockSide::Center).is_none() {
907 let empty = EmptyCenter { area: self, layout: &layout, actions: &actions, rect: arranged.center };
908 nodes.push(cx.child(&empty));
909 }
910 if let Some(aim) = drag.get().and_then(|d| d.aim) {
911 nodes.push(mark(cx, aim.mark));
912 }
913 let announcement = spoken.get();
914 nodes.push(reorder::announcer(cx, announcement));
915 let recorded = frames.clone();
916 let node = cx.container(
917 LayoutStyle {
918 height: self.height,
919 grow: if self.height.is_none() { 1.0 } else { 0.0 },
920 min_width: Some(0.0),
921 min_height: Some(0.0),
922 align_self: Some(Align::Stretch),
923 overflow_hidden: true,
924 ..LayoutStyle::stack(Axis::Vertical)
925 },
926 &nodes,
927 Paint::Custom(Rc::new(move |frame, scene| {
928 recorded.root.set(frame);
929 scene.push_quad(Quad::new(frame, theme.color(Semantic::Canvas)));
930 })),
931 );
932 cx.report_frame(node);
933 cx.clip(node);
934 let name = self.label.clone().unwrap_or_else(|| labels.dock_area_name.clone());
935 cx.semantics(node, Semantics::new(Role::Group).name(name));
936 node
937 }
938}
939
940#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
941impl DockArea {
942 #[allow(clippy::too_many_arguments, reason = "the parts of one build, private to it")]
943 fn build_group(
944 &self,
945 cx: &mut BuildCx<'_>,
946 group: &GroupPlace,
947 layout: &DockLayout,
948 actions: &Actions,
949 frames: &Rc<Frames>,
950 drag: &State<Option<TabDrag>>,
951 arranged: &Rc<Arranged>,
952 ) -> NodeId {
953 let theme = *cx.theme();
954 let mut parts = Vec::with_capacity(2);
955 let first = group.panels.first().map(String::as_str).unwrap_or_default();
956 if self.strip_height(group.panels.len()) > 0.0 {
957 let strip = Strip { area: self, group, layout, actions, frames, drag, arranged };
958 parts.push(cx.keyed_child(key_of(KEY_STRIP, first), &strip));
959 }
960 let shown = group.panels.get(group.active).map(String::as_str).unwrap_or_default();
961 let content = (self.content)(shown);
963 let content = cx.keyed_child(key_of(KEY_PANEL, shown), content.as_ref());
964 let body = cx.container(
965 LayoutStyle {
966 grow: 1.0,
967 height: Some(0.0),
968 min_height: Some(0.0),
969 min_width: Some(0.0),
970 align: Align::Stretch,
971 overflow_hidden: true,
972 ..LayoutStyle::stack(Axis::Vertical)
973 },
974 &[content],
975 Paint::Quad(QuadPaint::new(theme.color(Semantic::Surface))),
976 );
977 cx.clip(body);
978 cx.semantics(body, Semantics::new(Role::TabPanel).name(self.title_of(shown).to_owned()));
979 parts.push(body);
980 let node = cx.container(placed(group.rect), &parts, Paint::None);
981 cx.clip(node);
982 node
983 }
984
985 fn build_bar(&self, cx: &mut BuildCx<'_>, bar: &BarPlace, actions: &Actions) -> NodeId {
986 let labels = cx.labels();
987 let (value, write) = (bar.value, self.layout.clone());
988 let (name, key, inverted) = match &bar.kind {
989 BarKind::Dock(side) => (
990 labels.dock_resize_name(*side),
991 format!("{side:?}"),
992 matches!(side, DockSide::Right | DockSide::Bottom),
993 ),
994 BarKind::Split { side, path, index, names, .. } => (
995 labels.dock_resize_between(self.title_of(&names.0), self.title_of(&names.1)),
996 format!("{side:?}{path:?}{index}"),
997 false,
998 ),
999 };
1000 let kind = bar.kind.clone();
1001 let binding = Binding::new(
1002 move || value,
1003 move |next: f32| {
1004 let mut layout = write.get();
1005 match &kind {
1006 BarKind::Dock(side) => layout.set_size(*side, next),
1007 BarKind::Split { side, path, index, both, .. } => {
1008 layout.resize_split(*side, path, *index, next, *both);
1009 }
1010 }
1011 write.set(layout);
1012 },
1013 );
1014 let mut handle = SplitHandle::new(bar.axis, binding).range(bar.min, bar.max).label(name);
1015 if inverted {
1016 handle = handle.inverted();
1017 }
1018 let actions = actions.clone();
1019 let handle = handle.on_end(move || actions.settled());
1020 let handle = cx.keyed_child(key_of(KEY_BAR, &key), &handle);
1021 let style = LayoutStyle { axis: Some(bar.axis), ..placed(bar.rect) };
1023 cx.container(style, &[handle], Paint::None)
1024 }
1025}
1026
1027fn mark(cx: &mut BuildCx<'_>, rect: Rect) -> NodeId {
1029 let theme = *cx.theme();
1030 let accent = theme.color(Semantic::Accent);
1031 let line = rect.width() <= 2.5;
1032 let paint = if line {
1033 QuadPaint { corner_radius: 1.0, ..QuadPaint::new(accent) }
1034 } else {
1035 QuadPaint {
1036 corner_radius: theme.radii.sm,
1037 border: Some(Border { width: 2.0, color: accent }),
1038 ..QuadPaint::new(accent.with_alpha(0.18))
1039 }
1040 };
1041 let node = cx.container(placed(rect), &[], Paint::Quad(paint));
1042 cx.semantics(node, Semantics::hidden());
1043 node
1044}