use std::default::Default;
use egui::epaint::text::TextWrapping;
use egui::{Response, Ui};
struct ListItemSpacing {
height: f32,
text_to_icon_padding: f32,
icon_size: egui::Vec2,
}
impl Default for ListItemSpacing {
fn default() -> Self {
Self {
height: 24.0,
text_to_icon_padding: 4.0,
icon_size: egui::Vec2::splat(12.0),
}
}
}
struct ListItemResponse {
response: Response,
collapse_response: Option<Response>,
}
pub struct ShowCollapsingResponse<R> {
pub item_response: Response,
pub body_response: Option<R>,
}
#[derive(Default, Clone, Copy, Debug)]
pub enum WidthAllocationMode {
#[default]
Available,
Compact,
}
#[allow(clippy::type_complexity)]
#[allow(clippy::struct_excessive_bools)]
pub struct ListItem<'a> {
text: egui::WidgetText,
spacing: ListItemSpacing,
active: bool,
selected: bool,
subdued: bool,
weak: bool,
italics: bool,
force_hovered: bool,
collapse_openness: Option<f32>,
width_allocation_mode: WidthAllocationMode,
icon_fn: Option<Box<dyn FnOnce(&mut Ui, egui::Rect, egui::style::WidgetVisuals) + 'a>>,
buttons_fn: Option<Box<dyn FnOnce(&mut Ui) -> Response + 'a>>,
}
impl<'a> ListItem<'a> {
pub fn new(text: impl Into<egui::WidgetText>) -> Self {
Self {
text: text.into(),
spacing: ListItemSpacing::default(),
active: true,
selected: false,
subdued: false,
weak: false,
italics: false,
force_hovered: false,
collapse_openness: None,
width_allocation_mode: WidthAllocationMode::default(),
icon_fn: None,
buttons_fn: None,
}
}
#[must_use]
#[inline]
pub fn active(mut self, active: bool) -> Self {
self.active = active;
self
}
#[must_use]
#[inline]
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
#[must_use]
#[inline]
pub fn subdued(mut self, subdued: bool) -> Self {
self.subdued = subdued;
self
}
#[must_use]
#[inline]
pub fn weak(mut self, weak: bool) -> Self {
self.weak = weak;
self
}
#[must_use]
#[inline]
pub fn italics(mut self, italics: bool) -> Self {
self.italics = italics;
self
}
#[must_use]
#[inline]
pub fn force_hovered(mut self, force_hovered: bool) -> Self {
self.force_hovered = force_hovered;
self
}
#[must_use]
#[inline]
pub fn with_height(mut self, height: f32) -> Self {
self.spacing.height = height;
self
}
#[must_use]
#[inline]
pub fn width_allocation_mode(mut self, mode: WidthAllocationMode) -> Self {
self.width_allocation_mode = mode;
self
}
#[must_use]
#[inline]
pub fn with_icon_fn(
mut self,
icon_fn: impl FnOnce(&mut Ui, egui::Rect, egui::style::WidgetVisuals) + 'a,
) -> Self {
self.icon_fn = Some(Box::new(icon_fn));
self
}
#[must_use]
#[inline]
pub fn with_buttons(mut self, buttons: impl FnOnce(&mut Ui) -> Response + 'a) -> Self {
self.buttons_fn = Some(Box::new(buttons));
self
}
pub fn show(self, ui: &mut Ui) -> Response {
self.ui(ui, None).response
}
pub fn show_collapsing<R>(
mut self,
ui: &mut Ui,
id: egui::Id,
default_open: bool,
add_body: impl FnOnce(&mut Ui) -> R,
) -> ShowCollapsingResponse<R> {
let mut state = egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
id,
default_open,
);
self.collapse_openness = Some(state.openness(ui.ctx()));
let response = self.ui(ui, Some(id));
if let Some(collapse_response) = response.collapse_response {
if collapse_response.clicked() {
state.toggle(ui);
}
}
if response.response.double_clicked() {
state.toggle(ui);
}
let body_response = state.show_body_indented(&response.response, ui, |ui| add_body(ui));
ShowCollapsingResponse {
item_response: response.response,
body_response: body_response.map(|r| r.inner),
}
}
#[allow(clippy::too_many_lines)]
fn ui(mut self, ui: &mut Ui, id: Option<egui::Id>) -> ListItemResponse {
let icon_size = egui::Vec2::splat(ui.spacing().icon_width);
let collapse_extra = if self.collapse_openness.is_some() {
icon_size.x + self.spacing.text_to_icon_padding
} else {
0.0
};
let icon_extra = if self.icon_fn.is_some() {
self.spacing.icon_size.x + self.spacing.text_to_icon_padding
} else {
0.0
};
if self.italics {
self.text = self.text.italics();
}
#[allow(clippy::items_after_statements)]
fn icons_and_label_width(
ui: &Ui,
item: &ListItem<'_>,
collapse_extra: f32,
icon_extra: f32,
) -> f32 {
let layout_job = item.text.clone().into_layout_job(
ui.style(),
egui::FontSelection::Default,
egui::Align::LEFT,
);
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
let text_width = galley.size().x;
(collapse_extra + icon_extra + text_width).ceil()
}
let desired_width = match self.width_allocation_mode {
WidthAllocationMode::Available => ui.available_width(),
WidthAllocationMode::Compact => {
icons_and_label_width(ui, &self, collapse_extra, icon_extra)
}
};
let desired_size = egui::vec2(desired_width, self.spacing.height);
let (rect, mut response) = ui.allocate_at_least(desired_size, egui::Sense::click());
let mut bg_rect = rect;
bg_rect.extend_with_x(ui.clip_rect().right());
bg_rect.extend_with_x(ui.clip_rect().left());
let full_span_response = ui.interact(bg_rect, response.id, egui::Sense::click());
response.clicked = full_span_response.clicked;
response.hovered = full_span_response.hovered;
let mut style_response = response.clone();
if self.force_hovered {
style_response.hovered = true;
}
let mut collapse_response = None;
if ui.is_rect_visible(bg_rect) {
let mut visuals = if self.active {
ui.style()
.interact_selectable(&style_response, self.selected)
} else {
ui.visuals().widgets.inactive
};
if self.weak {
visuals.fg_stroke.color = ui.visuals().weak_text_color();
} else if self.subdued {
visuals.fg_stroke.color = visuals.fg_stroke.color.gamma_multiply(0.5);
}
let background_frame = ui.painter().add(egui::Shape::Noop);
if let Some(openness) = self.collapse_openness {
let triangle_pos = ui.painter().round_pos_to_pixels(egui::pos2(
rect.min.x,
rect.center().y - 0.5 * icon_size.y,
));
let triangle_rect = egui::Rect::from_min_size(triangle_pos, icon_size);
let triangle_response = ui.interact(
triangle_rect.expand(3.0), id.unwrap_or(ui.id()).with("collapsing_triangle"),
egui::Sense::click(),
);
egui::collapsing_header::paint_default_icon(ui, openness, &triangle_response);
collapse_response = Some(triangle_response);
}
if let Some(icon_fn) = self.icon_fn {
let icon_pos = ui.painter().round_pos_to_pixels(egui::pos2(
rect.min.x + collapse_extra,
rect.center().y - 0.5 * self.spacing.icon_size.y,
));
let icon_rect = egui::Rect::from_min_size(icon_pos, self.spacing.icon_size);
icon_fn(ui, icon_rect, visuals);
}
let button_response = if self.active
&& ui
.interact(
rect,
id.unwrap_or(ui.id()).with("buttons"),
egui::Sense::hover(),
)
.hovered()
{
if let Some(buttons) = self.buttons_fn {
let mut ui =
ui.child_ui(rect, egui::Layout::right_to_left(egui::Align::Center));
Some(buttons(&mut ui))
} else {
None
}
} else {
None
};
let mut text_rect = rect;
text_rect.min.x += collapse_extra + icon_extra;
if let Some(button_response) = &button_response {
text_rect.max.x -= button_response.rect.width() + self.spacing.text_to_icon_padding;
}
let mut layout_job = self.text.into_layout_job(
ui.style(),
egui::FontSelection::Default,
egui::Align::LEFT,
);
layout_job.wrap = TextWrapping::truncate_at_width(text_rect.width());
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
response.widget_info(|| {
egui::WidgetInfo::selected(
egui::WidgetType::SelectableLabel,
self.selected,
galley.text(),
)
});
let text_pos = egui::Align2::LEFT_CENTER
.align_size_within_rect(galley.size(), text_rect)
.min;
ui.painter().galley(text_pos, galley, visuals.text_color());
let bg_fill = if button_response.map_or(false, |r| r.hovered()) {
Some(visuals.bg_fill)
} else if self.selected
|| style_response.hovered()
|| style_response.highlighted()
|| style_response.has_focus()
{
Some(visuals.weak_bg_fill)
} else {
None
};
if let Some(bg_fill) = bg_fill {
ui.painter().set(
background_frame,
egui::Shape::rect_filled(bg_rect, 0.0, bg_fill),
);
}
}
ListItemResponse {
response,
collapse_response,
}
}
}