use crate::{Icon, LabelStyle, ReUi};
use egui::epaint::text::TextWrapping;
use egui::{Align, Align2, Response, Shape, Ui};
use std::default::Default;
struct ListItemResponse {
response: Response,
collapse_response: Option<Response>,
}
pub struct ShowCollapsingResponse<R> {
pub item_response: Response,
pub body_response: Option<egui::InnerResponse<R>>,
}
#[derive(Default, Clone, Copy, Debug)]
pub enum WidthAllocationMode {
#[default]
Available,
Compact,
}
#[allow(clippy::type_complexity)]
pub struct ListItem<'a> {
text: egui::WidgetText,
re_ui: &'a ReUi,
active: bool,
selected: bool,
draggable: bool,
drag_target: bool,
subdued: bool,
weak: bool,
italics: bool,
label_style: crate::LabelStyle,
force_hovered: bool,
collapse_openness: Option<f32>,
height: f32,
width_allocation_mode: WidthAllocationMode,
icon_fn: Option<Box<dyn FnOnce(&ReUi, &egui::Ui, egui::Rect, egui::style::WidgetVisuals) + 'a>>,
buttons_fn: Option<Box<dyn FnOnce(&ReUi, &mut egui::Ui) -> egui::Response + 'a>>,
}
impl<'a> ListItem<'a> {
pub fn new(re_ui: &'a ReUi, text: impl Into<egui::WidgetText>) -> Self {
Self {
text: text.into(),
re_ui,
active: true,
selected: false,
draggable: false,
drag_target: false,
subdued: false,
weak: false,
italics: false,
label_style: crate::LabelStyle::default(),
force_hovered: false,
collapse_openness: None,
height: ReUi::list_item_height(),
width_allocation_mode: Default::default(),
icon_fn: None,
buttons_fn: None,
}
}
#[inline]
pub fn active(mut self, active: bool) -> Self {
self.active = active;
self
}
#[inline]
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
#[inline]
pub fn draggable(mut self, draggable: bool) -> Self {
self.draggable = draggable;
self
}
#[inline]
pub fn drop_target_style(mut self, drag_target: bool) -> Self {
self.drag_target = drag_target;
self
}
#[inline]
pub fn subdued(mut self, subdued: bool) -> Self {
self.subdued = subdued;
self
}
#[inline]
pub fn weak(mut self, weak: bool) -> Self {
self.weak = weak;
self
}
#[inline]
pub fn italics(mut self, italics: bool) -> Self {
self.italics = italics;
self
}
#[inline]
pub fn label_style(mut self, style: crate::LabelStyle) -> Self {
self.label_style = style;
self
}
#[inline]
pub fn force_hovered(mut self, force_hovered: bool) -> Self {
self.force_hovered = force_hovered;
self
}
#[inline]
pub fn with_height(mut self, height: f32) -> Self {
self.height = height;
self
}
#[inline]
pub fn width_allocation_mode(mut self, mode: WidthAllocationMode) -> Self {
self.width_allocation_mode = mode;
self
}
#[inline]
pub fn with_icon(self, icon: &'a Icon) -> Self {
self.with_icon_fn(|_, ui, rect, visuals| {
let tint = visuals.fg_stroke.color;
icon.as_image().tint(tint).paint_at(ui, rect);
})
}
#[inline]
pub fn with_icon_fn(
mut self,
icon_fn: impl FnOnce(&ReUi, &egui::Ui, egui::Rect, egui::style::WidgetVisuals) + 'a,
) -> Self {
self.icon_fn = Some(Box::new(icon_fn));
self
}
#[inline]
pub fn with_buttons(
mut self,
buttons: impl FnOnce(&ReUi, &mut egui::Ui) -> egui::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(&ReUi, &mut egui::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 re_ui = self.re_ui;
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(re_ui, ui));
ShowCollapsingResponse {
item_response: response.response,
body_response,
}
}
fn ui(mut self, ui: &mut Ui, id: Option<egui::Id>) -> ListItemResponse {
let collapse_extra = if self.collapse_openness.is_some() {
ReUi::collapsing_triangle_size().x + ReUi::text_to_icon_padding()
} else {
0.0
};
let icon_extra = if self.icon_fn.is_some() {
ReUi::small_icon_size().x + ReUi::text_to_icon_padding()
} else {
0.0
};
match self.label_style {
LabelStyle::Normal => {}
LabelStyle::Unnamed => {
self.italics = true;
}
}
if self.italics {
self.text = self.text.italics();
}
fn icons_and_label_width(
ui: &egui::Ui,
item: &ListItem<'_>,
collapse_extra: f32,
icon_extra: f32,
) -> f32 {
let layout_job = item.text.clone().into_layout_job(
ui.style(),
egui::FontSelection::Default,
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.height);
let (rect, mut response) = ui.allocate_at_least(
desired_size,
if self.draggable {
egui::Sense::click_and_drag()
} else {
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.contains_pointer = full_span_response.contains_pointer;
response.hovered = full_span_response.hovered;
let mut style_response = response.clone();
if self.force_hovered {
style_response.contains_pointer = true;
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 * ReUi::collapsing_triangle_size().y,
));
let triangle_rect =
egui::Rect::from_min_size(triangle_pos, ReUi::collapsing_triangle_size());
let triangle_response = ui.interact(
triangle_rect.expand(3.0), id.unwrap_or(ui.id()).with("collapsing_triangle"),
egui::Sense::click(),
);
ReUi::paint_collapsing_triangle(ui, openness, triangle_rect, &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 * ReUi::small_icon_size().y,
));
let icon_rect = egui::Rect::from_min_size(icon_pos, ReUi::small_icon_size());
icon_fn(self.re_ui, ui, icon_rect, visuals);
}
let should_show_buttons = self.active
&& ui.rect_contains_pointer(rect)
&& !egui::DragAndDrop::has_any_payload(ui.ctx());
let button_response = if should_show_buttons {
if let Some(buttons) = self.buttons_fn {
let mut ui =
ui.child_ui(rect, egui::Layout::right_to_left(egui::Align::Center));
Some(buttons(self.re_ui, &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() + ReUi::text_to_icon_padding();
}
match self.label_style {
LabelStyle::Normal => {}
LabelStyle::Unnamed => {
self.text = self.text.color(visuals.fg_stroke.color.gamma_multiply(0.5));
}
}
let mut layout_job =
self.text
.into_layout_job(ui.style(), egui::FontSelection::Default, 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 = Align2::LEFT_CENTER
.align_size_within_rect(galley.size(), text_rect)
.min;
ui.painter().galley(text_pos, galley, visuals.text_color());
if self.drag_target {
ui.painter().set(
background_frame,
Shape::rect_stroke(bg_rect, 0.0, (1.0, ui.visuals().selection.bg_fill)),
);
} else {
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, Shape::rect_filled(bg_rect, 0.0, bg_fill));
}
}
}
ListItemResponse {
response,
collapse_response,
}
}
}