git_status_ui.rs (5240B)
1 use crate::git_status::GitStatusCache; 2 use egui::{Color32, RichText, Ui}; 3 4 const MODIFIED_COLOR: Color32 = Color32::from_rgb(200, 170, 50); 5 const ADDED_COLOR: Color32 = Color32::from_rgb(60, 180, 60); 6 const DELETED_COLOR: Color32 = Color32::from_rgb(200, 60, 60); 7 const UNTRACKED_COLOR: Color32 = Color32::from_rgb(128, 128, 128); 8 9 /// Snapshot of git status data extracted from the cache to avoid 10 /// borrow conflicts when mutating `cache.expanded`. 11 pub struct StatusSnapshot { 12 branch: Option<String>, 13 modified: usize, 14 added: usize, 15 deleted: usize, 16 untracked: usize, 17 is_clean: bool, 18 files: Vec<(String, String)>, // (status, path) 19 } 20 21 impl StatusSnapshot { 22 pub fn from_cache(cache: &GitStatusCache) -> Option<Result<Self, ()>> { 23 match cache.current() { 24 Some(Ok(data)) => Some(Ok(StatusSnapshot { 25 branch: data.branch.clone(), 26 modified: data.modified_count(), 27 added: data.added_count(), 28 deleted: data.deleted_count(), 29 untracked: data.untracked_count(), 30 is_clean: data.is_clean(), 31 files: data 32 .files 33 .iter() 34 .map(|f| (f.status.clone(), f.path.clone())) 35 .collect(), 36 })), 37 Some(Err(_)) => Some(Err(())), 38 None => None, 39 } 40 } 41 } 42 43 fn count_label(ui: &mut Ui, prefix: &str, count: usize, color: Color32) { 44 if count > 0 { 45 ui.label( 46 RichText::new(format!("{}{}", prefix, count)) 47 .color(color) 48 .monospace() 49 .size(11.0), 50 ); 51 } 52 } 53 54 fn status_color(status: &str) -> Color32 { 55 if status.starts_with('?') { 56 UNTRACKED_COLOR 57 } else if status.contains('D') { 58 DELETED_COLOR 59 } else if status.contains('A') { 60 ADDED_COLOR 61 } else { 62 MODIFIED_COLOR 63 } 64 } 65 66 /// Render the left-side git status content (expand arrow, branch, counts). 67 pub fn git_status_content_ui( 68 cache: &mut GitStatusCache, 69 snapshot: &Option<Result<StatusSnapshot, ()>>, 70 ui: &mut Ui, 71 ) { 72 match snapshot { 73 Some(Ok(snap)) => { 74 // Show expand arrow only when dirty 75 if !snap.is_clean { 76 let arrow = if cache.expanded { 77 "\u{25BC}" 78 } else { 79 "\u{25B6}" 80 }; 81 if ui 82 .add( 83 egui::Label::new(RichText::new(arrow).weak().monospace().size(9.0)) 84 .sense(egui::Sense::click()), 85 ) 86 .clicked() 87 { 88 cache.expanded = !cache.expanded; 89 } 90 } 91 92 // Branch name 93 let branch_text = snap.branch.as_deref().unwrap_or("detached"); 94 ui.label(RichText::new(branch_text).weak().monospace().size(11.0)); 95 96 if snap.is_clean { 97 ui.label(RichText::new("clean").weak().size(11.0)); 98 } else { 99 count_label(ui, "~", snap.modified, MODIFIED_COLOR); 100 count_label(ui, "+", snap.added, ADDED_COLOR); 101 count_label(ui, "-", snap.deleted, DELETED_COLOR); 102 count_label(ui, "?", snap.untracked, UNTRACKED_COLOR); 103 } 104 } 105 Some(Err(_)) => { 106 ui.label(RichText::new("git: not available").weak().size(11.0)); 107 } 108 None => { 109 ui.spinner(); 110 ui.label(RichText::new("checking git...").weak().size(11.0)); 111 } 112 } 113 } 114 115 /// Render the expanded file list portion of git status. 116 pub fn git_expanded_files_ui( 117 cache: &GitStatusCache, 118 snapshot: &Option<Result<StatusSnapshot, ()>>, 119 ui: &mut Ui, 120 ) { 121 if cache.expanded { 122 if let Some(Ok(snap)) = snapshot { 123 if !snap.files.is_empty() { 124 ui.add_space(4.0); 125 126 egui::Frame::new() 127 .fill(ui.visuals().extreme_bg_color) 128 .inner_margin(egui::Margin::symmetric(8, 4)) 129 .corner_radius(4.0) 130 .show(ui, |ui| { 131 egui::ScrollArea::vertical() 132 .max_height(150.0) 133 .show(ui, |ui| { 134 for (status, path) in &snap.files { 135 ui.horizontal(|ui| { 136 ui.spacing_mut().item_spacing.x = 8.0; 137 let color = status_color(status); 138 ui.label( 139 RichText::new(status) 140 .monospace() 141 .size(11.0) 142 .color(color), 143 ); 144 ui.label(RichText::new(path).monospace().size(11.0).weak()); 145 }); 146 } 147 }); 148 }); 149 } 150 } 151 } 152 }