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