#[doc(inline)]
pub use inflorescence_model::diff::{
contents_to_lines, Combined, DecodedFile, DiffWithContents,
DiffWithoutContents, File, FileAndState, IdHash, Lines, Section, State,
UndecodableContents, UndecodableFile,
};
use crate::view::Msg;
use crate::{checkbox, el, theme, Theme};
use inflorescence_iced_widget::{nav_scrollable, nav_selectable};
use inflorescence_model::to_record;
use libflorescence::file;
use iced::widget::{column, container, image, row, text};
use iced::{alignment, Element, Font, Length};
use std::cmp;
pub fn view<'a>(
state: &'a State,
nav: &'a nav_scrollable::State,
file: &'a File,
path: Option<&'a file::Path>,
diff_selected: bool,
select_sections: bool,
to_record: &to_record::State,
) -> Element<'a, Msg, Theme> {
match file {
File::Decoded(decoded_file) => view_decoded(
state,
nav,
decoded_file,
path,
diff_selected,
select_sections,
to_record,
),
File::Undecodable(undecodable_file) => {
view_undecodable(state, nav, undecodable_file, diff_selected)
}
File::Image(bytes) => {
el(image(image::Handle::from_bytes(bytes.clone())))
}
}
}
fn view_decoded<'a>(
_state: &'a State,
nav: &'a nav_scrollable::State,
file: &'a DecodedFile,
path: Option<&'a file::Path>,
diff_selected: bool,
select_sections: bool,
to_record: &to_record::State,
) -> Element<'a, Msg, Theme> {
let DecodedFile {
combined,
diffs_without_contents,
} = file;
let line_num_digits = combined.max_line_num.to_string().len();
let mut current_line = 1;
const CHECKBOX_SPACING: u32 = 4;
let sections_view = combined.sections.iter().map(|section| match section {
Section::Unchanged(lines) => {
let res = lines.iter().enumerate().map(move |(ix, line)| {
line_view(
LineKind::Unchanged,
current_line + ix,
line_num_digits,
line,
)
});
current_line += lines.len();
if path.is_some() {
let toggle_space =
el(container(text("")).width(checkbox::WIDTH));
el(row([toggle_space, el(column(res))])
.spacing(CHECKBOX_SPACING))
} else {
el(column(res))
}
}
Section::Changed {
deleted,
added,
diff_id,
} => {
let res = deleted
.iter()
.enumerate()
.map(move |(ix, line)| {
line_view(
LineKind::Deleted,
current_line + ix,
line_num_digits,
line,
)
})
.chain(added.iter().enumerate().map(move |(ix, line)| {
line_view(
LineKind::Added,
current_line + ix,
line_num_digits,
line,
)
}));
current_line += added.len();
if let Some(path) = path {
let to_record_pick =
to_record::determine_change(path, *diff_id, to_record);
let to_record_toggle = el(checkbox::two_way(to_record_pick)
.on_press_with(|| {
Msg::ToRecord(to_record::Msg::ToggleChange {
path: path.clone(),
diff_id: *diff_id,
})
}));
el(row([to_record_toggle, el(column(res))])
.spacing(CHECKBOX_SPACING))
} else {
el(column(res))
}
}
});
let nav_view = if diff_selected && select_sections {
nav_selectable(
nav,
sections_view,
|| theme::Container::NavNonSelectedSection,
|| theme::Container::NavSelectedSection,
)
} else {
nav_scrollable(nav, sections_view)
}
.class(if diff_selected {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
});
let sections = el(nav_view);
let nav_with_sections = if diffs_without_contents.is_empty() {
sections
} else {
let diffs_without_contents_view = diffs_without_contents
.iter()
.map(view_diff_without_contents);
el(column([el(column(diffs_without_contents_view)), sections])
.spacing(10))
};
if select_sections {
let selected_hunk_ix = nav.get_selected_section_ix().unwrap_or(0) + 1;
let hunk_count = combined
.sections
.iter()
.filter(|section| matches!(section, Section::Changed { .. }))
.count();
el(column([
el(text(format!(
"Selected hunk: {}/{}",
selected_hunk_ix, hunk_count
))),
nav_with_sections,
])
.spacing(10))
} else {
nav_with_sections
}
}
fn view_undecodable<'a>(
_state: &'a State,
nav: &'a nav_scrollable::State,
file: &'a UndecodableFile,
diff_selected: bool,
) -> Element<'a, Msg, Theme> {
let UndecodableFile {
diffs_with_contents,
diffs_without_contents,
} = file;
let diffs = diffs_with_contents
.iter()
.map(view_diff_with_contents)
.chain(
diffs_without_contents
.iter()
.map(view_diff_without_contents),
);
el(column([
el(text("Cannot display contents due to unknown encoding")),
el(nav_scrollable(nav, diffs).class(if diff_selected {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})),
]))
}
fn view_diff_with_contents<'a>(
(diff, _id): &(DiffWithContents, IdHash),
) -> Element<'a, Msg, Theme> {
match diff {
DiffWithContents::Add { contents } => {
if let Some(contents) = contents {
let line_num = 1;
let lines = contents_to_lines(contents);
let max_line_num = line_num + lines.len();
let line_num_digits = max_line_num.to_string().len();
let lines_view =
lines.into_iter().enumerate().map(|(ix, line)| {
line_view(
LineKind::Added,
line_num + ix,
line_num_digits,
line,
)
});
el(column(lines_view))
} else {
el(text("Added"))
}
}
DiffWithContents::Edit {
line,
deleted,
contents,
} => {
let line_num = *line;
let lines = contents_to_lines(contents);
let max_line_num = line_num + lines.len();
let line_num_digits = max_line_num.to_string().len();
let lines_view = lines.into_iter().enumerate().map(|(ix, line)| {
line_view(
if *deleted {
LineKind::Deleted
} else {
LineKind::Added
},
line_num + ix,
line_num_digits,
line,
)
});
el(column(lines_view))
}
DiffWithContents::Replacement {
line,
change_contents,
replacement_contents,
} => {
let line_num = *line;
let change_lines = contents_to_lines(change_contents);
let replacement_lines = contents_to_lines(replacement_contents);
let max_line_num = line_num
+ cmp::max(change_lines.len(), replacement_lines.len());
let line_num_digits = max_line_num.to_string().len();
let lines_view = change_lines
.into_iter()
.enumerate()
.map(|(ix, line)| {
line_view(
LineKind::Deleted,
line_num + ix,
line_num_digits,
line,
)
})
.chain(replacement_lines.into_iter().enumerate().map(
|(ix, line)| {
line_view(
LineKind::Added,
line_num + ix,
line_num_digits,
line,
)
},
));
el(column(lines_view))
}
DiffWithContents::Del { contents } => {
if let Some(contents) = contents {
let line_num = 1;
let lines = contents_to_lines(contents);
let max_line_num = line_num + lines.len();
let line_num_digits = max_line_num.to_string().len();
let lines_view =
lines.into_iter().enumerate().map(|(ix, line)| {
line_view(
LineKind::Deleted,
line_num + ix,
line_num_digits,
line,
)
});
el(column(lines_view))
} else {
el(text("Deleted"))
}
}
DiffWithContents::Undel => el(text("Revived")),
}
}
fn view_diff_without_contents<'a>(
diff: &'a DiffWithoutContents,
) -> Element<'a, Msg, Theme> {
match diff {
DiffWithoutContents::SolveNameConflict => {
el(text("Solve name conflict"))
}
DiffWithoutContents::UnsolveNameConflict => {
el(text("Unsolve name conflict"))
}
DiffWithoutContents::SolveOrderConflict => {
el(text("Solve order conflict"))
}
DiffWithoutContents::UnsolveOrderConflict => {
el(text("Unsolve order conflict"))
}
DiffWithoutContents::ResurrectZombines => el(text("Resurrect zombies")),
DiffWithoutContents::AddRoot => el(text("Add root")),
DiffWithoutContents::DelRoot => el(text("Delete root")),
DiffWithoutContents::Edit {
line,
deleted,
contents,
} => {
let line_num = *line;
let line = undecodable_contents_to_str(contents);
line_view(
if *deleted {
LineKind::Deleted
} else {
LineKind::Added
},
line_num,
1,
line,
)
}
DiffWithoutContents::Replacement {
line,
change_contents,
replacement_contents,
} => {
let line_num = *line;
let change_line = undecodable_contents_to_str(change_contents);
let replacement_line =
undecodable_contents_to_str(replacement_contents);
el(column([
line_view(LineKind::Deleted, line_num, 1, change_line),
line_view(LineKind::Added, line_num, 1, replacement_line),
]))
}
DiffWithoutContents::Move { old_path } => {
el(text(format!("Moved from {old_path}")))
}
DiffWithoutContents::MoveEdge => el(text("Moved".to_string())),
DiffWithoutContents::Del => el(text("Deleted file")),
}
}
fn mono_text<'a>(
txt: impl text::IntoFragment<'a>,
) -> iced::widget::Text<'a, Theme> {
text(txt)
.font(Font::MONOSPACE)
.wrapping(text::Wrapping::WordOrGlyph)
.align_y(alignment::Vertical::Top)
}
fn line_num_view<'a>(
num: usize,
digits: usize,
) -> iced::widget::Text<'a, Theme> {
let txt = format!("{num:digits$} ");
mono_text(txt)
.font(Font::MONOSPACE)
.align_y(alignment::Vertical::Top)
}
#[derive(Debug, Clone, Copy)]
enum LineKind {
Unchanged,
Added,
Deleted,
}
fn line_view<'a>(
kind: LineKind,
line_num: usize,
line_num_digits: usize,
line: impl text::IntoFragment<'a>,
) -> Element<'a, Msg, Theme> {
let line = container(row([
el(mono_text(match kind {
LineKind::Unchanged => " ",
LineKind::Added => "+ ",
LineKind::Deleted => "- ",
})),
el(line_num_view(line_num, line_num_digits)),
el(mono_text(line)
.shaping(text::Shaping::Advanced)
.width(Length::Fill)),
]));
el(match kind {
LineKind::Unchanged => line,
LineKind::Added => line.class(theme::Container::DiffAddition),
LineKind::Deleted => line.class(theme::Container::DiffDeletion),
})
}
fn undecodable_contents_to_str(contents: &UndecodableContents) -> &str {
match contents {
UndecodableContents::ShortBase64(short) => short,
UndecodableContents::UnknownEncoding => "Unknown encoding",
}
}