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")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ticked {
None,
Some,
All,
}
impl Ticked {
pub fn of(ticked: usize, shown: usize) -> Self {
match ticked {
0 => Ticked::None,
n if n >= shown => Ticked::All,
_ => Ticked::Some,
}
}
}
pub struct Col {
pub title: &'static str,
pub width: Column,
pub ticked: Option<Ticked>,
}
impl Col {
pub fn ticks(ticked: Ticked) -> Self {
Self {
title: "",
width: Column::exact(28.0),
ticked: Some(ticked),
}
}
pub fn fill(title: &'static str) -> Self {
Self {
title,
width: Column::remainder().at_least(160.0).clip(true),
ticked: None,
}
}
pub fn fixed(title: &'static str, width: f32) -> Self {
Self {
title,
width: Column::exact(width).clip(true),
ticked: None,
}
}
}
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 select_all_id(table: &str) -> egui::Id {
egui::Id::unique("ferrodock-table-select-all").with(table)
}
pub fn select_all_clicked(ui: &egui::Ui, id: &str) -> bool {
ui.data_mut(|d| d.remove_temp::<bool>(select_all_id(id)))
.unwrap_or(false)
}
pub fn tick(ui: &mut egui::Ui, p: &Palette, on: bool, name: &str) -> bool {
let state = if on { Ticked::All } else { Ticked::None };
tick_box(ui, p, state, &format!("Select {name}"))
}
pub fn tick_state(ui: &mut egui::Ui, p: &Palette, state: Ticked, label: &str) -> bool {
tick_box(ui, p, state, label)
}
fn tick_box(ui: &mut egui::Ui, p: &Palette, state: Ticked, label: &str) -> bool {
let (rect, response) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), Sense::click());
let response = response.on_hover_cursor(egui::CursorIcon::PointingHand);
response.widget_info(|| {
egui::WidgetInfo::selected(egui::Role::CheckBox, true, state == Ticked::All, label)
});
let painter = ui.painter();
let corner = egui::CornerRadius::same(4);
if state == Ticked::None {
let stroke = if response.hovered() {
p.accent
} else {
p.muted.gamma_multiply(0.6)
};
painter.rect_stroke(
rect.shrink(0.5),
corner,
egui::Stroke::new(1.3, stroke),
egui::StrokeKind::Inside,
);
} else {
painter.rect_filled(rect, corner, p.accent);
let white = egui::Stroke::new(2.0, p.on_accent);
let c = rect.center();
if state == Ticked::All {
painter.line(
vec![
c + egui::vec2(-4.0, 0.0),
c + egui::vec2(-1.0, 3.0),
c + egui::vec2(4.5, -3.5),
],
white,
);
} else {
painter.line_segment([c + egui::vec2(-4.0, 0.0), c + egui::vec2(4.0, 0.0)], white);
}
}
response.clicked()
}
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| match col.ticked {
Some(state) => {
let label = if state == Ticked::All {
"Select none"
} else {
"Select all"
};
if tick_box(ui, p, state, label) {
ui.data_mut(|d| d.insert_temp(select_all_id(id), true));
}
}
None => {
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);
}
}