1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//! Dropdown persistent state.
//!
//! `DropdownState` is a flat struct — fields irrelevant to the active
//! `DropdownRenderKind` are never touched.
use crate::layout::docking::DockPanel;
use crate::input::core::coordinator::InputCoordinator;
use crate::layout::LayoutManager;
use crate::types::Rect;
/// All per-dropdown instance state.
#[derive(Debug, Clone)]
pub struct DropdownState {
// --- Lifecycle ---
/// Whether the dropdown panel is currently visible.
pub open: bool,
// --- Position ---
/// Top-left of the open panel in screen coordinates.
/// Re-computed each frame from `anchor_rect` + orientation.
pub origin: (f64, f64),
/// Trigger button rect — used to re-anchor when layout reflows.
/// `None` = caller provides position via `open_position_override`.
pub anchor_rect: Option<Rect>,
// --- Selection ---
/// Id of the last-selected item (persistent across open/close cycles).
/// Used for accent bar in presets menu; optional for other menus.
pub selected_id: Option<String>,
// --- Hover ---
/// Id of the currently hovered item within the open list.
pub hovered_id: Option<String>,
// --- Scroll ---
/// Vertical scroll offset in pixels for long item lists.
/// `0.0` = top of list fully visible.
pub scroll_offset: f64,
// --- Submenu ---
/// Id of the item whose submenu is currently open.
/// `None` = no submenu open.
pub submenu_open: Option<String>,
/// Screen-space top-left position for the open submenu panel.
/// Computed as `(parent_menu.right() + gap, trigger_item.y)`.
pub submenu_origin: (f64, f64),
/// Id of the item the pointer is hovering over inside the submenu
/// panel. Set by `sync_flat_hover`.
pub submenu_hovered_id: Option<String>,
/// Submenu-trigger row id whose **chevron** is currently hovered (only
/// for `SubmenuTrigger::ChevronClick` rows). Independent from
/// `hovered_id` so the row body stays un-hovered while the chevron lights
/// up. Set by `sync_flat_hover`.
pub submenu_chevron_hovered_id: Option<String>,
// --- Sizing constraints ---
/// Maximum height of the panel in pixels.
/// `0.0` = no height limit (scroll disabled).
pub max_height: f64,
/// Minimum width of the panel in pixels.
/// Defaults to `180.0`; inline variant uses button width instead.
pub min_width: f64,
// --- Primed state ---
/// "Primed" item id: last quick-selected tool shown with accent on the trigger.
/// Specific to toolbar drawing-tool groups; `None` for all other kinds.
pub primed_id: Option<String>,
// --- Custom position override ---
/// When `Some`, overrides anchor-based positioning.
/// Used by external callers (chrome button, context trigger) that need to
/// open the dropdown at an arbitrary screen coordinate.
/// Cleared when the dropdown closes.
pub open_position_override: Option<(f64, f64)>,
}
impl Default for DropdownState {
fn default() -> Self {
Self {
open: false,
origin: (0.0, 0.0),
anchor_rect: None,
selected_id: None,
hovered_id: None,
scroll_offset: 0.0,
submenu_open: None,
submenu_origin: (0.0, 0.0),
submenu_hovered_id: None,
submenu_chevron_hovered_id: None,
max_height: 0.0,
min_width: 180.0,
primed_id: None,
open_position_override: None,
}
}
}
impl DropdownState {
/// Open the dropdown, computing origin from an anchor rect.
///
/// `anchor` — trigger button rect in screen coordinates.
/// `gap` — pixels between the bottom of the trigger and the panel top.
pub fn open_below(&mut self, anchor: Rect, gap: f64) {
self.open = true;
self.anchor_rect = Some(anchor);
self.origin = (anchor.x, anchor.y + anchor.height + gap);
self.open_position_override = None;
self.hovered_id = None;
self.submenu_open = None;
self.scroll_offset = 0.0;
}
/// Open the dropdown at an explicit screen-space position.
pub fn open_at(&mut self, x: f64, y: f64) {
self.open = true;
self.anchor_rect = None;
self.origin = (x, y);
self.open_position_override = Some((x, y));
self.hovered_id = None;
self.submenu_open = None;
self.scroll_offset = 0.0;
}
/// Close the dropdown and reset transient state.
pub fn close(&mut self) {
self.open = false;
self.hovered_id = None;
self.submenu_open = None;
self.open_position_override = None;
self.scroll_offset = 0.0;
}
/// Select an item by id (persists across open/close cycles).
pub fn select(&mut self, id: impl Into<String>) {
self.selected_id = Some(id.into());
}
/// Returns the effective panel origin: override > anchor-derived.
pub fn effective_origin(&self) -> (f64, f64) {
self.open_position_override.unwrap_or(self.origin)
}
/// Close all dropdowns in `states` except the one identified by `keep_id`.
///
/// `keep_id` — overlay slot id of the dropdown to leave open (e.g.
/// `"dd-file-overlay"`). Pass `""` to close every dropdown.
///
/// Designed for toolbar "one-click switch": opening dropdown B while A is
/// open should close A without a separate click.
///
/// # Example
/// ```ignore
/// DropdownState::close_all_except("", &mut [
/// ("dd-file-overlay", &mut self.dropdown_file_state),
/// ("dd-view-overlay", &mut self.dropdown_view_state),
/// ]);
/// ```
pub fn close_all_except(keep_id: &str, states: &mut [(&'static str, &mut Self)]) {
for (id, state) in states.iter_mut() {
if *id != keep_id {
state.close();
}
}
}
/// Toggle a dropdown open/closed, closing all others first.
///
/// `own_id` — overlay slot id of the dropdown to toggle.
/// `anchor` — trigger widget rect used to position the panel below the button.
/// `gap` — gap between trigger bottom and panel top.
/// `states` — mutable slice of `(overlay_id, state)` covering ALL
/// managed dropdowns (including the one being toggled).
pub fn toggle_at(own_id: &str, anchor: Rect, gap: f64, states: &mut [(&'static str, &mut Self)]) {
let was_open = states.iter().find(|(id, _)| *id == own_id).map(|(_, st)| st.open).unwrap_or(false);
Self::close_all_except("", states);
if !was_open {
if let Some((_, state)) = states.iter_mut().find(|(id, _)| *id == own_id) {
state.open_below(anchor, gap);
}
}
}
/// Sync the hovered-item id from the coordinator's hovered widget.
///
/// **Deprecated** — use `sync_hover_from_layout` when a `LayoutManager`
/// is available. Kept for back-compat with L3 callers.
pub fn sync_hover_from(&mut self, coord: &InputCoordinator, widget_id_prefix: &str) {
if !self.open {
return;
}
self.hovered_id = coord
.hovered_widget()
.map(|id| id.0.as_str())
.filter(|s| s.starts_with(widget_id_prefix))
.map(|s| s[widget_id_prefix.len()..].to_owned());
}
/// Sync hover state for a Flat dropdown via the `LayoutManager`.
///
/// Preferred over `sync_flat_hover` — reads from `LayoutManager::hovered_widget()`
/// which is kept current by `on_pointer_move` and not reset by `begin_frame`.
pub fn sync_flat_hover_from_layout<P: DockPanel>(
&mut self,
layout: &LayoutManager<P>,
dropdown_id: &str,
) {
if !self.open {
return;
}
let hovered = layout.hovered_widget().map(|w| w.0.clone());
self.apply_flat_hover(hovered, dropdown_id);
}
/// Sync hover state for a Flat dropdown that has both a main panel
/// and a submenu panel. Recognises four child-id prefixes:
/// `:item:`, `:submenu:`, `:submenu-chevron:`, `:sub-item:`.
/// Updates `hovered_id` (main panel) and `submenu_hovered_id`
/// (submenu panel).
///
/// **Deprecated** — use `sync_flat_hover_from_layout` when a
/// `LayoutManager` is available.
pub fn sync_flat_hover(&mut self, coord: &InputCoordinator, dropdown_id: &str) {
if !self.open {
return;
}
let hovered = coord.hovered_widget().map(|w| w.0.clone());
self.apply_flat_hover(hovered, dropdown_id);
}
fn apply_flat_hover(&mut self, hovered: Option<String>, dropdown_id: &str) {
let main_pref = format!("{}:item:", dropdown_id);
let sm_pref = format!("{}:submenu:", dropdown_id);
// Sticky chevron registers as `{parent}:chev:submenu:{row_id}`.
let chev_pref = format!("{}:chev:submenu:", dropdown_id);
let sub_pref = format!("{}:sub-item:", dropdown_id);
self.hovered_id = None;
self.submenu_hovered_id = None;
self.submenu_chevron_hovered_id = None;
if let Some(id) = hovered {
if let Some(rest) = id.strip_prefix(&main_pref) {
self.hovered_id = Some(rest.to_string());
} else if let Some(rest) = id.strip_prefix(&sm_pref) {
// Hover on a submenu-trigger row body itself.
self.hovered_id = Some(rest.to_string());
} else if let Some(rest) = id.strip_prefix(&sub_pref) {
self.submenu_hovered_id = Some(rest.to_string());
} else if let Some(rest) = id.strip_prefix(&chev_pref) {
// Chevron of a ChevronClick submenu trigger — light up the
// chevron only, keep the row body un-hovered.
self.submenu_chevron_hovered_id = Some(rest.to_string());
}
}
}
}