use eframe::egui::{self, Align, Layout, RichText, Sense};
use egui_extras::{Column, TableBuilder, TableRow};
use super::icons::{self, Icon};
use crate::theme::Palette;
const ROW_HEIGHT: f32 = 48.0;
const COMPACT_ROW_HEIGHT: f32 = 30.0;
pub fn set_compact(ctx: &egui::Context, compact: bool) {
ctx.data_mut(|d| d.insert_temp(compact_id(), compact));
}
pub fn is_compact(ui: &egui::Ui) -> bool {
ui.data(|d| d.get_temp(compact_id()).unwrap_or(false))
}
fn compact_id() -> egui::Id {
egui::Id::unique("ferrodock-compact")
}
pub struct Col {
pub title: &'static str,
pub width: Column,
}
impl Col {
pub fn fill(title: &'static str) -> Self {
Self {
title,
width: Column::remainder().at_least(160.0).clip(true),
}
}
pub fn fixed(title: &'static str, width: f32) -> Self {
Self {
title,
width: Column::exact(width).clip(true),
}
}
}
pub fn request_step(ctx: &egui::Context, delta: i32) {
ctx.data_mut(|d| d.insert_temp(step_id(), delta));
}
pub fn clear_step(ctx: &egui::Context) {
ctx.data_mut(|d| d.remove_temp::<i32>(step_id()));
}
fn step_id() -> egui::Id {
egui::Id::unique("ferrodock-table-step")
}
fn step(current: Option<usize>, delta: i32, rows: usize) -> Option<usize> {
if rows == 0 || delta == 0 {
return None;
}
let Some(current) = current else {
return Some(if delta > 0 { 0 } else { rows - 1 });
};
let next = (current as i64 + i64::from(delta)).clamp(0, rows as i64 - 1) as usize;
(next != current).then_some(next)
}
pub fn show(
ui: &mut egui::Ui,
p: &Palette,
id: &str,
columns: Vec<Col>,
rows: usize,
is_selected: impl Fn(usize) -> bool,
mut add_row: impl FnMut(usize, &mut TableRow<'_, '_>),
) -> Option<usize> {
let row_height = if is_compact(ui) {
COMPACT_ROW_HEIGHT
} else {
ROW_HEIGHT
};
let requested = ui.ctx().data_mut(|d| d.remove_temp::<i32>(step_id()));
let current = requested.and_then(|_| (0..rows).find(|&i| is_selected(i)));
let stepped = requested.and_then(|delta| step(current, delta, rows));
let mut clicked = stepped;
let mut builder = TableBuilder::new(ui)
.id_salt(id)
.sense(Sense::click())
.cell_layout(Layout::left_to_right(Align::Center));
if let Some(row) = stepped.or(current) {
builder = builder.scroll_to_row(row, None);
}
for col in &columns {
builder = builder.column(col.width);
}
builder
.header(26.0, |mut header| {
for col in &columns {
header.col(|ui| {
ui.label(RichText::new(col.title).size(11.0).color(p.muted));
});
}
})
.body(|body| {
body.rows(row_height, rows, |mut row| {
let index = row.index();
row.set_selected(is_selected(index));
add_row(index, &mut row);
if row.response().clicked() {
clicked = Some(index);
}
});
});
clicked
}
pub fn with_icon(
ui: &mut egui::Ui,
p: &Palette,
icon: Icon,
color: egui::Color32,
primary: &str,
secondary: &str,
) {
let size = if is_compact(ui) { 18.0 } else { 30.0 };
let (rect, _) = ui.allocate_exact_size(egui::vec2(size, size), Sense::hover());
ui.painter().rect_filled(
rect,
egui::CornerRadius::same(7),
color.gamma_multiply(0.12),
);
icons::paint(ui.painter(), rect.shrink(size * 0.22), icon, color);
ui.add_space(4.0);
two_line(ui, p, primary, secondary);
}
pub fn two_line(ui: &mut egui::Ui, p: &Palette, primary: &str, secondary: &str) {
let primary_text = RichText::new(primary)
.font(egui::FontId::new(13.5, crate::theme::semibold()))
.color(p.text);
if is_compact(ui) {
label(ui, primary_text);
label(ui, RichText::new(secondary).size(12.0).color(p.muted));
return;
}
ui.vertical(|ui| {
ui.add_space(6.0);
label(ui, primary_text);
label(ui, RichText::new(secondary).size(12.0).color(p.muted));
});
}
pub fn row_hovered(ui: &egui::Ui) -> bool {
let cell = ui.max_rect();
ui.ctx().pointer_hover_pos().is_some_and(|pos| {
ui.clip_rect().contains(pos) && pos.y >= cell.top() && pos.y < cell.bottom()
})
}
pub fn text(ui: &mut egui::Ui, p: &Palette, value: &str) {
label(ui, RichText::new(value).color(p.text));
}
pub fn muted(ui: &mut egui::Ui, p: &Palette, value: &str) {
label(ui, RichText::new(value).color(p.muted));
}
pub fn mono(ui: &mut egui::Ui, p: &Palette, value: &str) {
label(ui, RichText::new(value).monospace().color(p.muted));
}
fn label(ui: &mut egui::Ui, text: RichText) {
ui.add(egui::Label::new(text).truncate().selectable(false));
}
#[cfg(test)]
mod tests {
use super::step;
#[test]
fn steps_through_rows() {
assert_eq!(step(None, 1, 3), Some(0));
assert_eq!(step(None, -1, 3), Some(2));
assert_eq!(step(Some(0), 1, 3), Some(1));
assert_eq!(step(Some(2), 1, 3), None);
assert_eq!(step(Some(0), -1, 3), None);
assert_eq!(step(Some(1), 0, 3), None);
assert_eq!(step(None, 1, 0), None);
}
}