Skip to main content

journey/widgets/
diff_pane.rs

1//! A diff pane that shows either a text diff or a graphical image diff.
2//!
3//! Most files diff as text in a [`DiffView`]; image files instead show an
4//! [`ImageDiffView`] comparing the two versions visually. `DiffPane` owns both
5//! and shows whichever the current selection calls for, so [`crate::ui`] can
6//! treat the diff pane as a single widget (one Tab stop, one layout rect, one
7//! handle) and just call [`set_diff`](Self::set_diff) for text or
8//! [`show_image`](Self::show_image) for an image. Every [`Widget`] method
9//! delegates to the active inner view.
10
11use saudade::{Event, EventCtx, Painter, PopupRequest, Rect, Theme, Widget};
12
13use crate::backend::Diff;
14use crate::imagediff::ImageComparison;
15use crate::widgets::{DiffMode, DiffView, ImageDiffView};
16
17/// A diff pane backed by a text view and an image view, one shown at a time.
18pub struct DiffPane {
19    text: DiffView,
20    image: ImageDiffView,
21    showing_image: bool,
22    focused: bool,
23}
24
25impl DiffPane {
26    pub fn new(rect: Rect) -> Self {
27        Self {
28            text: DiffView::new(rect),
29            image: ImageDiffView::new(rect),
30            showing_image: false,
31            focused: false,
32        }
33    }
34
35    pub fn with_font_size(mut self, size: f32) -> Self {
36        self.text = self.text.with_font_size(size);
37        self.image = self.image.with_font_size(size);
38        self
39    }
40
41    /// Show `diff` as a text diff, switching away from the image view.
42    pub fn set_diff(&mut self, diff: Diff) {
43        self.text.set_diff(diff);
44        self.set_showing_image(false);
45    }
46
47    /// Show a graphical comparison of an image file's two versions.
48    pub fn show_image(&mut self, comparison: ImageComparison) {
49        self.image.set_comparison(Some(comparison));
50        self.set_showing_image(true);
51    }
52
53    /// Whether the graphical image view is currently shown (vs. the text diff).
54    pub fn showing_image(&self) -> bool {
55        self.showing_image
56    }
57
58    /// Cycle the image comparison mode (View ▸ Switch Mode). No-op unless an
59    /// image is currently shown.
60    pub fn cycle_image_mode(&mut self) {
61        if self.showing_image {
62            self.image.cycle_mode();
63        }
64    }
65
66    /// Show the "before" (old) or "after" (new) side of the image at full size
67    /// (View ▸ Before / After Image). No-op unless an image is currently shown.
68    pub fn show_image_side(&mut self, before: bool) {
69        if self.showing_image {
70            self.image.show_side(before);
71        }
72    }
73
74    /// Set the text view's staging mode (no-op while an image is shown — the
75    /// image view has no line-range selection).
76    pub fn set_mode(&mut self, mode: DiffMode) {
77        self.text.set_mode(mode);
78    }
79
80    /// Take a pending partial-stage request from the text view; always `None`
81    /// while an image is shown.
82    pub fn take_action(&mut self) -> Option<(usize, usize)> {
83        if self.showing_image {
84            None
85        } else {
86            self.text.take_action()
87        }
88    }
89
90    pub fn is_empty(&self) -> bool {
91        if self.showing_image {
92            self.image.is_empty()
93        } else {
94            self.text.is_empty()
95        }
96    }
97
98    /// Switch the active view, moving keyboard focus to the newly-shown one so
99    /// it responds immediately without waiting for a re-focus.
100    fn set_showing_image(&mut self, showing_image: bool) {
101        if showing_image == self.showing_image {
102            return;
103        }
104        self.showing_image = showing_image;
105        if self.focused {
106            self.text.set_focused(!showing_image);
107            self.image.set_focused(showing_image);
108        }
109    }
110
111    fn active(&self) -> &dyn Widget {
112        if self.showing_image {
113            &self.image
114        } else {
115            &self.text
116        }
117    }
118
119    fn active_mut(&mut self) -> &mut dyn Widget {
120        if self.showing_image {
121            &mut self.image
122        } else {
123            &mut self.text
124        }
125    }
126}
127
128impl Widget for DiffPane {
129    fn bounds(&self) -> Rect {
130        self.active().bounds()
131    }
132
133    fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
134        self.active_mut().paint(painter, theme);
135    }
136
137    fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
138        self.active_mut().event(event, ctx);
139    }
140
141    fn captures_pointer(&self) -> bool {
142        self.active().captures_pointer()
143    }
144
145    fn focusable(&self) -> bool {
146        self.active().focusable()
147    }
148
149    fn set_focused(&mut self, focused: bool) {
150        self.focused = focused;
151        self.active_mut().set_focused(focused);
152    }
153
154    fn focus_first(&mut self) -> bool {
155        self.focused = true;
156        self.active_mut().focus_first()
157    }
158
159    fn layout(&mut self, bounds: Rect) {
160        // Lay out both, so the inactive view is correctly sized the instant it
161        // is shown.
162        self.text.layout(bounds);
163        self.image.layout(bounds);
164    }
165
166    fn wants_ticks(&self) -> bool {
167        self.active().wants_ticks()
168    }
169
170    fn popup_request(&self) -> Option<PopupRequest> {
171        self.active().popup_request()
172    }
173}