use std::hash::Hash;
use std::ops::{Deref, DerefMut};
use eframe::egui;
use crate::state;
pub const GRID_COLUMNS: u32 = 12;
pub const GRID_GUTTER: f32 = 12.0;
pub const GRID_ROW_MODULE: f32 = 12.0;
pub const GRID_EDGE_PAD: f32 = GRID_GUTTER;
pub const GRID_COL_WIDTH: f32 = 51.0;
pub const fn span_width(n: u32) -> f32 {
n as f32 * GRID_COL_WIDTH + n.saturating_sub(1) as f32 * GRID_GUTTER
}
pub struct CardCtx<'a> {
ui: &'a mut egui::Ui,
store: &'a state::StateStore,
}
impl<'a> CardCtx<'a> {
pub(crate) fn new(ui: &'a mut egui::Ui, store: &'a state::StateStore) -> Self {
Self { ui, store }
}
pub fn store(&self) -> &state::StateStore {
self.store
}
pub fn ui_mut(&mut self) -> &mut egui::Ui {
self.ui
}
pub fn button(&mut self, text: impl Into<egui::WidgetText>) -> egui::Response {
self.ui.add(crate::widgets::Button::new(text))
}
pub fn toggle(&mut self, on: &mut bool, text: impl Into<egui::WidgetText>) -> egui::Response {
self.ui.add(crate::widgets::Button::new(text).on(on))
}
pub fn slider<Num: egui::emath::Numeric>(
&mut self,
value: &mut Num,
range: std::ops::RangeInclusive<Num>,
) -> egui::Response {
self.ui.add(crate::widgets::Slider::new(value, range))
}
pub fn number<Num: egui::emath::Numeric>(
&mut self,
value: &mut Num,
) -> egui::Response {
self.ui.add(crate::widgets::NumberField::new(value))
}
pub fn text_field(&mut self, text: &mut dyn egui::TextBuffer) -> egui::Response {
self.ui.add(crate::widgets::TextField::singleline(text))
}
pub fn progress(&mut self, fraction: f32) -> egui::Response {
self.ui.add(crate::widgets::ProgressBar::new(fraction))
}
pub fn markdown(&mut self, text: &str) {
crate::widgets::markdown(self.ui, text);
}
#[cfg(feature = "typst")]
pub fn typst(&mut self, source: &str) {
crate::widgets::typst_widget::typst_with_preamble(self.ui, source);
}
#[cfg(feature = "typst")]
pub fn typst_math_inline(&mut self, expr: &str) {
crate::widgets::typst_widget::typst_math_inline(self.ui, expr);
}
#[cfg(feature = "typst")]
pub fn typst_math_display(&mut self, expr: &str) {
crate::widgets::typst_widget::typst_math_display(self.ui, expr);
}
pub fn section(
&mut self,
title: &str,
add_contents: impl FnOnce(&mut CardCtx<'_>),
) {
self.section_inner(title, true, add_contents);
}
pub fn section_collapsed(
&mut self,
title: &str,
add_contents: impl FnOnce(&mut CardCtx<'_>),
) {
self.section_inner(title, false, add_contents);
}
fn section_inner(
&mut self,
title: &str,
default_open: bool,
add_contents: impl FnOnce(&mut CardCtx<'_>),
) {
use crate::themes::colorhash;
let id = self.ui.make_persistent_id(title);
let mut open = self.ui.ctx().data_mut(|d| {
*d.get_persisted_mut_or(id, default_open)
});
let color = colorhash::ral_categorical(title.as_bytes());
let text_color = colorhash::text_color_on(color);
let prev_spacing = self.ui.spacing().item_spacing.y;
self.ui.spacing_mut().item_spacing.y = 0.0;
let header_height = GRID_ROW_MODULE * 5.0;
let available_width = self.ui.available_width();
let (header_rect, header_response) = self.ui.allocate_exact_size(
egui::vec2(available_width, header_height),
egui::Sense::click(),
);
if header_response.clicked() {
open = !open;
self.ui.ctx().data_mut(|d| d.insert_persisted(id, open));
}
let painter = self.ui.painter();
painter.rect_filled(header_rect, 0.0, color);
let text_pos = egui::pos2(
header_rect.left() + GRID_EDGE_PAD,
header_rect.bottom() - GRID_ROW_MODULE,
);
painter.text(
text_pos,
egui::Align2::LEFT_BOTTOM,
title,
egui::FontId::proportional(header_height * 0.45),
text_color,
);
if header_response.hovered() {
self.ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
self.ui.spacing_mut().item_spacing.y = prev_spacing;
if open {
add_contents(self);
}
}
pub fn with_padding<R>(
&mut self,
padding: impl Into<egui::Margin>,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
egui::Frame::new()
.inner_margin(padding)
.show(self.ui, |ui| {
ui.set_width(ui.available_width());
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn horizontal<R>(
&mut self,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
self.ui.horizontal(|ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn horizontal_wrapped<R>(
&mut self,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
self.ui.horizontal_wrapped(|ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn vertical<R>(
&mut self,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
self.ui.vertical(|ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn with_layout<R>(
&mut self,
layout: egui::Layout,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
self.ui.with_layout(layout, |ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn push_id<R>(
&mut self,
id_salt: impl Hash,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
self.ui.push_id(id_salt, |ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn collapsing<R>(
&mut self,
heading: impl Into<egui::WidgetText>,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::CollapsingResponse<R> {
let store = self.store;
self.ui.collapsing(heading, |ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn scope<R>(
&mut self,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
self.ui.scope(|ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn indent<R>(
&mut self,
id_salt: impl Hash,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
self.ui.indent(id_salt, |ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn group<R>(
&mut self,
add_contents: impl FnOnce(&mut CardCtx<'_>) -> R,
) -> egui::InnerResponse<R> {
let store = self.store;
self.ui.group(|ui| {
let mut ctx = CardCtx::new(ui, store);
add_contents(&mut ctx)
})
}
pub fn grid(&mut self, build: impl FnOnce(&mut Grid<'_, '_>)) {
let store = self.store;
let left = self.ui.cursor().min.x + GRID_EDGE_PAD;
let top = self.ui.cursor().min.y + GRID_EDGE_PAD;
let mut g = Grid {
ui: self.ui,
store,
left,
cursor: 0,
row_top: top,
row_max_bottom: top,
};
build(&mut g);
g.finish();
}
}
pub struct FloatResponse {
pub closed: bool,
}
impl<'a> CardCtx<'a> {
#[track_caller]
pub fn float(
&mut self,
add_contents: impl FnOnce(&mut CardCtx<'_>),
) -> FloatResponse {
let float_id = self.ui.id().with("gorbie_float");
let initial_pos = self.ui.ctx().input(|i| {
i.pointer.hover_pos().unwrap_or(egui::pos2(100.0, 100.0))
});
let card_width = crate::NOTEBOOK_COLUMN_WIDTH;
let store = self.store;
let mut add_contents = Some(add_contents);
let resp = crate::floating::show_floating_card(
self.ui.ctx(),
float_id,
initial_pos,
card_width,
0.0,
store,
"Close",
&mut |ctx| {
if let Some(f) = add_contents.take() {
f(ctx);
}
},
);
FloatResponse { closed: resp.handle_clicked }
}
}
impl<'a> state::StateAccess for CardCtx<'a> {
fn store(&self) -> &state::StateStore {
self.store
}
}
impl<'a> Deref for CardCtx<'a> {
type Target = egui::Ui;
fn deref(&self) -> &Self::Target {
self.ui
}
}
impl<'a> DerefMut for CardCtx<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.ui
}
}
pub struct Grid<'ui, 'store> {
ui: &'ui mut egui::Ui,
store: &'store state::StateStore,
left: f32,
cursor: u32,
row_top: f32,
row_max_bottom: f32,
}
impl<'ui, 'store> Grid<'ui, 'store> {
pub fn full(&mut self, f: impl FnOnce(&mut CardCtx<'_>)) { self.place(12, f); }
pub fn three_quarters(&mut self, f: impl FnOnce(&mut CardCtx<'_>)) { self.place(9, f); }
pub fn two_thirds(&mut self, f: impl FnOnce(&mut CardCtx<'_>)) { self.place(8, f); }
pub fn half(&mut self, f: impl FnOnce(&mut CardCtx<'_>)) { self.place(6, f); }
pub fn third(&mut self, f: impl FnOnce(&mut CardCtx<'_>)) { self.place(4, f); }
pub fn quarter(&mut self, f: impl FnOnce(&mut CardCtx<'_>)) { self.place(3, f); }
pub fn skip_half(&mut self) { self.skip(6); }
pub fn skip_third(&mut self) { self.skip(4); }
pub fn skip_quarter(&mut self) { self.skip(3); }
pub fn place(
&mut self,
span: u32,
add_contents: impl FnOnce(&mut CardCtx<'_>),
) {
assert!(
span > 0 && span <= GRID_COLUMNS,
"span must be 1..={GRID_COLUMNS}, got {span}"
);
if self.needs_advance(span) {
self.new_row();
}
let x = self.left + col_x(self.cursor);
let width = span_width(span);
let cell_rect = egui::Rect::from_min_size(
egui::pos2(x, self.row_top),
egui::vec2(width, f32::MAX),
);
let store = self.store;
let mut child = self.ui.new_child(
egui::UiBuilder::new().max_rect(cell_rect),
);
child.set_width(width);
let mut ctx = CardCtx::new(&mut child, store);
add_contents(&mut ctx);
let used_bottom = child.min_rect().bottom();
if used_bottom > self.row_max_bottom {
self.row_max_bottom = used_bottom;
}
self.cursor += span;
if self.cursor >= GRID_COLUMNS {
self.cursor = 0;
}
}
pub fn skip(&mut self, span: u32) {
assert!(
span > 0 && span <= GRID_COLUMNS,
"skip must be 1..={GRID_COLUMNS}, got {span}"
);
if self.needs_advance(span) {
self.new_row();
}
self.cursor += span;
if self.cursor >= GRID_COLUMNS {
self.cursor = 0;
}
}
fn needs_advance(&self, span: u32) -> bool {
let pending_complete = self.cursor == 0 && self.row_max_bottom > self.row_top;
let overflow = self.cursor > 0 && self.cursor + span > GRID_COLUMNS;
pending_complete || overflow
}
fn new_row(&mut self) {
self.row_top = self.row_max_bottom + GRID_ROW_MODULE;
self.row_max_bottom = self.row_top;
self.cursor = 0;
}
fn finish(&mut self) {
let total_height = (self.row_max_bottom + GRID_EDGE_PAD - self.ui.cursor().min.y).max(0.0);
if total_height > 0.0 {
self.ui.allocate_space(egui::vec2(
2.0 * GRID_EDGE_PAD + span_width(GRID_COLUMNS),
total_height,
));
}
}
}
const fn col_x(col: u32) -> f32 {
col as f32 * (GRID_COL_WIDTH + GRID_GUTTER)
}