use gtk::glib;
use gtk::prelude::*;
use gtk4 as gtk;
use gtk4::subclass::prelude::ObjectSubclassIsExt;
use std::cell::RefCell;
use std::rc::Rc;
use crate::diff_engine::{DiffItem, DiffKind, DiffResult};
pub const COLOR_ADDED: &str = "#2D7A2D";
pub const COLOR_REMOVED: &str = "#7A2D2D";
pub const COLOR_CHANGED: &str = "#7A7A2D";
mod imp {
use super::*;
use gtk::glib;
use gtk::subclass::prelude::*;
use std::cell::RefCell;
#[derive(Default)]
pub struct DiffItemObject {
pub inner: RefCell<Option<DiffItem>>,
}
#[glib::object_subclass]
impl ObjectSubclass for DiffItemObject {
const NAME: &'static str = "RustDiffItem";
type Type = super::DiffItemObject;
type ParentType = glib::Object;
}
impl ObjectImpl for DiffItemObject {}
}
glib::wrapper! {
pub struct DiffItemObject(ObjectSubclass<imp::DiffItemObject>);
}
impl DiffItemObject {
pub fn new(item: DiffItem) -> Self {
let obj: Self = glib::Object::builder().build();
obj.imp().inner.replace(Some(item));
obj
}
pub fn inner(&self) -> std::cell::Ref<'_, Option<DiffItem>> {
self.imp().inner.borrow()
}
}
pub struct DiffPanel {
pub widget: gtk::Box,
store: gtk::gio::ListStore,
pub selection_model: gtk::SingleSelection,
summary_label: gtk::Label,
filters: Rc<RefCell<(bool, bool, bool)>>,
all_items: Rc<RefCell<Vec<DiffItem>>>,
}
impl Default for DiffPanel {
fn default() -> Self {
Self::new()
}
}
impl DiffPanel {
pub fn new() -> Self {
let filters = Rc::new(RefCell::new((true, true, true)));
let all_items: Rc<RefCell<Vec<DiffItem>>> = Rc::new(RefCell::new(Vec::new()));
let container = gtk::Box::new(gtk::Orientation::Vertical, 0);
container.add_css_class("diff-panel");
let toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 8);
toolbar.set_margin_start(8);
toolbar.set_margin_end(8);
toolbar.set_margin_top(4);
toolbar.set_margin_bottom(4);
let summary_label = gtk::Label::new(Some("Sin diferencias"));
summary_label.set_hexpand(true);
summary_label.set_halign(gtk::Align::Start);
summary_label.add_css_class("dim-label");
let btn_added = gtk::ToggleButton::with_label("✚ Añadidos");
btn_added.set_active(true);
btn_added.add_css_class("success");
let btn_removed = gtk::ToggleButton::with_label("✖ Eliminados");
btn_removed.set_active(true);
btn_removed.add_css_class("error");
let btn_changed = gtk::ToggleButton::with_label("● Modificados");
btn_changed.set_active(true);
btn_changed.add_css_class("warning");
toolbar.append(&summary_label);
toolbar.append(&btn_added);
toolbar.append(&btn_removed);
toolbar.append(&btn_changed);
container.append(&toolbar);
let store = gtk::gio::ListStore::new::<DiffItemObject>();
let selection_model = gtk::SingleSelection::new(Some(store.clone()));
let column_view = gtk::ColumnView::new(Some(selection_model.clone()));
column_view.set_show_row_separators(true);
column_view.set_show_column_separators(true);
let col_type = create_column("Tipo", 80, |item: &DiffItem| {
let label = match item.kind {
DiffKind::Added => "✚ ADDED",
DiffKind::Removed => "✖ REMOVED",
DiffKind::Changed => "● CHANGED",
};
label.to_string()
});
let col_path = create_column("Ruta", 300, |item: &DiffItem| item.path.clone());
let col_left = create_column("Izquierdo", 250, |item: &DiffItem| {
item.left.clone().unwrap_or_default()
});
let col_right = create_column("Derecho", 250, |item: &DiffItem| {
item.right.clone().unwrap_or_default()
});
column_view.append_column(&col_type);
column_view.append_column(&col_path);
column_view.append_column(&col_left);
column_view.append_column(&col_right);
let scrolled = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Automatic)
.vscrollbar_policy(gtk::PolicyType::Automatic)
.vexpand(true)
.min_content_height(150)
.child(&column_view)
.build();
container.append(&scrolled);
let panel = Self {
widget: container,
store,
selection_model,
summary_label,
filters: filters.clone(),
all_items: all_items.clone(),
};
{
let filters_c = filters.clone();
let all_items_c = all_items.clone();
let store_c = panel.store.clone();
btn_added.connect_toggled(move |btn| {
filters_c.borrow_mut().0 = btn.is_active();
apply_filters(&store_c, &all_items_c.borrow(), &filters_c.borrow());
});
}
{
let filters_c = filters.clone();
let all_items_c = all_items.clone();
let store_c = panel.store.clone();
btn_removed.connect_toggled(move |btn| {
filters_c.borrow_mut().1 = btn.is_active();
apply_filters(&store_c, &all_items_c.borrow(), &filters_c.borrow());
});
}
{
let filters_c = filters.clone();
let all_items_c = all_items.clone();
let store_c = panel.store.clone();
btn_changed.connect_toggled(move |btn| {
filters_c.borrow_mut().2 = btn.is_active();
apply_filters(&store_c, &all_items_c.borrow(), &filters_c.borrow());
});
}
panel
}
pub fn update(&self, result: &DiffResult) {
let mut items = Vec::new();
items.extend(result.added.iter().cloned());
items.extend(result.removed.iter().cloned());
items.extend(result.changed.iter().cloned());
items.sort_by(|a, b| a.path.cmp(&b.path));
*self.all_items.borrow_mut() = items;
apply_filters(
&self.store,
&self.all_items.borrow(),
&self.filters.borrow(),
);
self.summary_label.set_text(&result.summary());
}
pub fn clear(&self) {
self.store.remove_all();
self.all_items.borrow_mut().clear();
self.summary_label.set_text("Sin diferencias");
}
}
fn apply_filters(store: >k::gio::ListStore, items: &[DiffItem], filters: &(bool, bool, bool)) {
store.remove_all();
let (show_added, show_removed, show_changed) = *filters;
for item in items {
let show = match item.kind {
DiffKind::Added => show_added,
DiffKind::Removed => show_removed,
DiffKind::Changed => show_changed,
};
if show {
store.append(&DiffItemObject::new(item.clone()));
}
}
}
fn create_column(
title: &str,
fixed_width: i32,
extractor: fn(&DiffItem) -> String,
) -> gtk::ColumnViewColumn {
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(|_, list_item| {
let label = gtk::Label::new(None);
label.set_halign(gtk::Align::Start);
label.set_ellipsize(gtk::pango::EllipsizeMode::End);
label.set_max_width_chars(80);
list_item
.downcast_ref::<gtk::ListItem>()
.unwrap()
.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
let obj = list_item.item().and_downcast::<DiffItemObject>().unwrap();
let label = list_item.child().and_downcast::<gtk::Label>().unwrap();
let inner = obj.inner();
if let Some(ref item) = *inner {
label.set_text(&extractor(item));
label.remove_css_class("diff-added");
label.remove_css_class("diff-removed");
label.remove_css_class("diff-changed");
match item.kind {
DiffKind::Added => label.add_css_class("diff-added"),
DiffKind::Removed => label.add_css_class("diff-removed"),
DiffKind::Changed => label.add_css_class("diff-changed"),
}
}
});
let column = gtk::ColumnViewColumn::new(Some(title), Some(factory));
column.set_fixed_width(fixed_width);
column.set_resizable(true);
column
}
pub fn diff_css() -> &'static str {
r#"
.diff-added {
background-color: rgba(46, 160, 67, 0.18);
color: @theme_fg_color;
}
.diff-removed {
background-color: rgba(248, 81, 73, 0.18);
color: @theme_fg_color;
}
.diff-changed {
background-color: rgba(210, 153, 34, 0.20);
color: @theme_fg_color;
}
.diff-panel {
border-top: 1px solid alpha(currentColor, 0.15);
}
.rustdiff-editor,
.rustdiff-editor text {
font-family: monospace;
}
"#
}