uzor 1.0.13

Core UI engine — geometry, interaction, input state
Documentation
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
//! Separator - Interactive dividers between panels with hit testing
//!
//! This module provides the visual and interactive elements for panel separation:
//! - **Separator**: Draggable separators between panels with hit testing
//! - **SeparatorController**: Drag state management and constraint enforcement
//!
//! # Architecture
//!
//! Separators are the interactive dividers between panels in linear containers.
//! They support:
//! - Visual feedback (thickness changes on hover/drag)
//! - Hit testing with wider interaction area (8px hit width vs 2px visual)
//! - Constraint enforcement (minimum panel sizes)
//! - Snap-back indication when constraints violated (returns None)
//!
//! # Usage
//!
//! ```rust,ignore
//! use uzor_panels::{Separator, SeparatorOrientation, SeparatorController};
//!
//! // Create separator between two panels (child 0 and child 1)
//! let separator = Separator::new(
//!     SeparatorOrientation::Vertical,
//!     300.0,
//!     0.0,
//!     600.0,
//!     SeparatorLevel::Node {
//!         parent_id: BranchId(0),
//!         child_a: 1,
//!         child_b: 2
//!     }
//! );
//!
//! // Check if mouse is over separator
//! if separator.hit_test(mouse_x, mouse_y) {
//!     // Start drag
//!     controller.start_drag(0, container_id, 300.0, vec![1.0, 1.0]);
//! }
//!
//! // Update drag and check constraints
//! if let Some(new_shares) = controller.update_drag(delta, &children, &min_sizes, total_size) {
//!     // Apply new shares - constraints satisfied
//! } else {
//!     // Constraint violated - animate snap-back
//! }
//! ```

use super::id::{NodeId, BranchId};

// =============================================================================
// Separator Geometry
// =============================================================================

/// Separator between children of a branch node at any depth in the panel tree
#[derive(Clone, Debug)]
pub enum SeparatorLevel {
    /// Separator between two children of the same branch node
    Node {
        parent_id: BranchId,   // the branch node containing both siblings
        child_a: u64,          // left/top child node (raw ID - could be leaf or branch)
        child_b: u64,          // right/bottom child node (raw ID - could be leaf or branch)
    },
}

/// Separator orientation (vertical = |, horizontal = —)
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum SeparatorOrientation {
    /// Vertical separator (between horizontal panels: | )
    Vertical,
    /// Horizontal separator (between vertical panels: — )
    Horizontal,
}

/// Separator visual/interaction state
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum SeparatorState {
    /// Idle state (default thickness)
    Idle,
    /// Hover state (thicker, highlighted)
    Hover,
    /// Dragging state (thicker, highlighted)
    Dragging,
}

/// Separator between panels in linear container
///
/// Separators are draggable dividers that allow resizing adjacent panels.
/// They have:
/// - Visual thickness (2px idle, 4px hover/drag)
/// - Hit width (8px for easier grabbing)
/// - Position along axis
/// - Start position on perpendicular axis
/// - Length perpendicular to axis
pub struct Separator {
    /// Orientation (vertical or horizontal)
    pub orientation: SeparatorOrientation,
    /// Position along axis (pixels from container start)
    pub position: f32,
    /// Start position on perpendicular axis (y for vertical, x for horizontal)
    pub start: f32,
    /// Length perpendicular to axis (pixels)
    pub length: f32,
    /// Visual thickness (changes with state)
    #[allow(dead_code)]
    thickness: f32,
    /// Interaction width (always 8px for easier hit testing)
    hit_width: f32,
    /// Current state
    pub state: SeparatorState,
    /// What level this separator operates at (node level)
    pub level: SeparatorLevel,
}

impl Separator {
    /// Create new separator
    ///
    /// # Arguments
    /// - `orientation`: Vertical or horizontal
    /// - `position`: Position along axis (pixels)
    /// - `start`: Start position on perpendicular axis (y for vertical, x for horizontal)
    /// - `length`: Length perpendicular to axis (pixels)
    /// - `level`: What level this separator operates at (node)
    pub fn new(orientation: SeparatorOrientation, position: f32, start: f32, length: f32, level: SeparatorLevel) -> Self {
        Self {
            orientation,
            position,
            start,
            length,
            thickness: 2.0,
            hit_width: 8.0,
            state: SeparatorState::Idle,
            level,
        }
    }

    /// Get child_a (for backward compatibility with corner drag code)
    pub fn child_a(&self) -> Option<u64> {
        match &self.level {
            SeparatorLevel::Node { child_a, .. } => Some(*child_a),
        }
    }

    /// Get child_b (for backward compatibility with corner drag code)
    pub fn child_b(&self) -> Option<u64> {
        match &self.level {
            SeparatorLevel::Node { child_b, .. } => Some(*child_b),
        }
    }

    /// Hit test - check if point is over separator
    ///
    /// Uses wider hit_width (8px) for easier interaction.
    ///
    /// # Arguments
    /// - `x`, `y`: Point to test (relative to separator's container)
    ///
    /// # Returns
    /// `true` if point is within hit area
    pub fn hit_test(&self, x: f32, y: f32) -> bool {
        match self.orientation {
            SeparatorOrientation::Vertical => {
                // Check X position (with hit_width padding)
                let min_x = self.position - self.hit_width / 2.0;
                let max_x = self.position + self.hit_width / 2.0;
                x >= min_x && x <= max_x && y >= self.start && y <= self.start + self.length
            }
            SeparatorOrientation::Horizontal => {
                // Check Y position
                let min_y = self.position - self.hit_width / 2.0;
                let max_y = self.position + self.hit_width / 2.0;
                y >= min_y && y <= max_y && x >= self.start && x <= self.start + self.length
            }
        }
    }

    /// Get visual thickness based on current state
    ///
    /// - Idle: 2px
    /// - Hover: 4px
    /// - Dragging: 4px
    pub fn thickness_for_state(&self) -> f32 {
        match self.state {
            SeparatorState::Idle => 2.0,
            SeparatorState::Hover | SeparatorState::Dragging => 4.0,
        }
    }
}

// =============================================================================
// Separator Controller
// =============================================================================

/// Drag state for separator resizing
#[derive(Clone, Debug)]
pub struct SeparatorDragState {
    /// Index of separator being dragged
    separator_idx: usize,
    /// Container owning the separator
    #[allow(dead_code)]
    container_id: NodeId,
    /// Starting position of separator
    #[allow(dead_code)]
    start_pos: f32,
    /// Original shares of all children
    start_shares: Vec<f32>,
}

/// Separator drag controller
///
/// Manages separator dragging with constraint enforcement:
/// 1. Start drag: capture initial state
/// 2. Update drag: calculate new shares, check constraints
/// 3. End drag: finalize or cancel
///
/// When constraints are violated (panel below min_size), update_drag returns None.
/// The caller should then trigger a snap-back animation.
pub struct SeparatorController {
    /// Current drag state (if dragging)
    dragging: Option<SeparatorDragState>,
}

impl SeparatorController {
    /// Create new separator controller
    pub fn new() -> Self {
        Self { dragging: None }
    }

    /// Start drag operation
    ///
    /// # Arguments
    /// - `separator_idx`: Index of separator (between children[idx] and children[idx+1])
    /// - `container_id`: Container owning the separator
    /// - `pos`: Starting position of separator
    /// - `shares`: Current shares of all children
    pub fn start_drag(
        &mut self,
        separator_idx: usize,
        container_id: NodeId,
        pos: f32,
        shares: Vec<f32>,
    ) {
        self.dragging = Some(SeparatorDragState {
            separator_idx,
            container_id,
            start_pos: pos,
            start_shares: shares,
        });
    }

    /// Update drag and check constraints
    ///
    /// # Arguments
    /// - `delta`: Mouse movement delta along axis (pixels)
    /// - `children`: Child panel IDs
    /// - `min_sizes`: Minimum size for each child (pixels)
    /// - `total_size`: Total available size (pixels)
    ///
    /// # Returns
    /// - `Some(new_shares)`: New shares if constraints satisfied
    /// - `None`: Constraints violated (caller should animate snap-back)
    pub fn update_drag(
        &self,
        delta: f32,
        children: &[NodeId],
        min_sizes: &[f32],
        total_size: f32,
    ) -> Option<Vec<f32>> {
        let drag = self.dragging.as_ref()?;

        if children.len() < 2 || min_sizes.len() != children.len() {
            return None;
        }

        // Calculate new shares based on delta
        let mut new_shares = drag.start_shares.clone();
        let idx = drag.separator_idx;

        if idx >= children.len() - 1 {
            return None; // Invalid separator index
        }

        // Calculate total shares and convert delta to share delta
        let total_shares: f32 = new_shares.iter().sum();
        if total_shares <= 0.0 {
            return None;
        }

        // Convert pixel delta to share delta
        let delta_ratio = delta / total_size;
        let share_delta = delta_ratio * total_shares;

        // Adjust shares of adjacent children
        // Left/top child gets +delta, right/bottom child gets -delta
        new_shares[idx] += share_delta;
        new_shares[idx + 1] -= share_delta;

        // Enforce minimum sizes
        for (i, &share) in new_shares.iter().enumerate() {
            if share < 0.0 {
                // Negative share = constraint violated
                return None;
            }

            let pixel_size = (share / total_shares) * total_size;
            if pixel_size < min_sizes[i] {
                // Constraint violated - below minimum size
                return None;
            }
        }

        // Normalize shares (ensure they sum to original total)
        let current_sum: f32 = new_shares.iter().sum();
        if current_sum <= 0.0 {
            return None;
        }

        let scale = total_shares / current_sum;
        for share in &mut new_shares {
            *share *= scale;
        }

        Some(new_shares)
    }

    /// End drag operation
    pub fn end_drag(&mut self) {
        self.dragging = None;
    }

    /// Check if currently dragging
    pub fn is_dragging(&self) -> bool {
        self.dragging.is_some()
    }

    /// Get current drag state (if dragging)
    pub fn drag_state(&self) -> Option<&SeparatorDragState> {
        self.dragging.as_ref()
    }
}

impl Default for SeparatorController {
    fn default() -> Self {
        Self::new()
    }
}

// =============================================================================
// Tests
// =============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_separator_hit_test_vertical() {
        let separator = Separator::new(
            SeparatorOrientation::Vertical, 100.0, 0.0, 200.0,
            SeparatorLevel::Node { parent_id: BranchId(0), child_a: 0, child_b: 1 }
        );

        // Hit center
        assert!(separator.hit_test(100.0, 100.0));

        // Hit edges (within 8px hit width)
        assert!(separator.hit_test(96.0, 100.0)); // 4px left
        assert!(separator.hit_test(104.0, 100.0)); // 4px right

        // Miss horizontally
        assert!(!separator.hit_test(90.0, 100.0)); // Too far left
        assert!(!separator.hit_test(110.0, 100.0)); // Too far right

        // Miss vertically
        assert!(!separator.hit_test(100.0, -10.0)); // Above
        assert!(!separator.hit_test(100.0, 210.0)); // Below
    }

    #[test]
    fn test_separator_hit_test_horizontal() {
        let separator = Separator::new(
            SeparatorOrientation::Horizontal, 100.0, 0.0, 200.0,
            SeparatorLevel::Node { parent_id: BranchId(0), child_a: 0, child_b: 1 }
        );

        // Hit center
        assert!(separator.hit_test(100.0, 100.0));

        // Hit edges (within 8px hit width)
        assert!(separator.hit_test(100.0, 96.0)); // 4px above
        assert!(separator.hit_test(100.0, 104.0)); // 4px below

        // Miss vertically
        assert!(!separator.hit_test(100.0, 90.0)); // Too far above
        assert!(!separator.hit_test(100.0, 110.0)); // Too far below

        // Miss horizontally
        assert!(!separator.hit_test(-10.0, 100.0)); // Left
        assert!(!separator.hit_test(210.0, 100.0)); // Right
    }

    #[test]
    fn test_separator_thickness() {
        let mut separator = Separator::new(
            SeparatorOrientation::Vertical, 100.0, 0.0, 200.0,
            SeparatorLevel::Node { parent_id: BranchId(0), child_a: 0, child_b: 1 }
        );

        separator.state = SeparatorState::Idle;
        assert_eq!(separator.thickness_for_state(), 2.0);

        separator.state = SeparatorState::Hover;
        assert_eq!(separator.thickness_for_state(), 4.0);

        separator.state = SeparatorState::Dragging;
        assert_eq!(separator.thickness_for_state(), 4.0);
    }

    #[test]
    fn test_controller_normal_resize() {
        let mut controller = SeparatorController::new();
        let children = vec![NodeId(1), NodeId(2)];
        let min_sizes = vec![100.0, 100.0];

        // Start drag with equal shares (200px each in 400px total)
        controller.start_drag(0, NodeId(10), 200.0, vec![1.0, 1.0]);

        // Move separator right by 50px (left panel grows to 250px, right shrinks to 150px)
        let new_shares = controller.update_drag(50.0, &children, &min_sizes, 400.0);
        assert!(new_shares.is_some());

        let shares = new_shares.unwrap();
        assert_eq!(shares.len(), 2);

        // Verify shares maintain ratio
        let total: f32 = shares.iter().sum();
        assert!((total - 2.0).abs() < 0.01); // Should sum to original total (2.0)

        // Verify pixel sizes
        let left_size = (shares[0] / total) * 400.0;
        let right_size = (shares[1] / total) * 400.0;

        assert!((left_size - 250.0).abs() < 1.0);
        assert!((right_size - 150.0).abs() < 1.0);
    }

    #[test]
    fn test_controller_constraint_violation() {
        let mut controller = SeparatorController::new();
        let children = vec![NodeId(1), NodeId(2)];
        let min_sizes = vec![100.0, 100.0];

        // Start drag with equal shares (200px each in 400px total)
        controller.start_drag(0, NodeId(10), 200.0, vec![1.0, 1.0]);

        // Try to move separator right by 150px (right would be 50px < min 100px)
        let new_shares = controller.update_drag(150.0, &children, &min_sizes, 400.0);

        // Should return None (constraint violated)
        assert!(new_shares.is_none());
    }

    #[test]
    fn test_controller_drag_left() {
        let mut controller = SeparatorController::new();
        let children = vec![NodeId(1), NodeId(2)];
        let min_sizes = vec![100.0, 100.0];

        // Start drag (200px each in 400px total)
        controller.start_drag(0, NodeId(10), 200.0, vec![1.0, 1.0]);

        // Move separator left by 50px (left shrinks to 150px, right grows to 250px)
        let new_shares = controller.update_drag(-50.0, &children, &min_sizes, 400.0);
        assert!(new_shares.is_some());

        let shares = new_shares.unwrap();
        let total: f32 = shares.iter().sum();

        let left_size = (shares[0] / total) * 400.0;
        let right_size = (shares[1] / total) * 400.0;

        assert!((left_size - 150.0).abs() < 1.0);
        assert!((right_size - 250.0).abs() < 1.0);
    }

    #[test]
    fn test_controller_three_panels() {
        let mut controller = SeparatorController::new();
        let children = vec![NodeId(1), NodeId(2), NodeId(3)];
        let min_sizes = vec![100.0, 100.0, 100.0];

        // Start drag with equal shares (200px each in 600px total)
        controller.start_drag(0, NodeId(10), 200.0, vec![1.0, 1.0, 1.0]);

        // Move first separator right by 50px
        // Left grows from 200 to 250, middle shrinks from 200 to 150, right unchanged at 200
        let new_shares = controller.update_drag(50.0, &children, &min_sizes, 600.0);
        assert!(new_shares.is_some());

        let shares = new_shares.unwrap();
        let total: f32 = shares.iter().sum();

        let sizes: Vec<f32> = shares.iter().map(|&s| (s / total) * 600.0).collect();

        assert!((sizes[0] - 250.0).abs() < 1.0); // Left panel
        assert!((sizes[1] - 150.0).abs() < 1.0); // Middle panel
        assert!((sizes[2] - 200.0).abs() < 1.0); // Right panel (unchanged)
    }

    #[test]
    fn test_controller_drag_lifecycle() {
        let mut controller = SeparatorController::new();

        assert!(!controller.is_dragging());

        controller.start_drag(0, NodeId(10), 200.0, vec![1.0, 1.0]);
        assert!(controller.is_dragging());

        controller.end_drag();
        assert!(!controller.is_dragging());
    }
}