1use saudade::{
10 Color, Event, EventCtx, Key, MouseButton, NamedKey, Painter, Rect, SCROLLBAR_THICKNESS,
11 ScrollBar, Theme, Widget,
12};
13
14use crate::backend::{Diff, DiffLineKind};
15
16const TEXT_PAD_X: i32 = 4;
17const TEXT_PAD_Y: i32 = 2;
18
19const ADD_BG: Color = Color::rgb(0xDC, 0xFF, 0xDC);
21const ADD_FG: Color = Color::rgb(0x00, 0x64, 0x00);
22const DEL_BG: Color = Color::rgb(0xFF, 0xDC, 0xDC);
23const DEL_FG: Color = Color::rgb(0x90, 0x00, 0x00);
24const HUNK_BG: Color = Color::rgb(0xE2, 0xE8, 0xFF);
25const HUNK_FG: Color = Color::rgb(0x00, 0x00, 0x80);
26const COMMIT_BG: Color = Color::rgb(0xFF, 0xF6, 0xCC);
27const COMMIT_FG: Color = Color::rgb(0x40, 0x30, 0x00);
28const FILE_BG: Color = Color::rgb(0xE6, 0xE6, 0xE6);
29const FILE_FG: Color = Color::rgb(0x00, 0x00, 0x00);
30const META_FG: Color = Color::rgb(0x80, 0x80, 0x80);
31const CONTEXT_FG: Color = Color::rgb(0x20, 0x20, 0x20);
32
33pub struct DiffView {
35 rect: Rect,
36 diff: Diff,
37 v_scrollbar: ScrollBar,
38 focused: bool,
39 font_size: f32,
40}
41
42impl DiffView {
43 pub fn new(rect: Rect) -> Self {
44 let mut me = Self {
45 rect,
46 diff: Diff::default(),
47 v_scrollbar: ScrollBar::vertical(Rect::new(0, 0, 0, 0)),
48 focused: false,
49 font_size: 12.0,
50 };
51 me.relayout_scrollbar();
52 me
53 }
54
55 pub fn with_font_size(mut self, size: f32) -> Self {
56 self.font_size = size;
57 self
58 }
59
60 pub fn set_diff(&mut self, diff: Diff) {
62 self.diff = diff;
63 self.v_scrollbar.set_value(0);
64 self.sync_scrollbar();
65 }
66
67 pub fn is_empty(&self) -> bool {
68 self.diff.is_empty()
69 }
70
71 fn line_height(&self) -> i32 {
72 (self.font_size as i32 + 4).max(8)
73 }
74
75 fn text_area(&self) -> Rect {
76 let sb_w = if self.v_scrollbar.rect().w > 0 {
77 SCROLLBAR_THICKNESS
78 } else {
79 0
80 };
81 Rect::new(
82 self.rect.x,
83 self.rect.y,
84 (self.rect.w - sb_w).max(0),
85 self.rect.h,
86 )
87 }
88
89 fn visible_rows(&self) -> i32 {
90 ((self.text_area().h - TEXT_PAD_Y * 2) / self.line_height()).max(1)
91 }
92
93 fn scroll_top(&self) -> usize {
94 self.v_scrollbar.value().max(0) as usize
95 }
96
97 fn sync_scrollbar(&mut self) {
98 let visible = self.visible_rows();
99 let max_scroll = (self.diff.lines.len() as i32 - visible).max(0);
100 self.v_scrollbar.set_range(visible, max_scroll);
101 self.v_scrollbar.set_line_step(1);
102 }
103
104 fn relayout_scrollbar(&mut self) {
105 let sb_rect = Rect::new(
106 self.rect.right() - SCROLLBAR_THICKNESS,
107 self.rect.y,
108 SCROLLBAR_THICKNESS,
109 self.rect.h,
110 );
111 self.v_scrollbar.set_rect(sb_rect);
112 self.sync_scrollbar();
113 }
114
115 fn scroll_by(&mut self, delta: i32) {
116 let v = self.v_scrollbar.value();
117 self.v_scrollbar.set_value(v + delta);
118 }
119}
120
121impl Widget for DiffView {
122 fn bounds(&self) -> Rect {
123 self.rect
124 }
125
126 fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
127 self.sync_scrollbar();
128 let text = self.text_area();
129 painter.fill_rect(text, Color::WHITE);
130 painter.sunken_bevel(text, theme.highlight, theme.shadow);
131 painter.stroke_rect(text, theme.border);
132
133 let line_h = self.line_height();
134 let text_x = text.x + TEXT_PAD_X;
135 let text_y0 = text.y + TEXT_PAD_Y;
136 let row_w = (text.w - TEXT_PAD_X).max(0);
137 let visible = self.visible_rows() as usize;
138 let scroll_top = self.scroll_top();
139
140 let saved = painter.push_clip(text.inset(1));
142 for row_offset in 0..visible {
143 let row = scroll_top + row_offset;
144 let Some(line) = self.diff.lines.get(row) else {
145 break;
146 };
147 let y = text_y0 + row_offset as i32 * line_h;
148 let (fg, bg) = colors_for(line.kind);
149 if let Some(bg) = bg {
150 painter.fill_rect(Rect::new(text.x + 1, y, row_w, line_h), bg);
151 }
152 let label_y = y + (line_h - self.font_size as i32) / 2 - 1;
153 painter.mono_text(text_x, label_y, &line.text, self.font_size, fg);
154 }
155 painter.restore_clip(saved);
156
157 self.v_scrollbar.paint(painter, theme);
158 }
159
160 fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
161 if self.v_scrollbar.captures_pointer() {
163 self.v_scrollbar.event(event, ctx);
164 return;
165 }
166 if let Some(pos) = event.position()
167 && self.v_scrollbar.rect().contains(pos)
168 {
169 self.v_scrollbar.event(event, ctx);
170 return;
171 }
172
173 match event {
174 Event::PointerDown {
175 button: MouseButton::Left,
176 ..
177 } => {
178 ctx.request_focus();
179 ctx.request_paint();
180 }
181 Event::KeyDown { key, modifiers } if self.focused && !modifiers.has_command() => {
182 let page = (self.visible_rows() - 1).max(1);
183 let consumed = match key {
184 Key::Named(NamedKey::Up) => {
185 self.scroll_by(-1);
186 true
187 }
188 Key::Named(NamedKey::Down) => {
189 self.scroll_by(1);
190 true
191 }
192 Key::Named(NamedKey::PageUp) => {
193 self.scroll_by(-page);
194 true
195 }
196 Key::Named(NamedKey::PageDown) => {
197 self.scroll_by(page);
198 true
199 }
200 Key::Named(NamedKey::Home) => {
201 self.v_scrollbar.set_value(0);
202 true
203 }
204 Key::Named(NamedKey::End) => {
205 self.v_scrollbar.set_value(self.diff.lines.len() as i32);
206 true
207 }
208 _ => false,
209 };
210 if consumed {
211 ctx.request_paint();
212 }
213 }
214 _ => {}
215 }
216 }
217
218 fn captures_pointer(&self) -> bool {
219 self.v_scrollbar.captures_pointer()
220 }
221
222 fn focusable(&self) -> bool {
223 true
224 }
225
226 fn set_focused(&mut self, focused: bool) {
227 self.focused = focused;
228 }
229
230 fn layout(&mut self, bounds: Rect) {
231 self.rect = bounds;
232 self.relayout_scrollbar();
233 }
234}
235
236fn colors_for(kind: DiffLineKind) -> (Color, Option<Color>) {
238 match kind {
239 DiffLineKind::CommitHeader => (COMMIT_FG, Some(COMMIT_BG)),
240 DiffLineKind::Addition => (ADD_FG, Some(ADD_BG)),
241 DiffLineKind::Deletion => (DEL_FG, Some(DEL_BG)),
242 DiffLineKind::HunkHeader => (HUNK_FG, Some(HUNK_BG)),
243 DiffLineKind::FileHeader => (FILE_FG, Some(FILE_BG)),
244 DiffLineKind::Meta => (META_FG, None),
245 DiffLineKind::Context => (CONTEXT_FG, None),
246 }
247}