Skip to main content

sequoia_view/
dock_view.rs

1use std::cell::{Cell, RefCell};
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use sequoia_core::{Color, Constraints, Length, Point, Rect, Size};
6use sequoia_dock::{
7    DockDrop, DockModel, DockNode, DockOp, PanelId, PanelPayload, drop_from_fraction_or_float,
8    preview_rect,
9};
10use sequoia_input::{CursorIcon, Event, EventPhase, Modifiers, PointerButton, PointerEvent};
11use sequoia_layout::{Axis, LayoutCtx, LayoutStyle};
12use sequoia_reactive::Signal;
13use sequoia_style::VisualState;
14
15use crate::any::AnyView;
16use crate::dnd::{DragOptions, DropPosition};
17use crate::runtime::{ElementId, Runtime};
18use crate::view::{EventCtx, PaintCtx, View};
19use crate::views::{Text, Unit};
20
21pub type PanelRenderer = Rc<dyn Fn(PanelId) -> AnyView>;
22
23pub type TitleProvider = Rc<dyn Fn(PanelId) -> String>;
24
25pub(crate) fn default_title(panel: PanelId) -> String {
26    format!("{panel}")
27}
28
29const HANDLE_W: f32 = 7.0;
30const TAB_H: f32 = 28.0;
31const PREVIEW_FILL: Color = Color::hex(0x0D99FF);
32
33#[derive(Clone)]
34pub struct DockView {
35    model: Signal<DockModel>,
36    render: PanelRenderer,
37    title: TitleProvider,
38    edge: f32,
39    preview_frac: f32,
40    tabstrip_h: f32,
41    handle: f32,
42    empty_label: String,
43    layout: LayoutStyle,
44
45    split_drags: Rc<RefCell<HashMap<Vec<bool>, Rc<Cell<Option<SplitDrag>>>>>>,
46}
47
48impl DockView {
49    pub fn new(model: Signal<DockModel>, render: impl Fn(PanelId) -> AnyView + 'static) -> Self {
50        Self {
51            model,
52            render: Rc::new(render),
53            title: Rc::new(default_title),
54            edge: sequoia_dock::EDGE_FRACTION,
55            preview_frac: 0.5,
56            tabstrip_h: TAB_H,
57            handle: HANDLE_W,
58            empty_label: String::from("Drag panel here"),
59            layout: LayoutStyle {
60                width: Length::Fill(1.0),
61                height: Length::Fill(1.0),
62                ..LayoutStyle::default()
63            },
64            split_drags: Rc::new(RefCell::new(HashMap::new())),
65        }
66    }
67
68    pub fn title_provider(mut self, f: impl Fn(PanelId) -> String + 'static) -> Self {
69        self.title = Rc::new(f);
70        self
71    }
72
73    pub fn edge(mut self, edge: f32) -> Self {
74        self.edge = if edge.is_finite() {
75            edge.clamp(0.05, 0.45)
76        } else {
77            sequoia_dock::EDGE_FRACTION
78        };
79        self
80    }
81
82    pub fn tabstrip_height(mut self, h: f32) -> Self {
83        self.tabstrip_h = if h.is_finite() && h > 0.0 { h } else { TAB_H };
84        self
85    }
86
87    pub fn handle_width(mut self, w: f32) -> Self {
88        self.handle = if w.is_finite() { w.max(3.0) } else { HANDLE_W };
89        self
90    }
91
92    pub fn empty_label(mut self, label: impl Into<String>) -> Self {
93        self.empty_label = label.into();
94        self
95    }
96
97    pub fn width(mut self, width: impl Into<Length>) -> Self {
98        self.layout = self.layout.width(width.into());
99        self
100    }
101
102    pub fn height(mut self, height: impl Into<Length>) -> Self {
103        self.layout = self.layout.height(height.into());
104        self
105    }
106
107    pub fn fill_size(self) -> Self {
108        self.width(Length::Fill(1.0)).height(Length::Fill(1.0))
109    }
110
111    fn split_drag_for(&self, path: &[bool]) -> Rc<Cell<Option<SplitDrag>>> {
112        self.split_drags
113            .borrow_mut()
114            .entry(path.to_vec())
115            .or_insert_with(|| Rc::new(Cell::new(None)))
116            .clone()
117    }
118
119    fn prune_split_drags(&self, root: &DockNode) {
120        let mut live: Vec<Vec<bool>> = Vec::new();
121        collect_split_paths(root, Vec::new(), &mut live);
122        self.split_drags
123            .borrow_mut()
124            .retain(|path, _| live.contains(path));
125    }
126
127    fn slot(&self, panel: PanelId) -> DockSlot {
128        DockSlot {
129            model: self.model.clone(),
130            target: panel,
131            content: (self.render)(panel),
132            edge: self.edge,
133            preview_frac: self.preview_frac,
134        }
135    }
136
137    pub(crate) fn node_view(&self, node: &DockNode, path: Vec<bool>) -> AnyView {
138        match node {
139            DockNode::Leaf(p) => AnyView::new(self.slot(*p)),
140            DockNode::Tabs { tabs, active } => AnyView::new(DockTabs {
141                model: self.model.clone(),
142                render: self.render.clone(),
143                title: self.title.clone(),
144                tabs: tabs.clone(),
145                active: *active,
146                edge: self.edge,
147                preview_frac: self.preview_frac,
148                tabstrip_h: self.tabstrip_h,
149            }),
150            DockNode::Split {
151                axis,
152                frac,
153                first,
154                second,
155            } => {
156                let mut p_first = path.clone();
157                p_first.push(false);
158                let mut p_second = path.clone();
159                p_second.push(true);
160                let first = self.node_view(first, p_first);
161                let second = self.node_view(second, p_second);
162                let drag = self.split_drag_for(&path);
163                AnyView::new(DockSplit {
164                    model: self.model.clone(),
165                    axis: axis.axis(),
166                    frac: *frac,
167                    path,
168                    handle: self.handle,
169                    first,
170                    second,
171                    drag,
172                })
173            }
174
175            _ => AnyView::new(Unit),
176        }
177    }
178}
179
180impl View for DockView {
181    fn body(&self) -> impl View + 'static {
182        Unit
183    }
184
185    fn children_override(&self) -> Option<Vec<AnyView>> {
186        let m = self.model.get();
187        match &m.root {
188            Some(root) => {
189                self.prune_split_drags(root);
190                Some(vec![self.node_view(root, Vec::new())])
191            }
192            None => Some(vec![AnyView::new(Text::new(self.empty_label.clone()))]),
193        }
194    }
195
196    fn layout_style(&self) -> LayoutStyle {
197        self.layout
198    }
199}
200
201#[derive(Clone, Copy)]
202struct SplitDrag {
203    origin_main: f32,
204    frac: f32,
205}
206
207fn collect_split_paths(node: &DockNode, prefix: Vec<bool>, out: &mut Vec<Vec<bool>>) {
208    if let DockNode::Split { first, second, .. } = node {
209        out.push(prefix.clone());
210        let mut p_first = prefix.clone();
211        p_first.push(false);
212        collect_split_paths(first, p_first, out);
213        let mut p_second = prefix;
214        p_second.push(true);
215        collect_split_paths(second, p_second, out);
216    }
217}
218
219#[derive(Clone)]
220pub(crate) struct DockSplit {
221    model: Signal<DockModel>,
222    axis: Axis,
223    frac: f32,
224    path: Vec<bool>,
225    handle: f32,
226    first: AnyView,
227    second: AnyView,
228    drag: Rc<Cell<Option<SplitDrag>>>,
229}
230
231impl DockSplit {
232    fn main_pos(&self, p: Point) -> f32 {
233        match self.axis {
234            Axis::Horizontal => p.x,
235            Axis::Vertical => p.y,
236        }
237    }
238
239    fn handle_range(&self, content: Rect, frac: f32) -> (f32, f32) {
240        let total = match self.axis {
241            Axis::Horizontal => content.size.width,
242            Axis::Vertical => content.size.height,
243        };
244        let avail = (total - self.handle).max(0.0);
245        let off = avail * DockNode::sanitized_frac(frac);
246        let start = match self.axis {
247            Axis::Horizontal => content.left(),
248            Axis::Vertical => content.top(),
249        };
250        (start + off, start + off + self.handle)
251    }
252
253    fn live_frac(&self) -> f32 {
254        self.model
255            .get_untracked()
256            .frac_at(&self.path)
257            .unwrap_or(self.frac)
258    }
259}
260
261impl View for DockSplit {
262    fn body(&self) -> impl View + 'static {
263        Unit
264    }
265
266    fn children_override(&self) -> Option<Vec<AnyView>> {
267        let _ = self.model.get();
268        Some(vec![self.first.clone(), self.second.clone()])
269    }
270
271    fn layout_style(&self) -> LayoutStyle {
272        LayoutStyle {
273            width: Length::Fill(1.0),
274            height: Length::Fill(1.0),
275            ..LayoutStyle::default()
276        }
277    }
278
279    fn captures_pointer(&self) -> bool {
280        true
281    }
282
283    fn has_visual_state(&self) -> bool {
284        true
285    }
286
287    fn cursor_icon(&self, state: VisualState) -> CursorIcon {
288        if self.drag.get().is_some() || state.pressed {
289            match self.axis {
290                Axis::Horizontal => CursorIcon::ColResize,
291                Axis::Vertical => CursorIcon::RowResize,
292            }
293        } else {
294            CursorIcon::Default
295        }
296    }
297
298    fn arrange_override(
299        &self,
300        rt: &Runtime,
301        el: ElementId,
302        rect: Rect,
303        ctx: &mut LayoutCtx,
304    ) -> bool {
305        let kids = rt.children(el);
306        if kids.len() < 2 {
307            return false;
308        }
309        let frac = self.live_frac();
310        let avail_main = match self.axis {
311            Axis::Horizontal => rect.size.width,
312            Axis::Vertical => rect.size.height,
313        };
314        let avail = (avail_main - self.handle).max(0.0);
315        let a = avail * DockNode::sanitized_frac(frac);
316        let b = (avail - a).max(0.0);
317        let (ra, rb) = match self.axis {
318            Axis::Horizontal => (
319                Rect::from_xywh(rect.left(), rect.top(), a, rect.size.height),
320                Rect::from_xywh(
321                    rect.left() + a + self.handle,
322                    rect.top(),
323                    b,
324                    rect.size.height,
325                ),
326            ),
327            Axis::Vertical => (
328                Rect::from_xywh(rect.left(), rect.top(), rect.size.width, a),
329                Rect::from_xywh(
330                    rect.left(),
331                    rect.top() + a + self.handle,
332                    rect.size.width,
333                    b,
334                ),
335            ),
336        };
337        for (child, slot) in [(kids[0], ra), (kids[1], rb)] {
338            let max = Size::new(slot.size.width.max(0.0), slot.size.height.max(0.0));
339            let _ = rt.measure_node(
340                child,
341                Constraints {
342                    min: Size::ZERO,
343                    max,
344                },
345                ctx,
346            );
347            rt.arrange_node(child, slot, ctx);
348        }
349        true
350    }
351
352    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
353        if ctx.phase != EventPhase::Target {
354            return;
355        }
356        match event {
357            Event::Pointer(PointerEvent::Down {
358                button: PointerButton::Left,
359                pos,
360                ..
361            }) => {
362                let (lo, hi) = self.handle_range(ctx.rect, self.live_frac());
363                let p = self.main_pos(*pos);
364                if p >= lo && p <= hi {
365                    self.drag.set(Some(SplitDrag {
366                        origin_main: p,
367                        frac: self.live_frac(),
368                    }));
369                    ctx.capture_pointer();
370                    ctx.handled();
371                }
372            }
373            Event::Pointer(PointerEvent::Move { pos, .. }) => {
374                if let Some(st) = self.drag.get() {
375                    let total = match self.axis {
376                        Axis::Horizontal => ctx.rect.size.width,
377                        Axis::Vertical => ctx.rect.size.height,
378                    };
379                    let avail = (total - self.handle).max(0.0);
380                    if avail > 0.0 {
381                        let p = self.main_pos(*pos);
382                        let next = st.frac + (p - st.origin_main) / avail;
383                        let path = self.path.clone();
384                        self.model.update(|m| {
385                            m.set_frac_at(&path, next);
386                        });
387                    }
388                    ctx.handled();
389                }
390            }
391            Event::Pointer(PointerEvent::Up {
392                button: PointerButton::Left,
393                ..
394            }) => {
395                self.drag.set(None);
396            }
397            Event::Pointer(PointerEvent::Cancel) => {
398                self.drag.set(None);
399            }
400            _ => {}
401        }
402    }
403
404    fn paint_self(&self, ctx: &mut PaintCtx) {
405        let active = self.drag.get().is_some() || ctx.state.pressed;
406        let c = if active {
407            Color::rgb8(0x0D, 0x99, 0xFF)
408        } else {
409            Color::rgb8(0x75, 0x75, 0x75).with_alpha(0.55)
410        };
411        let (lo, _) = self.handle_range(ctx.rect, self.live_frac());
412        match self.axis {
413            Axis::Horizontal => {
414                let cx = lo + self.handle * 0.5;
415                ctx.quad(
416                    Rect::from_xywh(cx - 0.5, ctx.rect.top(), 1.0, ctx.rect.size.height),
417                    c,
418                    0.0,
419                );
420            }
421            Axis::Vertical => {
422                let cy = lo + self.handle * 0.5;
423                ctx.quad(
424                    Rect::from_xywh(ctx.rect.left(), cy - 0.5, ctx.rect.size.width, 1.0),
425                    c,
426                    0.0,
427                );
428            }
429        }
430    }
431}
432
433#[derive(Clone)]
434pub(crate) struct DockTabs {
435    model: Signal<DockModel>,
436    render: PanelRenderer,
437    title: TitleProvider,
438    tabs: Vec<PanelId>,
439    active: usize,
440    edge: f32,
441    preview_frac: f32,
442    tabstrip_h: f32,
443}
444
445impl View for DockTabs {
446    fn body(&self) -> impl View + 'static {
447        Unit
448    }
449
450    fn children_override(&self) -> Option<Vec<AnyView>> {
451        let m = self.model.get();
452
453        let (tabs, active) = match &m.root {
454            Some(root) => find_tabs(root, &self.tabs).unwrap_or((self.tabs.clone(), self.active)),
455            None => (self.tabs.clone(), self.active),
456        };
457        let mut strip = crate::views::Row::new();
458        for (i, t) in tabs.iter().copied().enumerate() {
459            let name = (self.title)(t);
460            strip = strip.child(DockTabButton {
461                model: self.model.clone(),
462                panel: t,
463                label: name,
464                active: i == active,
465            });
466        }
467        let active_panel = tabs.get(active).or(tabs.first()).copied();
468        let content = match active_panel {
469            Some(p) => AnyView::new(DockSlot {
470                model: self.model.clone(),
471                target: p,
472                content: (self.render)(p),
473                edge: self.edge,
474                preview_frac: self.preview_frac,
475            }),
476            None => AnyView::new(Unit),
477        };
478        Some(vec![AnyView::new(strip), content])
479    }
480
481    fn layout_style(&self) -> LayoutStyle {
482        LayoutStyle {
483            width: Length::Fill(1.0),
484            height: Length::Fill(1.0),
485            ..LayoutStyle::default()
486        }
487    }
488
489    fn arrange_override(
490        &self,
491        rt: &Runtime,
492        el: ElementId,
493        rect: Rect,
494        ctx: &mut LayoutCtx,
495    ) -> bool {
496        let kids = rt.children(el);
497        if kids.len() < 2 {
498            return false;
499        }
500        let strip_h = self.tabstrip_h.min(rect.size.height.max(0.0));
501        let strip_rect = Rect::from_xywh(rect.left(), rect.top(), rect.size.width, strip_h);
502        let body_rect = Rect::from_xywh(
503            rect.left(),
504            rect.top() + strip_h,
505            rect.size.width,
506            (rect.size.height - strip_h).max(0.0),
507        );
508        let _ = rt.measure_node(
509            kids[0],
510            Constraints {
511                min: Size::ZERO,
512                max: Size::new(strip_rect.size.width.max(0.0), strip_h),
513            },
514            ctx,
515        );
516        rt.arrange_node(kids[0], strip_rect, ctx);
517        let _ = rt.measure_node(
518            kids[1],
519            Constraints {
520                min: Size::ZERO,
521                max: Size::new(
522                    body_rect.size.width.max(0.0),
523                    body_rect.size.height.max(0.0),
524                ),
525            },
526            ctx,
527        );
528        rt.arrange_node(kids[1], body_rect, ctx);
529        true
530    }
531}
532
533fn find_tabs(root: &DockNode, tabs: &[PanelId]) -> Option<(Vec<PanelId>, usize)> {
534    match root {
535        DockNode::Leaf(_) => None,
536        DockNode::Tabs { tabs: t, active } => {
537            if (t.len() == tabs.len() && t.iter().all(|p| tabs.contains(p)))
538                || tabs.iter().any(|p| t.contains(p))
539            {
540                Some((t.clone(), *active))
541            } else {
542                None
543            }
544        }
545        DockNode::Split { first, second, .. } => {
546            find_tabs(first, tabs).or_else(|| find_tabs(second, tabs))
547        }
548
549        _ => None,
550    }
551}
552
553#[derive(Clone)]
554pub(crate) struct DockTabButton {
555    model: Signal<DockModel>,
556    panel: PanelId,
557    label: String,
558    active: bool,
559}
560
561impl View for DockTabButton {
562    fn body(&self) -> impl View + 'static {
563        Unit
564    }
565
566    fn children_override(&self) -> Option<Vec<AnyView>> {
567        Some(vec![AnyView::new(Text::new(self.label.clone()))])
568    }
569
570    fn layout_style(&self) -> LayoutStyle {
571        LayoutStyle {
572            width: Length::Fit,
573            height: Length::Fit,
574            ..LayoutStyle::default()
575        }
576    }
577
578    fn has_visual_state(&self) -> bool {
579        true
580    }
581
582    fn captures_pointer(&self) -> bool {
583        true
584    }
585
586    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
587        CursorIcon::Grab
588    }
589
590    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
591        match event {
592            Event::Pointer(PointerEvent::Down {
593                button: PointerButton::Left,
594                pos,
595                ..
596            }) if ctx.rect.contains(*pos) => {
597                ctx.begin_drag(PanelPayload(self.panel), DragOptions::new());
598            }
599            Event::Pointer(PointerEvent::Up {
600                button: PointerButton::Left,
601                pos,
602                ..
603            }) if ctx.rect.contains(*pos) => {
604                let panel = self.panel;
605                self.model.update(|m| {
606                    m.apply(DockOp::Activate { panel });
607                });
608            }
609            _ => {}
610        }
611    }
612
613    fn paint_self(&self, ctx: &mut PaintCtx) {
614        let bg = if self.active {
615            Color::rgb8(0x0D, 0x99, 0xFF).with_alpha(0.85)
616        } else {
617            Color::rgb8(0x3A, 0x3A, 0x3E).with_alpha(0.9)
618        };
619        ctx.paint_box(crate::BoxStyle::new().background(bg).radius(4.0));
620    }
621}
622
623#[derive(Clone)]
624pub struct DockSlot {
625    model: Signal<DockModel>,
626    target: PanelId,
627    content: AnyView,
628    edge: f32,
629    preview_frac: f32,
630}
631
632impl DockSlot {
633    pub fn target(&self) -> PanelId {
634        self.target
635    }
636
637    pub(crate) fn drop_for(&self, pos: DropPosition) -> DockDrop {
638        drop_from_fraction_or_float(
639            pos.fraction,
640            self.edge,
641            pos.modifiers.contains(Modifiers::ALT),
642        )
643    }
644}
645
646impl View for DockSlot {
647    fn body(&self) -> impl View + 'static {
648        Unit
649    }
650
651    fn children_override(&self) -> Option<Vec<AnyView>> {
652        Some(vec![self.content.clone()])
653    }
654
655    fn layout_style(&self) -> LayoutStyle {
656        LayoutStyle {
657            width: Length::Fill(1.0),
658            height: Length::Fill(1.0),
659            ..LayoutStyle::default()
660        }
661    }
662
663    fn has_visual_state(&self) -> bool {
664        true
665    }
666
667    fn accepts_drop(&self, payload: &crate::dnd::DragPayload) -> bool {
668        payload.is::<PanelPayload>()
669    }
670
671    fn on_drag_over(&self, _pos: DropPosition, _ctx: &mut EventCtx) {}
672
673    fn on_drop(&self, pos: DropPosition, ctx: &mut EventCtx) {
674        let Some(payload) = ctx
675            .drag_session()
676            .and_then(|s| s.payload.get::<PanelPayload>().copied())
677        else {
678            return;
679        };
680        let panel = payload.0;
681        if panel == self.target {
682            return;
683        }
684        let drop = self.drop_for(pos);
685        match drop {
686            DockDrop::Float => {
687                self.model.update(|m| {
688                    m.apply(DockOp::Float {
689                        panel,
690                        x: pos.global.x,
691                        y: pos.global.y,
692                        w: 320.0,
693                        h: 240.0,
694                    });
695                });
696            }
697            _ => {
698                let target = self.target;
699                let frac = self.preview_frac;
700                self.model.update(|m| {
701                    m.apply(DockOp::Insert {
702                        panel,
703                        target,
704                        drop,
705                        frac,
706                        moving: true,
707                    });
708                });
709            }
710        }
711        ctx.handled();
712    }
713
714    fn paint_self(&self, ctx: &mut PaintCtx) {
715        if !ctx.is_drag_target() {
716            return;
717        }
718        let Some(session) = ctx.drag_session() else {
719            return;
720        };
721        if !session.payload.is::<PanelPayload>() {
722            return;
723        }
724        let pos = DropPosition::in_rect(session.current, ctx.rect, session.modifiers);
725        let drop = drop_from_fraction_or_float(
726            pos.fraction,
727            self.edge,
728            pos.modifiers.contains(Modifiers::ALT),
729        );
730        let rect = preview_rect(ctx.rect, drop, self.preview_frac);
731        ctx.quad(rect, PREVIEW_FILL.with_alpha(0.35), 6.0);
732
733        ctx.paint_box_in(
734            crate::BoxStyle::new()
735                .border(1.5, PREVIEW_FILL.with_alpha(0.9))
736                .radius(6.0),
737            ctx.rect,
738        );
739    }
740}