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::{DockModel, DockOp, PanelId};
7use sequoia_input::{CursorIcon, Event, PointerButton, PointerEvent};
8use sequoia_layout::{LayoutCtx, LayoutStyle};
9use sequoia_reactive::Signal;
10use sequoia_style::VisualState;
11
12use crate::any::AnyView;
13use crate::dock_view::{PanelRenderer, TitleProvider, default_title};
14use crate::runtime::{ElementId, Runtime};
15use crate::view::{EventCtx, PaintCtx, View};
16use crate::views::{Text, Unit};
17
18const HEADER_H: f32 = 26.0;
19const BTN_W: f32 = 26.0;
20
21#[derive(Clone)]
22pub struct DockFloatLayer {
23 model: Signal<DockModel>,
24 render: PanelRenderer,
25 title: TitleProvider,
26 card_bg: Color,
27 header_bg: Color,
28 layout_width: Length,
29 layout_height: Length,
30
31 drags: Rc<RefCell<HashMap<u64, Rc<Cell<Option<CardDrag>>>>>>,
32}
33
34impl DockFloatLayer {
35 pub fn new(model: Signal<DockModel>, render: impl Fn(PanelId) -> AnyView + 'static) -> Self {
36 Self {
37 model,
38 render: Rc::new(render),
39 title: Rc::new(default_title),
40 card_bg: Color::rgb8(0x1E, 0x1E, 0x22),
41 header_bg: Color::rgb8(0x2C, 0x2C, 0x30),
42 layout_width: Length::Fill(1.0),
43 layout_height: Length::Fill(1.0),
44 drags: Rc::new(RefCell::new(HashMap::new())),
45 }
46 }
47
48 fn drag_for(&self, panel: PanelId) -> Rc<Cell<Option<CardDrag>>> {
49 self.drags
50 .borrow_mut()
51 .entry(panel.get())
52 .or_insert_with(|| Rc::new(Cell::new(None)))
53 .clone()
54 }
55
56 pub fn title_provider(mut self, f: impl Fn(PanelId) -> String + 'static) -> Self {
57 self.title = Rc::new(f);
58 self
59 }
60
61 pub fn card_background(mut self, color: Color) -> Self {
62 self.card_bg = color;
63 self
64 }
65
66 pub fn header_background(mut self, color: Color) -> Self {
67 self.header_bg = color;
68 self
69 }
70
71 pub fn width(mut self, width: impl Into<Length>) -> Self {
72 self.layout_width = width.into();
73 self
74 }
75
76 pub fn height(mut self, height: impl Into<Length>) -> Self {
77 self.layout_height = height.into();
78 self
79 }
80
81 pub fn fill_size(self) -> Self {
82 self.width(Length::Fill(1.0)).height(Length::Fill(1.0))
83 }
84}
85
86impl View for DockFloatLayer {
87 fn body(&self) -> impl View + 'static {
88 Unit
89 }
90
91 fn children_override(&self) -> Option<Vec<AnyView>> {
92 let m = self.model.get();
93
94 {
95 let live: Vec<u64> = m
96 .floating
97 .iter()
98 .filter(|f| f.window.is_none())
99 .map(|f| f.panel.get())
100 .collect();
101 self.drags.borrow_mut().retain(|id, _| live.contains(id));
102 }
103 let cards: Vec<AnyView> = m
104 .floating
105 .iter()
106 .filter(|f| f.window.is_none())
107 .map(|f| {
108 AnyView::new(
109 DockFloatCard {
110 model: self.model.clone(),
111 panel: f.panel,
112 title: (self.title)(f.panel),
113 content: (self.render)(f.panel),
114 x: f.x,
115 y: f.y,
116 w: f.w,
117 h: f.h,
118 card_bg: self.card_bg,
119 header_bg: self.header_bg,
120 drag: self.drag_for(f.panel),
121 }
122 .absolute_at(f.x, f.y),
123 )
124 })
125 .collect();
126 Some(cards)
127 }
128
129 fn layout_style(&self) -> LayoutStyle {
130 LayoutStyle {
131 width: self.layout_width,
132 height: self.layout_height,
133 ..LayoutStyle::default()
134 }
135 }
136
137 fn hit_testable(&self) -> bool {
138 false
139 }
140}
141
142#[derive(Clone, Copy)]
143struct CardDrag {
144 origin: Point,
145 card_x: f32,
146 card_y: f32,
147}
148
149#[derive(Clone)]
150pub(crate) struct DockFloatCard {
151 model: Signal<DockModel>,
152 panel: PanelId,
153 title: String,
154 content: AnyView,
155 x: f32,
156 y: f32,
157 w: f32,
158 h: f32,
159 card_bg: Color,
160 header_bg: Color,
161 drag: Rc<Cell<Option<CardDrag>>>,
162}
163
164impl DockFloatCard {
165 fn absolute_at(mut self, x: f32, y: f32) -> Self {
166 self.x = x;
167 self.y = y;
168 self
169 }
170
171 fn header_rect(&self, card: Rect) -> Rect {
172 Rect::from_xywh(
173 card.left(),
174 card.top(),
175 card.size.width,
176 HEADER_H.min(card.size.height),
177 )
178 }
179
180 fn body_rect(&self, card: Rect) -> Rect {
181 let hh = HEADER_H.min(card.size.height);
182 Rect::from_xywh(
183 card.left(),
184 card.top() + hh,
185 card.size.width,
186 (card.size.height - hh).max(0.0),
187 )
188 }
189}
190
191impl View for DockFloatCard {
192 fn body(&self) -> impl View + 'static {
193 Unit
194 }
195
196 fn children_override(&self) -> Option<Vec<AnyView>> {
197 let _ = self.model.get();
198 let panel = self.panel;
199 let model = self.model.clone();
200 let model2 = self.model.clone();
201 Some(vec![
202 AnyView::new(Text::new(self.title.clone())),
203 AnyView::new(DockFloatButton {
204 model,
205 panel,
206 kind: FloatButtonKind::DockBack,
207 }),
208 AnyView::new(DockFloatButton {
209 model: model2,
210 panel,
211 kind: FloatButtonKind::Close,
212 }),
213 self.content.clone(),
214 ])
215 }
216
217 fn layout_style(&self) -> LayoutStyle {
218 LayoutStyle {
219 width: Length::Px(self.w.max(1.0)),
220 height: Length::Px(self.h.max(1.0)),
221 ..LayoutStyle::default()
222 }
223 .position(sequoia_layout::Position::Absolute(
224 sequoia_layout::Inset::xy(self.x, self.y),
225 ))
226 }
227
228 fn captures_pointer(&self) -> bool {
229 true
230 }
231
232 fn has_visual_state(&self) -> bool {
233 true
234 }
235
236 fn arrange_override(
237 &self,
238 rt: &Runtime,
239 el: ElementId,
240 rect: Rect,
241 ctx: &mut LayoutCtx,
242 ) -> bool {
243 let kids = rt.children(el);
244 if kids.len() < 4 {
245 return false;
246 }
247 let head = self.header_rect(rect);
248 let body = self.body_rect(rect);
249
250 let title_w = (head.size.width - 2.0 * BTN_W).max(0.0);
251 let title_r = Rect::from_xywh(head.left(), head.top(), title_w, head.size.height);
252 let dock_r = Rect::from_xywh(head.left() + title_w, head.top(), BTN_W, head.size.height);
253 let close_r = Rect::from_xywh(
254 head.left() + title_w + BTN_W,
255 head.top(),
256 BTN_W,
257 head.size.height,
258 );
259 let slots = [
260 (kids[0], title_r),
261 (kids[1], dock_r),
262 (kids[2], close_r),
263 (kids[3], body),
264 ];
265 for (child, slot) in slots {
266 let _ = rt.measure_node(
267 child,
268 Constraints {
269 min: Size::ZERO,
270 max: Size::new(slot.size.width.max(0.0), slot.size.height.max(0.0)),
271 },
272 ctx,
273 );
274 rt.arrange_node(child, slot, ctx);
275 }
276 true
277 }
278
279 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
280 match event {
281 Event::Pointer(PointerEvent::Down {
282 button: PointerButton::Left,
283 pos,
284 ..
285 }) => {
286 let head = self.header_rect(ctx.rect);
287 let title_w = (head.size.width - 2.0 * BTN_W).max(0.0);
288 let grab = Rect::from_xywh(head.left(), head.top(), title_w, head.size.height);
289 if grab.contains(*pos) {
290 let cur = self.model.get_untracked().floating_of(self.panel);
291 let (cx, cy) = cur.map(|f| (f.x, f.y)).unwrap_or((self.x, self.y));
292 self.drag.set(Some(CardDrag {
293 origin: *pos,
294 card_x: cx,
295 card_y: cy,
296 }));
297 ctx.capture_pointer();
298 ctx.handled();
299 }
300 }
301 Event::Pointer(PointerEvent::Move { pos, .. }) => {
302 if let Some(st) = self.drag.get() {
303 let panel = self.panel;
304 let nx = st.card_x + (pos.x - st.origin.x);
305 let ny = st.card_y + (pos.y - st.origin.y);
306 self.model.update(|m| {
307 if let Some(f) = m.floating.iter_mut().find(|f| f.panel == panel) {
308 if nx.is_finite() {
309 f.x = nx;
310 }
311 if ny.is_finite() {
312 f.y = ny;
313 }
314 }
315 });
316 ctx.handled();
317 }
318 }
319 Event::Pointer(PointerEvent::Up {
320 button: PointerButton::Left,
321 ..
322 }) => {
323 self.drag.set(None);
324 }
325 Event::Pointer(PointerEvent::Cancel) => {
326 self.drag.set(None);
327 }
328 _ => {}
329 }
330 }
331
332 fn paint_self(&self, ctx: &mut PaintCtx) {
333 ctx.paint_box(crate::BoxStyle::new().background(self.card_bg).radius(8.0));
334 let head = self.header_rect(ctx.rect);
335 ctx.paint_box_in(
336 crate::BoxStyle::new()
337 .background(self.header_bg)
338 .radius(8.0),
339 head,
340 );
341 }
342}
343
344#[derive(Clone, Copy, PartialEq, Eq)]
345enum FloatButtonKind {
346 DockBack,
347 Close,
348}
349
350#[derive(Clone)]
351struct DockFloatButton {
352 model: Signal<DockModel>,
353 panel: PanelId,
354 kind: FloatButtonKind,
355}
356
357impl View for DockFloatButton {
358 fn body(&self) -> impl View + 'static {
359 Unit
360 }
361
362 fn children_override(&self) -> Option<Vec<AnyView>> {
363 let label = match self.kind {
364 FloatButtonKind::DockBack => "⧉",
365 FloatButtonKind::Close => "×",
366 };
367 Some(vec![AnyView::new(Text::new(label))])
368 }
369
370 fn layout_style(&self) -> LayoutStyle {
371 LayoutStyle {
372 width: Length::Px(BTN_W),
373 height: Length::Px(HEADER_H),
374 ..LayoutStyle::default()
375 }
376 }
377
378 fn has_visual_state(&self) -> bool {
379 true
380 }
381
382 fn captures_pointer(&self) -> bool {
383 true
384 }
385
386 fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
387 CursorIcon::Pointer
388 }
389
390 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
391 match event {
392 Event::Pointer(PointerEvent::Up {
393 button: PointerButton::Left,
394 pos,
395 ..
396 }) if ctx.rect.contains(*pos) => {
397 let panel = self.panel;
398 match self.kind {
399 FloatButtonKind::DockBack => {
400 self.model.update(|m| {
401 m.dock_first_tab(panel);
402 });
403 }
404 FloatButtonKind::Close => {
405 self.model.update(|m| {
406 m.apply(DockOp::Close { panel });
407 });
408 }
409 }
410 ctx.handled();
411 }
412 _ => {}
413 }
414 }
415}