egui_listview 0.2.0

A list view widget for egui.
Documentation
use egui::{
    Align, Frame, Id, Label, Layout, Margin, RichText, Rounding, ScrollArea, Sense, Style, TextEdit,
};
use std::borrow::Cow;

use crate::r#trait::ItemTrait;

pub struct ListView<'a, W: ItemTrait + 'a, L: Iterator<Item = &'a W>> {
    pub(crate) title: Cow<'a, str>,
    pub(crate) hold_text: Option<Cow<'a, str>>,
    pub(crate) items: L,
    pub(crate) data: W::Data<'a>,
    pub(crate) inner_margin: Margin,
    pub(crate) outer_margin: Margin,
    pub(crate) rounding: Rounding,
    pub(crate) striped: bool,
    pub(crate) hscroll: bool,
}

impl<'a, W: ItemTrait + 'a, L: Iterator<Item = &'a W>> ListView<'a, W, L> {
    pub fn new(items: L, data: W::Data<'a>) -> Self {
        Self {
            title: Cow::Borrowed("Search"),
            hold_text: None,
            items,
            data,
            inner_margin: Margin::same(3.0),
            outer_margin: Margin::default(),
            rounding: Rounding::default(),
            striped: false,
            hscroll: false,
        }
    }
}

impl<'a, W: ItemTrait + 'a, L: Iterator<Item = &'a W>> ListView<'a, W, L> {
    pub fn title(mut self, title: Cow<'a, str>) -> Self {
        self.title = title;
        self
    }

    pub fn hold_text(mut self, text: Cow<'a, str>) -> Self {
        self.hold_text = Some(text);
        self
    }

    pub fn inner_margin(mut self, margin: Margin) -> Self {
        self.inner_margin = margin;
        self
    }

    pub fn outer_margin(mut self, margin: Margin) -> Self {
        self.outer_margin = margin;
        self
    }

    pub fn rounding(mut self, rounding: Rounding) -> Self {
        self.rounding = rounding;
        self
    }

    pub fn striped(mut self) -> Self {
        self.striped = true;
        self
    }

    pub fn show(
        self,
        ctx: &egui::Context,
        ui: &mut egui::Ui,
    ) -> egui::InnerResponse<Option<&'a W>> {
        let mut selected_item = None;

        let mut resp = ui.vertical(|outer_ui| {
            let ListView {
                title,
                hold_text,
                items,
                data,
                inner_margin,
                outer_margin,
                rounding,
                striped,
                hscroll,
            } = self;

            let resp = outer_ui.scope(|ui| {
                let root_id = ui.auto_id_with("ListView");
                let search_id = root_id.with("search");
                let selected_id = root_id.with("selected");
                let hovered_id = root_id.with("hovered");

                let mut search: String = ui.data_mut(|d| d.get_temp(search_id)).unwrap_or_default();
                let mut selected: Option<Id> =
                    ui.data_mut(|d| d.get_temp(selected_id)).unwrap_or_default();
                let old_selected = selected;
                let mut hovered: Option<Id> =
                    ui.data_mut(|d| d.get_temp(hovered_id)).unwrap_or_default();

                ui.horizontal_top(|ui| {
                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
                        ui.visuals_mut().button_frame = true;
                        ui.add(Label::new(RichText::new(title).strong()));
                    });
                    ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
                        if !search.is_empty() {
                            ui.visuals_mut().button_frame = false;
                            if ui.button("").on_hover_text("Clear search text").clicked() {
                                search.clear();
                            }
                        }

                        let mut search_text = TextEdit::singleline(&mut search);
                        if let Some(text) = hold_text {
                            search_text = search_text.hint_text(text);
                        }
                        ui.add(search_text);
                    });
                });

                ui.separator();

                ScrollArea::vertical()
                    .id_source(root_id.with("list"))
                    .hscroll(hscroll)
                    .auto_shrink([false, false])
                    .show(ui, |ui| {
                        egui::Grid::new("list view container")
                            .num_columns(1)
                            .striped(striped)
                            .show(ui, |ui| {
                                for item in items {
                                    let id = item.id(&data);
                                    let checked = selected == Some(id);
                                    let hover = hovered == Some(id);

                                    if checked {
                                        selected_item = Some(item);
                                    }

                                    if search.is_empty() || item.on_search(&search, &data) {
                                        let mut child_frame = egui::Frame::default()
                                            .inner_margin(inner_margin)
                                            .outer_margin(outer_margin)
                                            .rounding(rounding);
                                        if checked {
                                            item.style_clicked(&mut child_frame);
                                        } else if hover {
                                            item.style_hovered(&mut child_frame);
                                        } else {
                                            item.style_normal(&mut child_frame);
                                        }
                                        let mut interact_area = child_frame
                                            .show(ui, |ui| {
                                                item.show(checked, hover, ctx, ui, &data);
                                                ui.interact(
                                                    ui.max_rect(),
                                                    item.id(&data),
                                                    Sense::click(),
                                                )
                                            })
                                            .inner;
                                        if let Some(tips) = item.hovered_text() {
                                            interact_area = interact_area.on_hover_text(tips);
                                        }

                                        if interact_area.hovered() && hovered != Some(id) {
                                            hovered = Some(id);
                                        }

                                        if interact_area.clicked() && !checked {
                                            selected = Some(id);
                                            selected_item = Some(item);
                                        }

                                        ui.end_row();
                                    }
                                }
                            });
                    });

                if let Some(item) = selected_item {
                    item.selected_item(&data);
                }

                ui.data_mut(|d| {
                    d.insert_temp(search_id, search);
                    d.insert_temp(selected_id, selected);
                    d.insert_temp(hovered_id, hovered);
                });

                old_selected != selected
            });

            resp.inner
        });

        if resp.inner {
            resp.response.mark_changed();
        }

        egui::InnerResponse::new(selected_item, resp.response)
    }
}