use crate::ui::slash::Row;
use crate::ui::{Model, Selector, theme};
pub mod diagnostics;
pub mod dropup;
pub mod selectors;
pub mod waiting;
pub use diagnostics::{READY, state};
pub use selectors::NO_MODEL;
pub const LEAD: &str = theme::glyph::LEAD;
pub const SEP: &str = theme::glyph::SEP;
pub const ENGINES: &str = "engines";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Door {
Row(Row),
Drop(Selector),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Run {
pub text: String,
pub ink: egui::Color32,
pub door: Option<Door>,
pub mark: Option<(&'static str, egui::Color32)>,
}
pub fn run(text: impl Into<String>, ink: egui::Color32) -> Run {
Run {
text: text.into(),
ink,
door: None,
mark: None,
}
}
pub fn door(model: &Model, word: &str) -> Option<Row> {
crate::ui::slash::rows(model)
.into_iter()
.find(|row| row.word == word)
}
pub fn selected(model: &Model) -> Option<crate::reply::convs::ConvRow> {
let id = model.conversation.as_ref()?;
model.rows().into_iter().find(|row| row.root_id == *id)
}
pub fn name(model: &Model) -> Run {
let text = match (selected(model), &model.aim) {
(Some(row), _) => row.display,
(None, Some(aim)) => aim.address.clone(),
(None, None) => ENGINES.to_owned(),
};
Run {
door: door(model, crate::ui::switcher::WORD).map(Door::Row),
..run(text, theme::INK)
}
}
pub fn runs(model: &Model) -> (Vec<Run>, usize) {
let (mut out, kept) = selectors::selectors(model);
out.extend(diagnostics::diagnostics(model));
(out, kept)
}
pub fn width(ui: &egui::Ui, text: &str) -> f32 {
let font = egui::TextStyle::Small.resolve(ui.style());
ui.fonts(|fonts| {
fonts
.layout_no_wrap(text.to_owned(), font, theme::INK)
.size()
.x
})
}
pub fn elide(ui: &egui::Ui, text: &str, room: f32) -> String {
let mut chars: Vec<char> = text.chars().collect();
let mut said = text.to_owned();
while width(ui, &said) > room && !chars.is_empty() {
chars.pop();
said = format!(
"{}{}",
chars.iter().collect::<String>(),
theme::glyph::ELIDED
);
}
said
}
fn cost(ui: &egui::Ui, run: &Run) -> f32 {
let gap = ui.spacing().item_spacing.x;
let mark = run
.mark
.map_or(0.0, |(glyph, _)| width(ui, &format!(" {glyph}")));
width(ui, SEP) + 2.0 * gap + width(ui, &run.text) + mark
}
pub fn fitting(ui: &egui::Ui, runs: &[Run], kept: usize, name: f32, room: f32) -> (usize, f32) {
let gap = ui.spacing().item_spacing.x;
let mut used = width(ui, LEAD) * 2.0 + name + 4.0 * gap;
let core: f32 = runs.iter().take(kept).map(|run| cost(ui, run)).sum();
if used + core > room {
let mut words: Vec<f32> = runs.iter().take(kept).map(|r| width(ui, &r.text)).collect();
words.sort_by(f32::total_cmp);
let mut left = room - used - (core - words.iter().sum::<f32>());
let mut n: f32 = words.iter().map(|_| 1.0).sum();
for said in words {
if said > left / n {
break;
}
left -= said;
n -= 1.0;
}
return (kept, (left / n).max(0.0));
}
used += core;
let mut shown = kept;
for run in runs.iter().skip(kept) {
used += cost(ui, run);
if used > room {
break;
}
shown += 1;
}
(shown, f32::INFINITY)
}
pub(crate) fn paint(ui: &mut egui::Ui, run: &Run, words: &str) -> (bool, egui::Rect) {
let Some(door) = run.door else {
let seat = ui.label(egui::RichText::new(words).small().color(run.ink));
return (false, seat.rect);
};
let seat = match run.mark {
Some(mark) => theme::paint::link_marked(ui, words, run.ink, mark),
None => theme::paint::link(ui, words, run.ink),
};
crate::ui::act::tag(&seat, selectors::ops(door));
(seat.clicked(), seat.rect)
}
pub fn render(ui: &mut egui::Ui, model: &mut Model) {
let (mut runs, kept) = runs(model);
let name = name(model);
let room = ui.available_width();
let named = elide(ui, &name.text, room / 4.0);
let noticed = model.notice.as_ref().map(|_| state(model));
if let Some(said) = noticed
&& fitting(ui, &runs, kept, width(ui, &named), room).0 < runs.len()
{
let words = egui::RichText::new(&said.text).small().color(said.ink);
ui.add(egui::Label::new(words).wrap());
if let Some(slot) = runs.iter_mut().find(|run| **run == said) {
*slot = diagnostics::calm(model);
}
}
let (shown, cut) = fitting(ui, &runs, kept, width(ui, &named), room);
let faint = |ui: &mut egui::Ui, text: &str| {
ui.label(egui::RichText::new(text).small().color(theme::INK_FAINT));
};
let mut fired = None;
ui.horizontal(|ui| {
faint(ui, LEAD);
for (at, run) in runs.iter().take(shown).enumerate() {
if at > 0 {
faint(ui, SEP);
}
let words = if at < kept {
elide(ui, &run.text, cut)
} else {
run.text.clone()
};
let (clicked, rect) = paint(ui, run, &words);
if let Some(Door::Drop(selector)) = run.door {
dropup::anchor(ui, model, selector, rect);
}
fired = clicked.then_some(run.door).flatten().or(fired);
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
fired = paint(ui, &name, &named)
.0
.then_some(name.door)
.flatten()
.or(fired);
faint(ui, LEAD);
});
});
match fired {
Some(Door::Row(row)) => {
crate::ui::slash::fire(model, row.word, "");
}
Some(Door::Drop(selector)) => dropup::toggle(ui, model, selector),
None => {}
}
}
#[cfg(test)]
mod tests;