Skip to main content

ravenrs_widgets/dock/
area.rs

1//! The drawing of a [`DockArea`]: everything sits where [`geometry`](super::geometry) says, under
2//! one node, so a panel keeps its place in the widget tree as it moves between groups.
3
4use 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;
29/// How thick the strip that collapses a dock is.
30const TOGGLE: f32 = 12.0;
31/// What keeps the keys of the different kinds of things under the area apart.
32const KEY_PANEL: u8 = 1;
33const KEY_STRIP: u8 = 2;
34const KEY_BAR: u8 = 3;
35const KEY_TOGGLE: u8 = 4;
36
37/// What runs when the arrangement settles.
38type OnEnd = Rc<dyn Fn(&DockLayout)>;
39
40/// A panel of a [`DockArea`]: what it is called and whether it can close. What it shows comes from
41/// the function given to the area, by its id.
42#[derive(Clone, Debug, PartialEq)]
43#[non_exhaustive]
44#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
45pub struct DockPanel {
46    /// What identifies it, in the arrangement and in what is saved. It should never change.
47    pub id: String,
48    /// What its tab says.
49    pub title: String,
50    /// An icon before its title.
51    pub icon: Option<IconName>,
52    /// It has a way to close (on by default).
53    pub closable: bool,
54}
55
56#[cfg_attr(feature = "live", ravenrs_live::expose)]
57impl DockPanel {
58    /// A panel with this id, whose tab says `title`.
59    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    /// An icon before its title.
64    pub fn icon(mut self, icon: IconName) -> Self {
65        self.icon = Some(icon);
66        self
67    }
68
69    /// Whether it has a way to close. The app can still close it through the arrangement.
70    pub fn closable(mut self, closable: bool) -> Self {
71        self.closable = closable;
72        self
73    }
74}
75
76/// When a group of a [`DockArea`] shows its tabs.
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
78#[non_exhaustive]
79#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
80pub enum DockTabBar {
81    /// Always, even for one panel.
82    #[default]
83    Always,
84    /// Only for more than one panel, leaving a panel alone with just its content.
85    WhenSeveral,
86    /// Never: the panels are columns with no title, which only change size and collapse.
87    Never,
88}
89
90/// Panels in a tree of splits and groups of tabs, at the left, the right, the bottom and the center
91/// of a space. Bars between them resize with the pointer or the keyboard, docks collapse from the
92/// strips on their edges, and panels close and come back.
93///
94/// A tab goes anywhere with a drag, with the target marked: to another group, to one side of a
95/// group, or to a border that has no dock showing. Without a pointer, the arrows walk between tabs,
96/// Alt with an arrow moves one within its group, Alt and Shift with an arrow send it to a dock, the
97/// menu of a tab or of the button at the end of the strip has "Move to…", and Delete closes.
98///
99/// The arrangement is a [`DockLayout`] in a [`Binding`], which holds ids alone and can be saved as
100/// text. What a panel shows is asked from the app, by id, for the panel at the front of each group:
101/// that is all a change of tab builds, and what a panel must remember while behind others belongs in
102/// the app. An id in the arrangement with no [`DockPanel`] is left out.
103///
104/// ```
105/// use ravenrs_reactive::State;
106/// use ravenrs_widgets::{AnyView, DockArea, DockLayout, DockNode, DockPanel, Text, Ui};
107///
108/// let saved = None::<String>; // What the app kept the last time.
109/// let start = DockLayout::new()
110///     .center(DockNode::tabs(["photo"]))
111///     .left(DockNode::tabs(["folders", "search"]), 240.0);
112/// let layout = State::new(saved.and_then(|text| DockLayout::parse(&text).ok()).unwrap_or(start));
113///
114/// let area = DockArea::new(
115///     layout.binding(),
116///     vec![
117///         DockPanel::new("photo", "Photo").closable(false),
118///         DockPanel::new("folders", "Folders"),
119///         DockPanel::new("search", "Search"),
120///     ],
121///     |id| -> AnyView { Box::new(Text::new(format!("Panel {id}"))) },
122/// )
123/// .on_end(|now| println!("{}", now.to_text()));
124/// let mut ui = Ui::new(area, ravenrs_style::Theme::raven_dark());
125/// ui.update();
126/// ```
127#[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    /// These panels in the arrangement of `layout`, each showing what `content` makes for its id.
149    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    /// Its height, in logical pixels. Without it, it takes the room its parent has.
176    pub fn height(mut self, height: f32) -> Self {
177        self.height = Some(height);
178        self
179    }
180
181    /// The smallest and the largest a dock gets, in logical pixels.
182    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    /// The width and the height the center keeps, which the docks do not take from it.
194    pub fn center_min(mut self, width: f32, height: f32) -> Self {
195        self.metrics.center_min = (width, height);
196        self
197    }
198
199    /// The smallest a group gets when a bar inside a split moves.
200    pub fn pane_min(mut self, min: f32) -> Self {
201        self.metrics.pane_min = min;
202        self
203    }
204
205    /// Whether panels change places (they do by default). Without it the tabs stay where the app
206    /// put them, and only the sizes, the docks and closing are left.
207    pub fn movable(mut self, movable: bool) -> Self {
208        self.movable = movable;
209        self
210    }
211
212    /// When a group shows its tabs ([`DockTabBar::Always`] by default).
213    pub fn tab_bar(mut self, tab_bar: DockTabBar) -> Self {
214        self.tab_bar = tab_bar;
215        self
216    }
217
218    /// Whether the strips that collapse the docks show (they do by default). Without them, the
219    /// app does it with [`DockLayout::set_open`].
220    pub fn edge_toggles(mut self, on: bool) -> Self {
221        self.metrics.toggle = if on { TOGGLE } else { 0.0 };
222        self
223    }
224
225    /// Runs when the arrangement settles: after a drop, a bar let go, a key, a tab, a dock or a
226    /// panel. It is the moment to save.
227    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    /// What screen readers call the area, instead of the name in [`Labels`].
233    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
259/// A key for something under the area that stays the same wherever it goes.
260fn 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
268/// Places a node exactly where the geometry says.
269fn 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/// Where things were last painted. What a drag aims at has to be compared with the pointer, and
282/// a tab inside a strip that scrolls is known only then.
283#[derive(Debug, Default)]
284struct Frames {
285    root: Cell<Rect>,
286    tabs: RefCell<HashMap<String, Rect>>,
287}
288
289/// A tab being dragged.
290#[derive(Clone, Debug, PartialEq)]
291struct TabDrag {
292    panel: String,
293    pressed_at: Point,
294    moving: bool,
295    aim: Option<DropAim>,
296}
297
298/// What the parts of the area change the arrangement through.
299#[derive(Clone)]
300struct Actions {
301    layout: Binding<DockLayout>,
302    on_end: Option<OnEnd>,
303    spoken: State<String>,
304    /// The panel whose tab takes focus when it is next built.
305    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    /// Sends a panel somewhere, keeping focus on its tab and saying where it went.
330    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
355/// The items of a panel's menu.
356fn 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        // These menus are made on every build, so what each place allows is read off the tree
367        // rather than tried on a copy of it.
368        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
412/// The submenu of what can come back, if anything can.
413fn 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
427/// What a tab's drag does: it aims as it goes, and moves on release.
428struct 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        // A drop that would leave things as they are marks nothing.
449        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                // Nothing here when it was called off with Escape.
462                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
482/// A tab.
483struct 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        // Whoever is beside it takes focus when it closes from the keyboard.
524        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                // A gap, as the layout counts them: past the tab itself it is one more.
614                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
632/// Which dock Alt and Shift with an arrow send a panel to.
633fn 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
646/// A tab with its menu.
647struct 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
683/// The tabs of a group, and the button with the menu of the one that shows.
684struct 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
749/// The button at the end of a strip, so that all a tab's menu has is reached from the keyboard.
750struct 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
768/// The strip that collapses a dock and brings it back.
769struct 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        // It points the way the dock will go: out to hide, in to show.
782        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
815/// What shows when the center holds nothing: the words, and a way back for each closed panel.
816struct 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        // From here on what was saved fits what exists, and it is saved that way next time.
874        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        // Known from the second build on; until then the window is the best guess there is.
885        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        // Under the area itself and by id: what a panel keeps goes with it to any group.
962        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        // The bar stretches across its stack, so the stack goes the way the bar's own axis does.
1022        let style = LayoutStyle { axis: Some(bar.axis), ..placed(bar.rect) };
1023        cx.container(style, &[handle], Paint::None)
1024    }
1025}
1026
1027/// The mark of where a drop lands: a line between tabs, or the room a panel would take.
1028fn 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}