325 lines · 11.5 KB · Rust
RawDownload
use gpui::ElementId;use ui::{    ContextMenu, GradientFade, IconButton, IconName, IconSize, PopoverMenu, Tooltip, prelude::*,}; use crate::{    CsvPreviewView,    settings::FilterSortOrder,    table_data_engine::{        filtering_by_column::{FilterEntry, FilterEntryState},        sorting_by_column::{AppliedSorting, SortDirection},    },    types::AnyColumn,}; impl CsvPreviewView {    /// Create header for data, which is orderable with text on the left and sort button on the right    pub(crate) fn create_header_element_with_sort_button(        &self,        header_text: SharedString,        cx: &mut Context<'_, CsvPreviewView>,        col_idx: AnyColumn,    ) -> AnyElement {        let has_active_filter = self.engine.has_active_filters(col_idx);        let has_active_sort = self            .engine            .applied_sorting            .is_some_and(|o| o.col_idx == col_idx);        let always_show_buttons = has_active_filter || has_active_sort;        let group_name = SharedString::from(format!("csv-col-header-{}", col_idx.get()));         let colors = cx.theme().colors();        let base_bg = colors.editor_background;        let grad_width_hovered = px(100.);        let grad_width = if always_show_buttons {            grad_width_hovered        } else {            px(20.)        };        h_flex()            .group(group_name.clone())            .relative()            .overflow_hidden()            .w_full()            .items_center()            .font_buffer(cx)            .text_buffer(cx)            .child(                div()                    .flex_1()                    .min_w_0()                    .overflow_hidden()                    .whitespace_nowrap()                    .child(header_text),            )            .child(                GradientFade::new(base_bg, base_bg, base_bg)                    .width(grad_width)                    .width_hovered(grad_width_hovered)                    .right(px(0.))                    .gradient_stop(0.8)                    .group_name(group_name.clone()),            )            .child(                h_flex()                    .absolute()                    .right_0()                    .top_0()                    .h_full()                    .items_center()                    .gap_1()                    .when(!always_show_buttons, |this| {                        this.visible_on_hover(group_name)                    })                    .child(self.create_filter_button(cx, col_idx))                    .child(self.create_sort_button(cx, col_idx)),            )            .into_any_element()    }     fn create_sort_button(        &self,        cx: &mut Context<'_, CsvPreviewView>,        col_idx: AnyColumn,    ) -> Button {        let sort_btn = Button::new(            ElementId::NamedInteger("sort-button".into(), col_idx.get() as u64),            match self.engine.applied_sorting {                Some(ordering) if ordering.col_idx == col_idx => match ordering.direction {                    SortDirection::Asc => "↓",                    SortDirection::Desc => "↑",                },                _ => "↕", // Unsorted/available for sorting            },        )        .size(ButtonSize::Compact)        .style(            if self                .engine                .applied_sorting                .is_some_and(|o| o.col_idx == col_idx)            {                ButtonStyle::Filled            } else {                ButtonStyle::Subtle            },        )        .tooltip(Tooltip::text(match self.engine.applied_sorting {            Some(ordering) if ordering.col_idx == col_idx => match ordering.direction {                SortDirection::Asc => "Sorted A-Z. Click to sort Z-A",                SortDirection::Desc => "Sorted Z-A. Click to disable sorting",            },            _ => "Not sorted. Click to sort A-Z",        }))        .on_click(cx.listener(move |this, _event, _window, cx| {            let new_sorting = match this.engine.applied_sorting {                Some(ordering) if ordering.col_idx == col_idx => {                    // Same column clicked - cycle through states                    match ordering.direction {                        SortDirection::Asc => Some(AppliedSorting {                            col_idx,                            direction: SortDirection::Desc,                        }),                        SortDirection::Desc => None, // Clear sorting                    }                }                _ => {                    // Different column or no sorting - start with ascending                    Some(AppliedSorting {                        col_idx,                        direction: SortDirection::Asc,                    })                }            };             this.engine.applied_sorting = new_sorting;            this.apply_sort(cx);            cx.notify();        }));        sort_btn    }     fn create_filter_button(        &self,        cx: &mut Context<'_, CsvPreviewView>,        col: AnyColumn,    ) -> PopoverMenu<ContextMenu> {        let has_active_filters = self.engine.has_active_filters(col);         PopoverMenu::new(ElementId::NamedInteger(            "filter-menu".into(),            col.get() as u64,        ))        .trigger_with_tooltip(            IconButton::new(                ElementId::NamedInteger("filter-button".into(), col.get() as u64),                IconName::Filter,            )            .icon_size(IconSize::Small)            .style(if has_active_filters {                ButtonStyle::Filled            } else {                ButtonStyle::Subtle            })            .toggle_state(has_active_filters),            Tooltip::text(if has_active_filters {                "Column has active filters. Click to manage"            } else {                "No filters applied. Click to add filters"            }),        )        .menu({            let view_entity = cx.entity();            move |window, cx| {                let view = view_entity.read(cx);                let column_filters = match view.engine.get_filters_for_column(col) {                    Ok(filters) => filters,                    Err(err) => {                        log::error!("Failed to get filters for column: {err}");                        return None;                    }                };                let filter_sort_order = view.settings.filter_sort_order;                let filter_menu = Self::create_filter_menu(                    window,                    cx,                    view_entity.clone(),                    col,                    &column_filters,                    has_active_filters,                    filter_sort_order,                );                Some(filter_menu)            }        })    }     fn create_filter_menu(        window: &mut ui::Window,        cx: &mut ui::App,        view_entity: gpui::Entity<CsvPreviewView>,        col: AnyColumn,        column_filters: &[(FilterEntry, FilterEntryState)],        has_active_filters: bool,        sort_order: FilterSortOrder,    ) -> gpui::Entity<ContextMenu> {        let mut available: Vec<&FilterEntry> = column_filters            .iter()            .filter_map(|(entry, state)| {                matches!(state, FilterEntryState::Available { .. }).then_some(entry)            })            .collect();         match sort_order {            FilterSortOrder::AlphaThenCount => available.sort_by(|a, b| {                a.content                    .cmp(&b.content)                    .then_with(|| b.occurred_times().cmp(&a.occurred_times()))            }),            FilterSortOrder::CountThenAlpha => available.sort_by(|a, b| {                b.occurred_times()                    .cmp(&a.occurred_times())                    .then_with(|| a.content.cmp(&b.content))            }),        }         let unavailable: Vec<(&FilterEntry, AnyColumn)> = column_filters            .iter()            .filter_map(|(entry, state)| {                if let FilterEntryState::Unavailable { blocked_by } = state {                    Some((entry, *blocked_by))                } else {                    None                }            })            .collect();         // Pre-build applied-state lookup before moving into the closure        let applied_states: Vec<(FilterEntry, bool)> = column_filters            .iter()            .filter_map(|(entry, state)| {                if let FilterEntryState::Available { is_applied } = state {                    Some((entry.clone(), *is_applied))                } else {                    None                }            })            .collect();         let available_cloned: Vec<FilterEntry> = available.iter().map(|e| (*e).clone()).collect();        let unavailable_cloned: Vec<(FilterEntry, AnyColumn)> = unavailable            .into_iter()            .map(|(e, col)| (e.clone(), col))            .collect();         ContextMenu::build(window, cx, move |menu, _, _| {            let mut menu = menu;             if has_active_filters {                menu = menu                    .toggleable_entry("Clear all", false, ui::IconPosition::Start, None, {                        let view_entity = view_entity.clone();                        move |_window, cx| {                            view_entity.update(cx, |view, cx| {                                view.clear_filters(col, cx);                                cx.notify();                            });                        }                    })                    .separator();            }             for entry in &available_cloned {                let is_applied = applied_states                    .iter()                    .find(|(e, _)| e.content == entry.content)                    .map_or(false, |(_, applied)| *applied);                 let label: SharedString =                    format_filter_label(entry.content.as_ref(), entry.occurred_times()).into();                let entry_value = entry.content.clone();                 menu = menu.toggleable_entry(&label, is_applied, ui::IconPosition::Start, None, {                    let view_entity = view_entity.clone();                    move |_window, cx| {                        view_entity.update(cx, |view, cx| {                            view.toggle_filter(col, entry_value.clone(), cx);                            cx.notify();                        });                    }                });            }             if !unavailable_cloned.is_empty() {                menu = menu.separator().header("Hidden by other filters");                for (entry, _blocked_by) in &unavailable_cloned {                    let label: SharedString =                        format_filter_label(entry.content.as_ref(), entry.occurred_times()).into();                    menu = menu.custom_entry(                        {                            let label = label.clone();                            move |_window, cx| {                                div()                                    .px_2()                                    .text_color(cx.theme().colors().text_muted)                                    .child(label.clone())                                    .into_any_element()                            }                        },                        |_, _| {},                    );                }            }             menu        })    }} fn format_filter_label(content: Option<&SharedString>, count: usize) -> String {    match content {        Some(s) => format!("{s} ({count})"),        None => format!("<null> ({count})"),    }}