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
//! Modal persistent state.
use crate::types::{Rect, ScrollState};
use super::super::resize_drag::ResizeDrag;
/// All per-modal frame state.
///
/// Fields are stored flat (no per-template sub-enums) so the caller can hold
/// a single `ModalState` regardless of which `ModalRenderKind` is in use.
/// Fields irrelevant to the active kind are simply never touched.
#[derive(Debug, Default, Clone)]
pub struct ModalState {
// --- Position & drag (all draggable modals) ---
/// Modal top-left position. `(0.0, 0.0)` = use caller-supplied `rect` as-is.
///
/// Set to a non-zero value when the user drags the modal.
pub position: (f64, f64),
/// Whether a drag gesture is currently in progress.
pub dragging: bool,
/// Cursor position relative to the modal origin recorded at drag start.
pub drag_offset: (f64, f64),
// --- Tab navigation (TopTabs, SideTabs) ---
/// Index of the currently selected tab (0-based).
pub active_tab: usize,
/// Index of the tab the pointer is currently hovering over.
pub hovered_tab: Option<usize>,
// --- Wizard pagination ---
/// Index of the currently displayed wizard page (0-based).
pub current_page: usize,
// --- Scroll ---
/// Shared scroll state for single-body modals.
pub scroll: ScrollState,
// --- Close button hover ---
/// Whether the pointer is over the close-X button this frame.
pub hovered_close: bool,
/// Index of the footer button the pointer is hovering over (0-based).
pub footer_hovered: Option<usize>,
// --- Resize ---
/// Resize drag in progress. Set by `start_resize`, consumed by
/// `update_resize`, cleared on `end_resize` (called from
/// `on_mouse_up` / `end_drag`).
pub resize_drag: Option<ResizeDrag>,
/// User-resized override for the modal frame. `None` = use the
/// caller-supplied measured rect. `Some(rect)` = composite returns
/// this rect from `effective_rect()`.
pub resized_rect: Option<Rect>,
// --- Body scroll (Scrollbar / Chevrons overflow modes) ---
/// Total content size of the body (caller-set every frame).
pub body_content_h: f64,
pub body_content_w: f64,
/// Track rect of the body scrollbar — set by the composite at
/// register time. Used by `body_scroll_track_click` / drag.
pub body_scroll_track: Option<Rect>,
/// Body viewport size — set by the composite at register time.
pub body_viewport_h: f64,
pub body_viewport_w: f64,
/// Horizontal scroll offset (Chevrons / Scrollbar overflow). Vertical
/// offset lives on `scroll.offset` (legacy).
pub body_scroll_x: f64,
}
impl ModalState {
/// Begin a drag gesture at `cursor_pos` with the modal currently at `modal_origin`.
pub fn start_drag(&mut self, cursor_pos: (f64, f64), modal_origin: (f64, f64)) {
self.dragging = true;
self.drag_offset = (
cursor_pos.0 - modal_origin.0,
cursor_pos.1 - modal_origin.1,
);
}
/// Update modal position while dragging.
///
/// `cursor_pos` — current pointer position.
/// `screen_size` — `(width, height)` used to clamp the modal inside the viewport.
/// `modal_size` — `(width, height)` of the modal frame.
pub fn update_drag(
&mut self,
cursor_pos: (f64, f64),
screen_size: (f64, f64),
modal_size: (f64, f64),
) {
if !self.dragging {
return;
}
let x = (cursor_pos.0 - self.drag_offset.0)
.clamp(0.0, (screen_size.0 - modal_size.0).max(0.0));
let y = (cursor_pos.1 - self.drag_offset.1)
.clamp(0.0, (screen_size.1 - modal_size.1).max(0.0));
self.position = (x, y);
}
/// End the current drag gesture.
pub fn end_drag(&mut self) {
self.dragging = false;
}
/// Switch the active tab and reset per-tab scroll.
pub fn set_active_tab(&mut self, index: usize) {
self.active_tab = index;
self.scroll.reset();
}
/// Advance to the next wizard page (saturates at `page_count - 1`).
pub fn next_page(&mut self, page_count: usize) {
if page_count > 0 {
self.current_page = (self.current_page + 1).min(page_count.saturating_sub(1));
}
}
/// Go back to the previous wizard page (saturates at 0).
pub fn prev_page(&mut self) {
self.current_page = self.current_page.saturating_sub(1);
}
/// Begin a resize drag from a `ResizeHandleDragStarted` event.
/// `start_rect` — the current frame rect, `cursor` — pointer at mouse-down.
/// `min` / `cap` — width/height bounds.
pub fn start_resize(
&mut self,
edge: crate::layout::ResizeEdge,
start_rect: Rect,
cursor: (f64, f64),
min: (f64, f64),
cap: (f64, f64),
) {
self.resize_drag = Some(ResizeDrag::begin(edge, start_rect, cursor, min, cap));
}
/// Update the resized rect from a fresh cursor position. No-op when
/// no resize drag is in progress. Writes the resolved rect to
/// `resized_rect` and the resolved origin to `position` so the modal
/// composite picks them up next frame.
pub fn update_resize(&mut self, cursor: (f64, f64)) {
if let Some(drag) = self.resize_drag {
let rect = drag.resolve(cursor);
self.resized_rect = Some(rect);
self.position = (rect.x, rect.y);
}
}
/// End any active resize drag (call from `on_mouse_up`).
pub fn end_resize(&mut self) {
self.resize_drag = None;
}
// --- Body scroll helpers (used by overflow Scrollbar / Chevrons) ---
/// Begin a scrollbar-thumb drag. Cursor Y at mouse-down.
pub fn start_body_scroll_drag(&mut self, cursor_y: f64) {
self.scroll.start_drag(cursor_y);
}
/// Update the scrollbar drag from a fresh cursor Y. Uses the
/// composite-recorded track rect / viewport / content height.
pub fn update_body_scroll_drag(&mut self, cursor_y: f64) {
if let Some(track) = self.body_scroll_track {
self.scroll.handle_drag(cursor_y, track.height,
self.body_content_h, self.body_viewport_h);
}
}
/// Apply a track click — jump scroll to the click position.
pub fn body_scroll_track_click(&mut self, cursor_y: f64) {
if let Some(track) = self.body_scroll_track {
self.scroll.handle_track_click(cursor_y, track.y, track.height,
self.body_content_h, self.body_viewport_h);
}
}
/// Step the body scroll by one viewport in the given direction.
pub fn body_chevron_step(&mut self, direction: crate::layout::ChevronStepDirection) {
use crate::layout::ChevronStepDirection as D;
match direction {
D::Up | D::Down => {
let max = (self.body_content_h - self.body_viewport_h).max(0.0);
let step = self.body_viewport_h.max(40.0);
let signed = if matches!(direction, D::Up) { -step } else { step };
let before = self.scroll.offset;
self.scroll.offset = (self.scroll.offset + signed).clamp(0.0, max);
eprintln!("[body_chevron_step] V dir={:?} ch={:.1} vh={:.1} max={:.1} step={:.1} {:.1}->{:.1}",
direction, self.body_content_h, self.body_viewport_h, max, step, before, self.scroll.offset);
}
D::Left | D::Right => {
let max = (self.body_content_w - self.body_viewport_w).max(0.0);
let step = self.body_viewport_w.max(40.0);
let signed = if matches!(direction, D::Left) { -step } else { step };
let before = self.body_scroll_x;
self.body_scroll_x = (self.body_scroll_x + signed).clamp(0.0, max);
eprintln!("[body_chevron_step] H dir={:?} cw={:.1} vw={:.1} max={:.1} step={:.1} {:.1}->{:.1}",
direction, self.body_content_w, self.body_viewport_w, max, step, before, self.body_scroll_x);
}
}
}
/// End any scrollbar drag (call from `on_mouse_up`).
pub fn end_body_scroll_drag(&mut self) {
self.scroll.end_drag();
}
}