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}