GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/plate_dock.rs (6.9K)
1 //! The plate-corner dock affordance (RFC Phase 7c), generalized from
2 //! cce-designer's plate corner: a small circular control on a pane plate's
3 //! top-right that opens a menu (Collapse/Expand, Detach/Reattach, plus
4 //! host-specific rows), collapses the plate to a title stub, and arms
5 //! click-vs-drag for dock repositioning.
6 //!
7 //! This module owns the app-agnostic PROTOCOL: control geometry and hit
8 //! testing, the per-plate state vocabulary, the standard menu assembly, and
9 //! the press-becomes-drag threshold. The HOST keeps everything that is
10 //! policy: which plates carry controls, where docks are and what dropping
11 //! means, how a detached pane becomes a window (process model, sync channel),
12 //! and how collapse reshapes its layout. The role flip a detach implies on
13 //! the plate itself is [`crate::scene::paint::PlateSpec::detached`].
14
15 /// Radius of the corner control's hit circle (and its drawn dot).
16 pub const CORNER_R: f32 = 8.0;
17
18 /// Centre inset from the plate's top-right corner, on both axes. Clears the
19 /// plate's own corner arc at the radii the DE ships.
20 pub const CORNER_INSET: f32 = 14.0;
21
22 /// A plate needs at least this much room before it earns a corner control —
23 /// below it the trigger would cover the pane it belongs to.
24 pub const MIN_PLATE_SPAN: f32 = 3.0 * CORNER_INSET;
25
26 /// Height of a collapsed plate: its title stub. Deep enough for the title
27 /// text and the corner control that restores it, and no deeper.
28 pub const STUB_H: f32 = 26.0;
29
30 /// Pointer travel (Chebyshev) past which an armed corner press stops being a
31 /// click and becomes a dock drag.
32 pub const DRAG_THRESHOLD: f32 = 4.0;
33
34 /// One plate's dock state. `collapsed` and `detached` are exclusive in
35 /// practice (a detached pane's stub is not collapsible — the menu offers only
36 /// Reattach), but the type does not enforce it; the host's dispatch does.
37 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
38 pub struct PlateDockState {
39 /// Shrunk to its title stub in the host layout.
40 pub collapsed: bool,
41 /// Moved out into its own window; the host keeps a stub for reattaching.
42 pub detached: bool,
43 }
44
45 impl PlateDockState {
46 /// Whether the plate currently shows as a title stub (either way).
47 pub fn stubbed(&self) -> bool {
48 self.collapsed || self.detached
49 }
50 }
51
52 /// What the standard corner menu can do to its plate. Hosts append their own
53 /// rows after these (the designer's spreadsheet span modes, for example) —
54 /// the row protocol is (label, action) pairs in display order.
55 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
56 pub enum PlateDockAction {
57 Collapse,
58 Expand,
59 Detach,
60 Reattach,
61 }
62
63 /// Centre of the corner control for a plate occupying `rect`
64 /// (`(x, y, w, h)`), or `None` when the plate is too small to carry one. A
65 /// stub is BUILT to carry the control and is shorter than the minimum span a
66 /// full pane must clear, so it centres vertically instead — that control is
67 /// the only way to bring the pane back.
68 pub fn corner_center(rect: (f32, f32, f32, f32), stubbed: bool) -> Option<(f32, f32)> {
69 let (x, y, w, h) = rect;
70 if w < MIN_PLATE_SPAN {
71 return None;
72 }
73 if stubbed {
74 return Some((x + w - CORNER_INSET, y + h / 2.0));
75 }
76 if h < MIN_PLATE_SPAN {
77 return None;
78 }
79 Some((x + w - CORNER_INSET, y + CORNER_INSET))
80 }
81
82 /// Whether `(px, py)` hits a control centred at `center`.
83 pub fn corner_hit(center: (f32, f32), px: f32, py: f32) -> bool {
84 let (dx, dy) = (px - center.0, py - center.1);
85 dx * dx + dy * dy <= CORNER_R * CORNER_R
86 }
87
88 /// Whether an armed corner press at `press` has travelled far enough at
89 /// `cursor` to become a dock drag rather than a click.
90 pub fn press_becomes_drag(press: (f32, f32), cursor: (f32, f32)) -> bool {
91 (cursor.0 - press.0).abs().max((cursor.1 - press.1).abs()) > DRAG_THRESHOLD
92 }
93
94 /// Draw the corner control at `center`: the plate-border-colored dot,
95 /// enlarged when `emphasized` (hovered, or its menu is open). Earned by the
96 /// second consumer (RFC 7c-2) — both hosts drew the identical dot.
97 pub fn draw_corner_dot(
98 pc: &mut crate::scene::paint::PaintCtx,
99 center: (f32, f32),
100 emphasized: bool,
101 ) {
102 let r = if emphasized { CORNER_R * 1.15 } else { CORNER_R };
103 let fill = crate::color::plate_border_color().unwrap_or([0.55, 0.58, 0.66, 0.85]);
104 pc.circle(center.0, center.1, r, fill);
105 }
106
107 /// The standard corner-menu rows for a plate in `state`. A detached pane's
108 /// stub offers ONLY Reattach (collapsing it would mean nothing); otherwise
109 /// Collapse/Expand per state, then Detach when the host allows it. The host
110 /// appends its own rows after these and owns dispatch.
111 pub fn standard_menu(state: PlateDockState, can_detach: bool) -> Vec<(String, PlateDockAction)> {
112 if state.detached {
113 return vec![("Reattach".to_string(), PlateDockAction::Reattach)];
114 }
115 let mut rows = Vec::new();
116 if state.collapsed {
117 rows.push(("Expand".to_string(), PlateDockAction::Expand));
118 } else {
119 rows.push(("Collapse".to_string(), PlateDockAction::Collapse));
120 }
121 if can_detach {
122 rows.push(("Detach".to_string(), PlateDockAction::Detach));
123 }
124 rows
125 }
126
127 #[cfg(test)]
128 mod tests {
129 use super::*;
130
131 #[test]
132 fn corner_geometry_and_arming() {
133 // A full-size plate: control inset from the top-right.
134 let c = corner_center((100.0, 50.0, 300.0, 200.0), false).unwrap();
135 assert_eq!(c, (100.0 + 300.0 - CORNER_INSET, 50.0 + CORNER_INSET));
136 assert!(corner_hit(c, c.0 + CORNER_R - 0.1, c.1));
137 assert!(!corner_hit(c, c.0 + CORNER_R + 0.1, c.1));
138
139 // Too narrow, or full-height too short: no control.
140 assert!(corner_center((0.0, 0.0, MIN_PLATE_SPAN - 1.0, 200.0), false).is_none());
141 assert!(corner_center((0.0, 0.0, 300.0, MIN_PLATE_SPAN - 1.0), false).is_none());
142
143 // A stub is exempt from the height guard and centres vertically —
144 // its control is the only way back.
145 let s = corner_center((0.0, 0.0, 300.0, STUB_H), true).unwrap();
146 assert_eq!(s, (300.0 - CORNER_INSET, STUB_H / 2.0));
147
148 // Click-vs-drag threshold.
149 assert!(!press_becomes_drag((10.0, 10.0), (13.0, 13.0)));
150 assert!(press_becomes_drag((10.0, 10.0), (10.0, 15.0)));
151 }
152
153 #[test]
154 fn standard_menu_variants() {
155 let plain = PlateDockState::default();
156 let rows = standard_menu(plain, true);
157 assert_eq!(
158 rows.iter().map(|(l, _)| l.as_str()).collect::<Vec<_>>(),
159 ["Collapse", "Detach"]
160 );
161 assert_eq!(standard_menu(plain, false).len(), 1);
162
163 let collapsed = PlateDockState { collapsed: true, detached: false };
164 assert_eq!(standard_menu(collapsed, true)[0].1, PlateDockAction::Expand);
165 assert!(collapsed.stubbed());
166
167 let detached = PlateDockState { collapsed: false, detached: true };
168 let rows = standard_menu(detached, true);
169 assert_eq!(rows.len(), 1);
170 assert_eq!(rows[0].1, PlateDockAction::Reattach);
171 assert!(detached.stubbed());
172 }
173 }