1use std::rc::Rc;
2
3use sequoia_core::{Color, Constraints, Length, Rect, Size};
4use sequoia_dock::{PanelId, PanelPayload};
5use sequoia_input::{CursorIcon, Event, PointerButton, PointerEvent};
6use sequoia_layout::{LayoutCtx, LayoutStyle};
7use sequoia_style::VisualState;
8
9use crate::any::AnyView;
10use crate::dnd::DragOptions;
11use crate::runtime::{ElementId, Runtime};
12use crate::view::{EventCtx, PaintCtx, View};
13use crate::views::{Text, Unit};
14
15const HEADER_H: f32 = 28.0;
16
17#[derive(Clone)]
18pub struct DockHeader {
19 panel: PanelId,
20 title: String,
21 height: f32,
22 background: Color,
23 on_drag_start: Option<Rc<dyn Fn()>>,
24}
25
26impl DockHeader {
27 pub fn new(panel: PanelId, title: impl Into<String>) -> Self {
28 Self {
29 panel,
30 title: title.into(),
31 height: HEADER_H,
32 background: Color::rgb8(0x2C, 0x2C, 0x30),
33 on_drag_start: None,
34 }
35 }
36
37 pub fn height(mut self, h: f32) -> Self {
38 self.height = if h.is_finite() && h > 0.0 {
39 h
40 } else {
41 HEADER_H
42 };
43 self
44 }
45
46 pub fn background(mut self, color: Color) -> Self {
47 self.background = color;
48 self
49 }
50
51 pub fn on_drag_start(mut self, f: impl Fn() + 'static) -> Self {
52 self.on_drag_start = Some(Rc::new(f));
53 self
54 }
55
56 pub fn panel(&self) -> PanelId {
57 self.panel
58 }
59}
60
61impl View for DockHeader {
62 fn body(&self) -> impl View + 'static {
63 Unit
64 }
65
66 fn children_override(&self) -> Option<Vec<AnyView>> {
67 Some(vec![AnyView::new(Text::new(self.title.clone()))])
68 }
69
70 fn layout_style(&self) -> LayoutStyle {
71 LayoutStyle {
72 width: Length::Fill(1.0),
73 height: Length::Px(self.height),
74 ..LayoutStyle::default()
75 }
76 }
77
78 fn has_visual_state(&self) -> bool {
79 true
80 }
81
82 fn captures_pointer(&self) -> bool {
83 true
84 }
85
86 fn cursor_icon(&self, state: VisualState) -> CursorIcon {
87 if state.pressed {
88 CursorIcon::Grabbing
89 } else {
90 CursorIcon::Grab
91 }
92 }
93
94 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
95 match event {
96 Event::Pointer(PointerEvent::Down {
97 button: PointerButton::Left,
98 pos,
99 ..
100 }) if ctx.rect.contains(*pos) => {
101 ctx.begin_drag(PanelPayload(self.panel), DragOptions::new());
102 if let Some(f) = &self.on_drag_start {
103 f();
104 }
105 }
106 _ => {}
107 }
108 }
109
110 fn paint_self(&self, ctx: &mut PaintCtx) {
111 ctx.paint_box(
112 crate::BoxStyle::new()
113 .background(self.background)
114 .radius(6.0),
115 );
116 }
117}
118
119#[derive(Clone)]
120pub struct Dockable {
121 panel: PanelId,
122 header: DockHeader,
123 content: AnyView,
124}
125
126impl Dockable {
127 pub fn new(panel: PanelId, title: impl Into<String>, content: impl View + 'static) -> Self {
128 let title = title.into();
129 Self {
130 panel,
131 header: DockHeader::new(panel, title),
132 content: AnyView::new(content),
133 }
134 }
135
136 pub fn header(mut self, header: DockHeader) -> Self {
137 debug_assert_eq!(header.panel(), self.panel, "other panel header");
138 self.header = header;
139 self
140 }
141
142 pub fn header_height(mut self, h: f32) -> Self {
143 self.header = self.header.height(h);
144 self
145 }
146
147 pub fn panel(&self) -> PanelId {
148 self.panel
149 }
150}
151
152impl View for Dockable {
153 fn body(&self) -> impl View + 'static {
154 Unit
155 }
156
157 fn children_override(&self) -> Option<Vec<AnyView>> {
158 Some(vec![
159 AnyView::new(self.header.clone()),
160 self.content.clone(),
161 ])
162 }
163
164 fn layout_style(&self) -> LayoutStyle {
165 LayoutStyle {
166 width: Length::Fill(1.0),
167 height: Length::Fill(1.0),
168 ..LayoutStyle::default()
169 }
170 }
171
172 fn arrange_override(
173 &self,
174 rt: &Runtime,
175 el: ElementId,
176 rect: Rect,
177 ctx: &mut LayoutCtx,
178 ) -> bool {
179 let kids = rt.children(el);
180 if kids.len() < 2 {
181 return false;
182 }
183 let hh = self.header.height.min(rect.size.height.max(0.0));
184 let head = Rect::from_xywh(rect.left(), rect.top(), rect.size.width, hh);
185 let body = Rect::from_xywh(
186 rect.left(),
187 rect.top() + hh,
188 rect.size.width,
189 (rect.size.height - hh).max(0.0),
190 );
191 let _ = rt.measure_node(
192 kids[0],
193 Constraints {
194 min: Size::ZERO,
195 max: Size::new(head.size.width.max(0.0), hh),
196 },
197 ctx,
198 );
199 rt.arrange_node(kids[0], head, ctx);
200 let _ = rt.measure_node(
201 kids[1],
202 Constraints {
203 min: Size::ZERO,
204 max: Size::new(body.size.width.max(0.0), body.size.height.max(0.0)),
205 },
206 ctx,
207 );
208 rt.arrange_node(kids[1], body, ctx);
209 true
210 }
211}
212
213const PANEL_CLOSE_W: f32 = 26.0;
214
215#[derive(Clone)]
216pub struct DockPanel {
217 panel: PanelId,
218 header: DockHeader,
219 content: AnyView,
220 background: Color,
221 radius: f32,
222 show_close: bool,
223 on_close: Option<Rc<dyn Fn()>>,
224}
225
226impl DockPanel {
227 pub fn new(panel: PanelId, title: impl Into<String>, content: impl View + 'static) -> Self {
228 Self {
229 panel,
230 header: DockHeader::new(panel, title),
231 content: AnyView::new(content),
232 background: Color::rgb8(0x1E, 0x1E, 0x22),
233 radius: 6.0,
234 show_close: false,
235 on_close: None,
236 }
237 }
238
239 pub fn header(mut self, header: DockHeader) -> Self {
240 debug_assert_eq!(header.panel(), self.panel, "other panel header");
241 self.header = header;
242 self
243 }
244
245 pub fn header_height(mut self, h: f32) -> Self {
246 self.header = self.header.height(h);
247 self
248 }
249
250 pub fn header_background(mut self, color: Color) -> Self {
251 self.header = self.header.background(color);
252 self
253 }
254
255 pub fn background(mut self, color: Color) -> Self {
256 self.background = color;
257 self
258 }
259
260 pub fn radius(mut self, radius: f32) -> Self {
261 self.radius = radius;
262 self
263 }
264
265 pub fn closable(mut self, closable: bool) -> Self {
266 self.show_close = closable;
267 self
268 }
269
270 pub fn on_close(mut self, f: impl Fn() + 'static) -> Self {
271 self.on_close = Some(Rc::new(f));
272 self.show_close = true;
273 self
274 }
275
276 pub fn panel(&self) -> PanelId {
277 self.panel
278 }
279}
280
281impl View for DockPanel {
282 fn body(&self) -> impl View + 'static {
283 Unit
284 }
285
286 fn children_override(&self) -> Option<Vec<AnyView>> {
287 if self.show_close {
288 Some(vec![
289 AnyView::new(self.header.clone()),
290 AnyView::new(DockPanelClose {
291 on_close: self.on_close.clone(),
292 }),
293 self.content.clone(),
294 ])
295 } else {
296 Some(vec![
297 AnyView::new(self.header.clone()),
298 self.content.clone(),
299 ])
300 }
301 }
302
303 fn layout_style(&self) -> LayoutStyle {
304 LayoutStyle {
305 width: Length::Fill(1.0),
306 height: Length::Fill(1.0),
307 ..LayoutStyle::default()
308 }
309 }
310
311 fn arrange_override(
312 &self,
313 rt: &Runtime,
314 el: ElementId,
315 rect: Rect,
316 ctx: &mut LayoutCtx,
317 ) -> bool {
318 let kids = rt.children(el);
319 let hh = self.header.height.min(rect.size.height.max(0.0));
320 let head = Rect::from_xywh(rect.left(), rect.top(), rect.size.width, hh);
321 let body = Rect::from_xywh(
322 rect.left(),
323 rect.top() + hh,
324 rect.size.width,
325 (rect.size.height - hh).max(0.0),
326 );
327 if self.show_close {
328 if kids.len() < 3 {
329 return false;
330 }
331 let close_w = PANEL_CLOSE_W.min(head.size.width.max(0.0));
332 let title_r = Rect::from_xywh(
333 head.left(),
334 head.top(),
335 (head.size.width - close_w).max(0.0),
336 head.size.height,
337 );
338 let close_r =
339 Rect::from_xywh(head.left() + title_r.size.width, head.top(), close_w, hh);
340 let slots = [(kids[0], title_r), (kids[1], close_r), (kids[2], body)];
341 for (child, slot) in slots {
342 let _ = rt.measure_node(
343 child,
344 Constraints {
345 min: Size::ZERO,
346 max: Size::new(slot.size.width.max(0.0), slot.size.height.max(0.0)),
347 },
348 ctx,
349 );
350 rt.arrange_node(child, slot, ctx);
351 }
352 true
353 } else {
354 if kids.len() < 2 {
355 return false;
356 }
357 let _ = rt.measure_node(
358 kids[0],
359 Constraints {
360 min: Size::ZERO,
361 max: Size::new(head.size.width.max(0.0), hh),
362 },
363 ctx,
364 );
365 rt.arrange_node(kids[0], head, ctx);
366 let _ = rt.measure_node(
367 kids[1],
368 Constraints {
369 min: Size::ZERO,
370 max: Size::new(body.size.width.max(0.0), body.size.height.max(0.0)),
371 },
372 ctx,
373 );
374 rt.arrange_node(kids[1], body, ctx);
375 true
376 }
377 }
378
379 fn paint_self(&self, ctx: &mut PaintCtx) {
380 ctx.paint_box(
381 crate::BoxStyle::new()
382 .background(self.background)
383 .radius(self.radius),
384 );
385 }
386}
387
388#[derive(Clone)]
389struct DockPanelClose {
390 on_close: Option<Rc<dyn Fn()>>,
391}
392
393impl View for DockPanelClose {
394 fn body(&self) -> impl View + 'static {
395 Unit
396 }
397
398 fn children_override(&self) -> Option<Vec<AnyView>> {
399 Some(vec![AnyView::new(Text::new("×"))])
400 }
401
402 fn layout_style(&self) -> LayoutStyle {
403 LayoutStyle {
404 width: Length::Px(PANEL_CLOSE_W),
405 height: Length::Fill(1.0),
406 ..LayoutStyle::default()
407 }
408 }
409
410 fn has_visual_state(&self) -> bool {
411 true
412 }
413
414 fn captures_pointer(&self) -> bool {
415 true
416 }
417
418 fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
419 CursorIcon::Pointer
420 }
421
422 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
423 match event {
424 Event::Pointer(PointerEvent::Up {
425 button: PointerButton::Left,
426 pos,
427 ..
428 }) if ctx.rect.contains(*pos) => {
429 if let Some(f) = &self.on_close {
430 f();
431 }
432 ctx.handled();
433 }
434 _ => {}
435 }
436 }
437}