Skip to main content

sequoia_view/
edge_dock.rs

1use std::rc::Rc;
2
3use sequoia_core::{Color, Length, Point, Rect, Size};
4use sequoia_layout::LayoutStyle;
5
6use crate::any::AnyView;
7use crate::dnd::{DragPayload, DropPosition};
8use crate::view::{EventCtx, PaintCtx, View};
9use crate::views::{Column, Row, Unit};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum EdgeSide {
13    Left,
14    Right,
15    Top,
16    Bottom,
17}
18
19impl EdgeSide {
20    pub const ALL: [EdgeSide; 4] = [
21        EdgeSide::Left,
22        EdgeSide::Right,
23        EdgeSide::Top,
24        EdgeSide::Bottom,
25    ];
26
27    pub fn is_vertical(self) -> bool {
28        matches!(self, EdgeSide::Left | EdgeSide::Right)
29    }
30
31    pub fn is_horizontal(self) -> bool {
32        !self.is_vertical()
33    }
34}
35
36pub const EDGE_HINT_PX: f32 = 28.0;
37
38pub const EDGE_GHOST_PX: f32 = 72.0;
39
40pub const EDGE_LINE_PX: f32 = 3.0;
41
42pub const UNDOCK_THRESHOLD_PX: f32 = 6.0;
43
44pub const MIN_VISIBLE_W: f32 = 120.0;
45pub const MIN_VISIBLE_H: f32 = 34.0;
46
47pub fn edge_hint_at(pos: Point, window: Size, threshold: f32) -> Option<EdgeSide> {
48    let t = if threshold.is_finite() && threshold > 0.0 {
49        threshold
50    } else {
51        EDGE_HINT_PX
52    };
53    if pos.x < t {
54        Some(EdgeSide::Left)
55    } else if pos.x > window.width - t {
56        Some(EdgeSide::Right)
57    } else if pos.y < t {
58        Some(EdgeSide::Top)
59    } else if pos.y > window.height - t {
60        Some(EdgeSide::Bottom)
61    } else {
62        None
63    }
64}
65
66pub fn edge_ghost_rect(side: EdgeSide, window: Size, thickness: f32) -> Rect {
67    let t = if thickness.is_finite() && thickness > 0.0 {
68        thickness
69    } else {
70        EDGE_GHOST_PX
71    };
72    match side {
73        EdgeSide::Left => Rect::from_xywh(0.0, 0.0, t, window.height),
74        EdgeSide::Right => Rect::from_xywh((window.width - t).max(0.0), 0.0, t, window.height),
75        EdgeSide::Top => Rect::from_xywh(0.0, 0.0, window.width, t),
76        EdgeSide::Bottom => Rect::from_xywh(0.0, (window.height - t).max(0.0), window.width, t),
77    }
78}
79
80pub fn clamp_card_top_left(global: Point, grab_offset: (f32, f32), window: Size) -> (f32, f32) {
81    let nx = global.x - grab_offset.0;
82    let ny = global.y - grab_offset.1;
83    let max_x = (window.width - MIN_VISIBLE_W).max(0.0);
84    let max_y = (window.height - MIN_VISIBLE_H).max(0.0);
85    (nx.clamp(0.0, max_x), ny.clamp(0.0, max_y))
86}
87
88pub fn exceeded_undock(origin: Point, current: Point) -> bool {
89    let dx = current.x - origin.x;
90    let dy = current.y - origin.y;
91    dx * dx + dy * dy >= UNDOCK_THRESHOLD_PX * UNDOCK_THRESHOLD_PX
92}
93
94pub fn main_first_index(side: EdgeSide) -> bool {
95    matches!(side, EdgeSide::Right | EdgeSide::Bottom)
96}
97
98#[derive(Clone)]
99pub struct EdgeGhost {
100    side: EdgeSide,
101    color: Color,
102}
103
104impl EdgeGhost {
105    pub fn new(side: EdgeSide) -> Self {
106        Self {
107            side,
108
109            color: Color::rgb8(0xFF, 0xFF, 0xFF),
110        }
111    }
112
113    pub fn color(mut self, color: Color) -> Self {
114        self.color = color;
115        self
116    }
117}
118
119impl View for EdgeGhost {
120    fn body(&self) -> impl View + 'static {
121        Unit
122    }
123
124    fn children_override(&self) -> Option<Vec<AnyView>> {
125        let line_color = self.color;
126        let bar: AnyView = match self.side {
127            EdgeSide::Left => AnyView::new(
128                Row::new()
129                    .fill_size()
130                    .child(
131                        crate::views::Panel::new()
132                            .background(line_color)
133                            .width(EDGE_LINE_PX)
134                            .fill_height(),
135                    )
136                    .child_flex(Unit, 1.0),
137            ),
138            EdgeSide::Right => AnyView::new(
139                Row::new().fill_size().child_flex(Unit, 1.0).child(
140                    crate::views::Panel::new()
141                        .background(line_color)
142                        .width(EDGE_LINE_PX)
143                        .fill_height(),
144                ),
145            ),
146            EdgeSide::Top => AnyView::new(
147                Column::new()
148                    .fill_size()
149                    .child(
150                        crate::views::Panel::new()
151                            .background(line_color)
152                            .fill_width()
153                            .height(EDGE_LINE_PX),
154                    )
155                    .child_flex(Unit, 1.0),
156            ),
157            EdgeSide::Bottom => AnyView::new(
158                Column::new().fill_size().child_flex(Unit, 1.0).child(
159                    crate::views::Panel::new()
160                        .background(line_color)
161                        .fill_width()
162                        .height(EDGE_LINE_PX),
163                ),
164            ),
165        };
166
167        Some(vec![bar])
168    }
169
170    fn layout_style(&self) -> LayoutStyle {
171        let wide = Length::Px(EDGE_GHOST_PX);
172        let full = Length::Fill(1.0);
173        let (w, h) = match self.side {
174            EdgeSide::Left | EdgeSide::Right => (wide, full),
175            EdgeSide::Top | EdgeSide::Bottom => (full, wide),
176        };
177        LayoutStyle {
178            width: w,
179            height: h,
180            ..LayoutStyle::default()
181        }
182    }
183
184    fn hit_testable(&self) -> bool {
185        false
186    }
187}
188
189pub struct EdgeSlab {
190    side: EdgeSide,
191    accepts: Rc<dyn Fn(&DragPayload) -> bool>,
192    on_over: Rc<dyn Fn()>,
193    on_drop: Rc<dyn Fn(DropPosition, &mut EventCtx)>,
194}
195
196impl Clone for EdgeSlab {
197    fn clone(&self) -> Self {
198        Self {
199            side: self.side,
200            accepts: self.accepts.clone(),
201            on_over: self.on_over.clone(),
202            on_drop: self.on_drop.clone(),
203        }
204    }
205}
206
207impl EdgeSlab {
208    pub fn new(side: EdgeSide) -> Self {
209        Self {
210            side,
211            accepts: Rc::new(|_| false),
212            on_over: Rc::new(|| {}),
213            on_drop: Rc::new(|_, _| {}),
214        }
215    }
216
217    pub fn get_side(&self) -> EdgeSide {
218        self.side
219    }
220
221    pub fn accepts(mut self, f: impl Fn(&DragPayload) -> bool + 'static) -> Self {
222        self.accepts = Rc::new(f);
223        self
224    }
225
226    pub fn on_drag_over(mut self, f: impl Fn() + 'static) -> Self {
227        self.on_over = Rc::new(f);
228        self
229    }
230
231    pub fn on_drop_with(mut self, f: impl Fn(DropPosition, &mut EventCtx) + 'static) -> Self {
232        self.on_drop = Rc::new(f);
233        self
234    }
235}
236
237impl View for EdgeSlab {
238    fn body(&self) -> impl View + 'static {
239        Unit
240    }
241
242    fn children_override(&self) -> Option<Vec<AnyView>> {
243        None
244    }
245
246    fn layout_style(&self) -> LayoutStyle {
247        let wide = Length::Px(EDGE_GHOST_PX);
248        let full = Length::Fill(1.0);
249        let (w, h) = match self.side {
250            EdgeSide::Left | EdgeSide::Right => (wide, full),
251            EdgeSide::Top | EdgeSide::Bottom => (full, wide),
252        };
253        LayoutStyle {
254            width: w,
255            height: h,
256            ..LayoutStyle::default()
257        }
258    }
259
260    fn accepts_drop(&self, payload: &DragPayload) -> bool {
261        (self.accepts)(payload)
262    }
263
264    fn on_drag_over(&self, _pos: DropPosition, _ctx: &mut EventCtx) {
265        (self.on_over)();
266    }
267
268    fn on_drop(&self, pos: DropPosition, ctx: &mut EventCtx) {
269        (self.on_drop)(pos, ctx);
270    }
271
272    fn paint_self(&self, _ctx: &mut PaintCtx) {}
273}
274
275#[allow(dead_code)]
276fn _assert_send_sync() {
277    fn is<T: Clone + 'static>() {}
278    is::<EdgeSide>();
279    is::<EdgeGhost>();
280}