use crate::ui::{Model, theme};
pub const LEAD: &str = theme::glyph::LEAD;
pub const SEP: &str = theme::glyph::SEP;
pub const READY: &str = "ready";
pub const ENGINES: &str = "engines";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Run {
pub text: String,
pub ink: egui::Color32,
}
fn run(text: impl Into<String>, ink: egui::Color32) -> Run {
Run {
text: text.into(),
ink,
}
}
pub fn state(model: &Model) -> Run {
if let Some(notice) = &model.notice {
let ink = match notice {
crate::ui::Notice::Said(_) => theme::accent(theme::State::Annotation),
_ => theme::accent(theme::State::Error),
};
return run(notice.line(), ink);
}
match selected(model) {
Some(row) => run(
format!(
"{} · {}",
row.state.label(),
crate::ui::convs::age(row.age_secs)
),
theme::state_ink(&row.state),
),
None => run(READY, theme::INK_WEAK),
}
}
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)
}
fn answered_by(model: &Model) -> Option<String> {
model
.transcript
.entries
.iter()
.rev()
.find_map(|entry| match &entry.kind {
crate::reply::transcript::EntryKind::Model { model_id, .. } => Some(model_id.clone()),
_ => None,
})
}
pub fn waiting(model: &Model) -> u64 {
model
.roster
.iter()
.flat_map(|chunk| chunk.walls.iter())
.map(|wall| wall.attention)
.sum()
}
pub fn facts(model: &Model) -> Vec<Run> {
let mut out = Vec::new();
if let Some(model_id) = answered_by(model) {
out.push(run(model_id, theme::INK_WEAK));
}
if let Some(agent) = &model.records.agent {
out.push(run(
crate::ui::records::header::costing(agent),
theme::INK_WEAK,
));
}
let asking = waiting(model);
if asking > 0 {
out.push(run(
format!("{asking} waiting"),
theme::accent(theme::State::Attention),
));
}
let engines = model.roster.len();
out.push(run(
if engines == 1 {
"1 engine".to_owned()
} else {
format!("{engines} engines")
},
theme::INK_WEAK,
));
out
}
pub fn name(model: &Model) -> String {
if let Some(row) = selected(model) {
return row.display;
}
model
.aim
.as_ref()
.map_or_else(|| ENGINES.to_owned(), |aim| aim.address.clone())
}
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 fitting(ui: &egui::Ui, state: &Run, facts: &[Run], name: &str, room: f32) -> usize {
let gap = ui.spacing().item_spacing.x;
let sep = width(ui, SEP) + 2.0 * gap;
let fixed = width(ui, LEAD) * 2.0 + width(ui, &state.text) + width(ui, name) + 6.0 * gap;
let mut used = fixed;
let mut shown = 0;
for fact in facts {
let more = sep + width(ui, &fact.text);
if used + more > room {
break;
}
used += more;
shown += 1;
}
shown
}
pub fn render(ui: &mut egui::Ui, model: &Model) {
let state = state(model);
let facts = facts(model);
let name = name(model);
let shown = fitting(ui, &state, &facts, &name, ui.available_width());
let small = |text: &str, ink| egui::RichText::new(text).small().color(ink);
ui.horizontal(|ui| {
ui.label(small(LEAD, theme::INK_FAINT));
ui.label(small(&state.text, state.ink));
for fact in facts.iter().take(shown) {
ui.label(small(SEP, theme::INK_FAINT));
ui.label(small(&fact.text, fact.ink));
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(small(&name, theme::INK));
ui.label(small(LEAD, theme::INK_FAINT));
});
});
}
#[cfg(test)]
mod tests;