Skip to main content

branchdiff/ui/
mod.rs

1use ratatui::{
2    layout::{Constraint, Direction, Layout},
3    Frame,
4};
5
6use crate::app::{App, FrameContext};
7
8pub mod colors;
9pub mod diff_view;
10pub mod image_view;
11pub mod modals;
12pub mod selection;
13pub mod spans;
14pub mod status_bar;
15pub mod wrapping;
16
17// Re-export commonly used items
18pub use modals::{draw_help_modal, draw_warning_banner};
19pub use status_bar::{draw_status_bar, status_bar_height, status_bar_plain_text};
20
21/// Width of the prefix after line numbers: prefix char + space + status symbol + trailing space
22pub const PREFIX_CHAR_WIDTH: usize = 4;
23
24/// Represents how a logical DiffLine maps to a screen row
25#[derive(Debug, Clone)]
26pub struct ScreenRowInfo {
27    /// The actual text content of this screen row (for copy operations)
28    pub content: String,
29    /// Whether this row is a file header (for collapse detection)
30    pub is_file_header: bool,
31    /// The file path this row belongs to (for collapse toggle)
32    pub file_path: Option<String>,
33    /// Whether this row is a continuation of a wrapped line (not start of new logical line)
34    pub is_continuation: bool,
35}
36
37/// Draw the main UI with a pre-computed frame context
38pub fn draw_with_frame(frame: &mut Frame, app: &mut App, ctx: &FrameContext) {
39    let size = frame.area();
40
41    let has_warning = app.conflict_warning.is_some() || app.error.is_some();
42    let status_height = status_bar_height(app, size.width);
43
44    let chunks = Layout::default()
45        .direction(Direction::Vertical)
46        .constraints(if has_warning {
47            vec![
48                Constraint::Length(1),
49                Constraint::Min(1),
50                Constraint::Length(status_height),
51            ]
52        } else {
53            vec![
54                Constraint::Min(1),
55                Constraint::Length(status_height),
56            ]
57        })
58        .split(size);
59
60    let (warning_area, diff_area, status_area) = if has_warning {
61        (Some(chunks[0]), chunks[1], chunks[2])
62    } else {
63        (None, chunks[0], chunks[1])
64    };
65
66    if let Some(area) = warning_area {
67        if let Some(error) = &app.error {
68            draw_warning_banner(frame, error, area);
69        } else if let Some(warning) = &app.conflict_warning {
70            draw_warning_banner(frame, warning, area);
71        }
72    }
73
74    let search_bar_rows = u16::from(app.search.is_some());
75    let content_height = diff_area.height.saturating_sub(2 + search_bar_rows) as usize;
76    app.set_viewport_height(content_height);
77
78    diff_view::draw_diff_view_with_frame(frame, app, diff_area, ctx);
79    draw_status_bar(frame, app, status_area);
80
81    // Store status bar text and position for selection support
82    app.view.status_bar_lines = status_bar_plain_text(app, status_area.width);
83    app.view.status_bar_screen_y = status_area.y;
84
85    if app.view.show_help {
86        draw_help_modal(frame, size, app);
87    }
88}