fresh/view/
overlay.rs

1use crate::model::marker::{MarkerId, MarkerList};
2use ratatui::style::{Color, Style};
3use std::ops::Range;
4
5// Re-export types from fresh-core for shared type usage
6pub use fresh_core::overlay::{OverlayHandle, OverlayNamespace};
7
8/// Overlay face - defines the visual appearance of an overlay
9#[derive(Debug, Clone, PartialEq)]
10pub enum OverlayFace {
11    /// Underline with a specific style
12    Underline { color: Color, style: UnderlineStyle },
13    /// Background color
14    Background { color: Color },
15    /// Foreground (text) color
16    Foreground { color: Color },
17    /// Combined style with multiple attributes
18    Style { style: Style },
19}
20
21/// Style of underline
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum UnderlineStyle {
24    /// Straight line
25    Straight,
26    /// Wavy/squiggly line (for errors)
27    Wavy,
28    /// Dotted line
29    Dotted,
30    /// Dashed line
31    Dashed,
32}
33
34/// Priority for overlay z-ordering
35/// Higher priority overlays are rendered on top of lower priority ones
36pub type Priority = i32;
37
38/// An overlay represents a visual decoration over a range of text
39/// Uses markers for content-anchored positions that automatically adjust with edits
40#[derive(Debug, Clone)]
41pub struct Overlay {
42    /// Unique handle for this overlay (opaque, for removal by handle)
43    pub handle: OverlayHandle,
44
45    /// Namespace this overlay belongs to (for bulk removal)
46    pub namespace: Option<OverlayNamespace>,
47
48    /// Start marker (left affinity - stays before inserted text)
49    pub start_marker: MarkerId,
50
51    /// End marker (right affinity - moves after inserted text)
52    pub end_marker: MarkerId,
53
54    /// Visual appearance of the overlay
55    pub face: OverlayFace,
56
57    /// Priority for z-ordering (higher = on top)
58    pub priority: Priority,
59
60    /// Optional tooltip/message to show when hovering over this overlay
61    pub message: Option<String>,
62
63    /// Whether to extend the overlay's background to the end of the visual line
64    /// Used for full-width line highlighting (e.g., in diff views)
65    pub extend_to_line_end: bool,
66}
67
68impl Overlay {
69    /// Create a new overlay with markers at the given range
70    ///
71    /// # Arguments
72    /// * `marker_list` - MarkerList to create markers in
73    /// * `range` - Byte range for the overlay
74    /// * `face` - Visual appearance
75    ///
76    /// Returns the overlay (which contains its handle for later removal)
77    pub fn new(marker_list: &mut MarkerList, range: Range<usize>, face: OverlayFace) -> Self {
78        let start_marker = marker_list.create(range.start, true); // left affinity
79        let end_marker = marker_list.create(range.end, false); // right affinity
80
81        Self {
82            handle: OverlayHandle::new(),
83            namespace: None,
84            start_marker,
85            end_marker,
86            face,
87            priority: 0,
88            message: None,
89            extend_to_line_end: false,
90        }
91    }
92
93    /// Create an overlay with a namespace (for bulk removal)
94    pub fn with_namespace(
95        marker_list: &mut MarkerList,
96        range: Range<usize>,
97        face: OverlayFace,
98        namespace: OverlayNamespace,
99    ) -> Self {
100        let mut overlay = Self::new(marker_list, range, face);
101        overlay.namespace = Some(namespace);
102        overlay
103    }
104
105    /// Create an overlay with a specific priority
106    pub fn with_priority(
107        marker_list: &mut MarkerList,
108        range: Range<usize>,
109        face: OverlayFace,
110        priority: Priority,
111    ) -> Self {
112        let mut overlay = Self::new(marker_list, range, face);
113        overlay.priority = priority;
114        overlay
115    }
116
117    /// Add a message/tooltip to this overlay
118    pub fn with_message(mut self, message: String) -> Self {
119        self.message = Some(message);
120        self
121    }
122
123    /// Set the priority
124    pub fn with_priority_value(mut self, priority: Priority) -> Self {
125        self.priority = priority;
126        self
127    }
128
129    /// Set the namespace
130    pub fn with_namespace_value(mut self, namespace: OverlayNamespace) -> Self {
131        self.namespace = Some(namespace);
132        self
133    }
134
135    /// Set whether to extend the overlay to the end of the visual line
136    pub fn with_extend_to_line_end(mut self, extend: bool) -> Self {
137        self.extend_to_line_end = extend;
138        self
139    }
140
141    /// Get the current byte range by resolving markers
142    /// This is called once per frame during rendering setup
143    pub fn range(&self, marker_list: &MarkerList) -> Range<usize> {
144        let start = marker_list.get_position(self.start_marker).unwrap_or(0);
145        let end = marker_list.get_position(self.end_marker).unwrap_or(0);
146        start..end
147    }
148
149    /// Check if this overlay contains a position
150    pub fn contains(&self, position: usize, marker_list: &MarkerList) -> bool {
151        self.range(marker_list).contains(&position)
152    }
153
154    /// Check if this overlay overlaps with a range
155    pub fn overlaps(&self, range: &Range<usize>, marker_list: &MarkerList) -> bool {
156        let self_range = self.range(marker_list);
157        self_range.start < range.end && range.start < self_range.end
158    }
159}
160
161/// Manages overlays for a buffer
162/// Overlays are sorted by priority for efficient rendering
163#[derive(Debug, Clone)]
164pub struct OverlayManager {
165    /// All active overlays, indexed for O(1) lookup by handle
166    overlays: Vec<Overlay>,
167}
168
169impl OverlayManager {
170    /// Create a new empty overlay manager
171    pub fn new() -> Self {
172        Self {
173            overlays: Vec::new(),
174        }
175    }
176
177    /// Add an overlay and return its handle for later removal
178    pub fn add(&mut self, overlay: Overlay) -> OverlayHandle {
179        let handle = overlay.handle.clone();
180        self.overlays.push(overlay);
181        // Keep sorted by priority (ascending - lower priority first)
182        self.overlays.sort_by_key(|o| o.priority);
183        handle
184    }
185
186    /// Remove an overlay by its handle
187    pub fn remove_by_handle(
188        &mut self,
189        handle: &OverlayHandle,
190        marker_list: &mut MarkerList,
191    ) -> bool {
192        if let Some(pos) = self.overlays.iter().position(|o| &o.handle == handle) {
193            let overlay = self.overlays.remove(pos);
194            marker_list.delete(overlay.start_marker);
195            marker_list.delete(overlay.end_marker);
196            true
197        } else {
198            false
199        }
200    }
201
202    /// Remove all overlays in a namespace
203    pub fn clear_namespace(&mut self, namespace: &OverlayNamespace, marker_list: &mut MarkerList) {
204        // Collect markers to delete
205        let markers_to_delete: Vec<_> = self
206            .overlays
207            .iter()
208            .filter(|o| o.namespace.as_ref() == Some(namespace))
209            .flat_map(|o| vec![o.start_marker, o.end_marker])
210            .collect();
211
212        // Remove overlays
213        self.overlays
214            .retain(|o| o.namespace.as_ref() != Some(namespace));
215
216        // Delete markers
217        for marker_id in markers_to_delete {
218            marker_list.delete(marker_id);
219        }
220    }
221
222    /// Replace overlays in a namespace that overlap a range with new overlays.
223    ///
224    /// This preserves overlays outside the range, which helps avoid flicker and
225    /// unnecessary marker churn during viewport-only updates.
226    pub fn replace_range_in_namespace(
227        &mut self,
228        namespace: &OverlayNamespace,
229        range: &Range<usize>,
230        mut new_overlays: Vec<Overlay>,
231        marker_list: &mut MarkerList,
232    ) {
233        let mut markers_to_delete = Vec::new();
234
235        self.overlays.retain(|overlay| {
236            let in_namespace = overlay.namespace.as_ref() == Some(namespace);
237            if in_namespace && overlay.overlaps(range, marker_list) {
238                markers_to_delete.push(overlay.start_marker);
239                markers_to_delete.push(overlay.end_marker);
240                false
241            } else {
242                true
243            }
244        });
245
246        for marker_id in markers_to_delete {
247            marker_list.delete(marker_id);
248        }
249
250        if !new_overlays.is_empty() {
251            self.overlays.append(&mut new_overlays);
252            self.overlays.sort_by_key(|o| o.priority);
253        }
254    }
255
256    /// Remove all overlays in a range and clean up their markers
257    pub fn remove_in_range(&mut self, range: &Range<usize>, marker_list: &mut MarkerList) {
258        // Collect markers to delete
259        let markers_to_delete: Vec<_> = self
260            .overlays
261            .iter()
262            .filter(|o| o.overlaps(range, marker_list))
263            .flat_map(|o| vec![o.start_marker, o.end_marker])
264            .collect();
265
266        // Remove overlays
267        self.overlays.retain(|o| !o.overlaps(range, marker_list));
268
269        // Delete markers
270        for marker_id in markers_to_delete {
271            marker_list.delete(marker_id);
272        }
273    }
274
275    /// Clear all overlays and their markers
276    pub fn clear(&mut self, marker_list: &mut MarkerList) {
277        // Delete all markers
278        for overlay in &self.overlays {
279            marker_list.delete(overlay.start_marker);
280            marker_list.delete(overlay.end_marker);
281        }
282
283        self.overlays.clear();
284    }
285
286    /// Get all overlays at a specific position, sorted by priority
287    pub fn at_position(&self, position: usize, marker_list: &MarkerList) -> Vec<&Overlay> {
288        self.overlays
289            .iter()
290            .filter(|o| {
291                let range = o.range(marker_list);
292                range.contains(&position)
293            })
294            .collect()
295    }
296
297    /// Get all overlays that overlap with a range, sorted by priority
298    pub fn in_range(&self, range: &Range<usize>, marker_list: &MarkerList) -> Vec<&Overlay> {
299        self.overlays
300            .iter()
301            .filter(|o| o.overlaps(range, marker_list))
302            .collect()
303    }
304
305    /// Query overlays in a viewport range efficiently using the marker interval tree
306    ///
307    /// This is much faster than calling `at_position()` for every character in the range.
308    /// Returns overlays with their resolved byte ranges.
309    ///
310    /// # Performance
311    /// - Old approach: O(N * M) where N = positions to check, M = overlay count
312    /// - This approach: O(log M + k) where k = overlays in viewport (typically 2-10)
313    pub fn query_viewport(
314        &self,
315        start: usize,
316        end: usize,
317        marker_list: &MarkerList,
318    ) -> Vec<(&Overlay, Range<usize>)> {
319        use std::collections::HashMap;
320
321        // Query the marker interval tree once for all markers in viewport
322        // This is O(log N + k) where k = markers in viewport
323        let visible_markers = marker_list.query_range(start, end);
324
325        // Build a quick lookup map: marker_id -> position
326        let marker_positions: HashMap<_, _> = visible_markers
327            .into_iter()
328            .map(|(id, start, _end)| (id, start))
329            .collect();
330
331        // Find overlays whose markers are in the viewport
332        // Only resolve positions for overlays that are actually visible
333        self.overlays
334            .iter()
335            .filter_map(|overlay| {
336                // Try to get positions from our viewport query results
337                let start_pos = marker_positions.get(&overlay.start_marker)?;
338                let end_pos = marker_positions.get(&overlay.end_marker)?;
339
340                let range = *start_pos..*end_pos;
341
342                // Only include if actually overlaps viewport
343                if range.start < end && range.end > start {
344                    Some((overlay, range))
345                } else {
346                    None
347                }
348            })
349            .collect()
350    }
351
352    /// Get overlay by handle
353    pub fn get_by_handle(&self, handle: &OverlayHandle) -> Option<&Overlay> {
354        self.overlays.iter().find(|o| &o.handle == handle)
355    }
356
357    /// Get mutable overlay by handle
358    pub fn get_by_handle_mut(&mut self, handle: &OverlayHandle) -> Option<&mut Overlay> {
359        self.overlays.iter_mut().find(|o| &o.handle == handle)
360    }
361
362    /// Get total number of overlays
363    pub fn len(&self) -> usize {
364        self.overlays.len()
365    }
366
367    /// Check if there are any overlays
368    pub fn is_empty(&self) -> bool {
369        self.overlays.is_empty()
370    }
371
372    /// Get all overlays (for rendering)
373    pub fn all(&self) -> &[Overlay] {
374        &self.overlays
375    }
376}
377
378impl Default for OverlayManager {
379    fn default() -> Self {
380        Self::new()
381    }
382}
383
384/// Helper functions for creating common overlay types
385impl Overlay {
386    /// Create an error underline overlay (wavy red line)
387    pub fn error(
388        marker_list: &mut MarkerList,
389        range: Range<usize>,
390        message: Option<String>,
391    ) -> Self {
392        let mut overlay = Self::with_priority(
393            marker_list,
394            range,
395            OverlayFace::Underline {
396                color: Color::Red,
397                style: UnderlineStyle::Wavy,
398            },
399            10, // Higher priority for errors
400        );
401        overlay.message = message;
402        overlay
403    }
404
405    /// Create a warning underline overlay (wavy yellow line)
406    pub fn warning(
407        marker_list: &mut MarkerList,
408        range: Range<usize>,
409        message: Option<String>,
410    ) -> Self {
411        let mut overlay = Self::with_priority(
412            marker_list,
413            range,
414            OverlayFace::Underline {
415                color: Color::Yellow,
416                style: UnderlineStyle::Wavy,
417            },
418            5, // Medium priority for warnings
419        );
420        overlay.message = message;
421        overlay
422    }
423
424    /// Create an info underline overlay (wavy blue line)
425    pub fn info(
426        marker_list: &mut MarkerList,
427        range: Range<usize>,
428        message: Option<String>,
429    ) -> Self {
430        let mut overlay = Self::with_priority(
431            marker_list,
432            range,
433            OverlayFace::Underline {
434                color: Color::Blue,
435                style: UnderlineStyle::Wavy,
436            },
437            3, // Lower priority for info
438        );
439        overlay.message = message;
440        overlay
441    }
442
443    /// Create a hint underline overlay (dotted gray line)
444    pub fn hint(
445        marker_list: &mut MarkerList,
446        range: Range<usize>,
447        message: Option<String>,
448    ) -> Self {
449        let mut overlay = Self::with_priority(
450            marker_list,
451            range,
452            OverlayFace::Underline {
453                color: Color::Gray,
454                style: UnderlineStyle::Dotted,
455            },
456            1, // Lowest priority for hints
457        );
458        overlay.message = message;
459        overlay
460    }
461
462    /// Create a selection highlight overlay
463    pub fn selection(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
464        Self::with_priority(
465            marker_list,
466            range,
467            OverlayFace::Background {
468                color: Color::Rgb(38, 79, 120), // VSCode-like selection color
469            },
470            -10, // Very low priority so it's under other overlays
471        )
472    }
473
474    /// Create a search result highlight overlay
475    pub fn search_match(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
476        Self::with_priority(
477            marker_list,
478            range,
479            OverlayFace::Background {
480                color: Color::Rgb(72, 72, 0), // Yellow-ish highlight
481            },
482            -5, // Low priority
483        )
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[test]
492    fn test_overlay_creation_with_markers() {
493        let mut marker_list = MarkerList::new();
494        marker_list.set_buffer_size(100);
495
496        let overlay = Overlay::new(
497            &mut marker_list,
498            5..10,
499            OverlayFace::Background { color: Color::Red },
500        );
501
502        assert_eq!(marker_list.get_position(overlay.start_marker), Some(5));
503        assert_eq!(marker_list.get_position(overlay.end_marker), Some(10));
504        assert_eq!(overlay.range(&marker_list), 5..10);
505    }
506
507    #[test]
508    fn test_overlay_adjusts_with_insert() {
509        let mut marker_list = MarkerList::new();
510        marker_list.set_buffer_size(100);
511
512        let overlay = Overlay::new(
513            &mut marker_list,
514            10..20,
515            OverlayFace::Background { color: Color::Red },
516        );
517
518        // Insert before overlay
519        marker_list.adjust_for_insert(5, 10);
520
521        // Overlay should have moved forward
522        assert_eq!(overlay.range(&marker_list), 20..30);
523    }
524
525    #[test]
526    fn test_overlay_adjusts_with_delete() {
527        let mut marker_list = MarkerList::new();
528        marker_list.set_buffer_size(100);
529
530        let overlay = Overlay::new(
531            &mut marker_list,
532            20..30,
533            OverlayFace::Background { color: Color::Red },
534        );
535
536        // Delete before overlay
537        marker_list.adjust_for_delete(5, 10);
538
539        // Overlay should have moved backward
540        assert_eq!(overlay.range(&marker_list), 10..20);
541    }
542
543    #[test]
544    fn test_overlay_manager_add_remove() {
545        let mut marker_list = MarkerList::new();
546        marker_list.set_buffer_size(100);
547        let mut manager = OverlayManager::new();
548
549        let overlay = Overlay::new(
550            &mut marker_list,
551            5..10,
552            OverlayFace::Background { color: Color::Red },
553        );
554
555        let handle = manager.add(overlay);
556        assert_eq!(manager.len(), 1);
557
558        manager.remove_by_handle(&handle, &mut marker_list);
559        assert_eq!(manager.len(), 0);
560    }
561
562    #[test]
563    fn test_overlay_namespace_clear() {
564        let mut marker_list = MarkerList::new();
565        marker_list.set_buffer_size(100);
566        let mut manager = OverlayManager::new();
567
568        let ns = OverlayNamespace::from_string("todo".to_string());
569
570        // Add overlays in namespace
571        let overlay1 = Overlay::with_namespace(
572            &mut marker_list,
573            5..10,
574            OverlayFace::Background { color: Color::Red },
575            ns.clone(),
576        );
577        let overlay2 = Overlay::with_namespace(
578            &mut marker_list,
579            15..20,
580            OverlayFace::Background { color: Color::Blue },
581            ns.clone(),
582        );
583        // Add overlay without namespace
584        let overlay3 = Overlay::new(
585            &mut marker_list,
586            25..30,
587            OverlayFace::Background {
588                color: Color::Green,
589            },
590        );
591
592        manager.add(overlay1);
593        manager.add(overlay2);
594        manager.add(overlay3);
595        assert_eq!(manager.len(), 3);
596
597        // Clear only the namespace
598        manager.clear_namespace(&ns, &mut marker_list);
599        assert_eq!(manager.len(), 1); // Only overlay3 remains
600    }
601
602    #[test]
603    fn test_overlay_priority_sorting() {
604        let mut marker_list = MarkerList::new();
605        marker_list.set_buffer_size(100);
606        let mut manager = OverlayManager::new();
607
608        manager.add(Overlay::with_priority(
609            &mut marker_list,
610            5..10,
611            OverlayFace::Background { color: Color::Red },
612            10,
613        ));
614        manager.add(Overlay::with_priority(
615            &mut marker_list,
616            5..10,
617            OverlayFace::Background { color: Color::Blue },
618            5,
619        ));
620        manager.add(Overlay::with_priority(
621            &mut marker_list,
622            5..10,
623            OverlayFace::Background {
624                color: Color::Green,
625            },
626            15,
627        ));
628
629        let overlays = manager.at_position(7, &marker_list);
630        assert_eq!(overlays.len(), 3);
631        // Should be sorted by priority (low to high)
632        assert_eq!(overlays[0].priority, 5);
633        assert_eq!(overlays[1].priority, 10);
634        assert_eq!(overlays[2].priority, 15);
635    }
636
637    #[test]
638    fn test_overlay_contains_and_overlaps() {
639        let mut marker_list = MarkerList::new();
640        marker_list.set_buffer_size(100);
641
642        let overlay = Overlay::new(
643            &mut marker_list,
644            10..20,
645            OverlayFace::Background { color: Color::Red },
646        );
647
648        assert!(!overlay.contains(9, &marker_list));
649        assert!(overlay.contains(10, &marker_list));
650        assert!(overlay.contains(15, &marker_list));
651        assert!(overlay.contains(19, &marker_list));
652        assert!(!overlay.contains(20, &marker_list));
653
654        assert!(!overlay.overlaps(&(0..10), &marker_list));
655        assert!(overlay.overlaps(&(5..15), &marker_list));
656        assert!(overlay.overlaps(&(15..25), &marker_list));
657        assert!(!overlay.overlaps(&(20..30), &marker_list));
658    }
659}