Skip to main content

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