use super::ShellState;
use super::menus::{BallRef, Target};
use super::start_rows::{ball_ref, continue_row, new_ball_form, ready_row};
use crate::AppModel;
use crate::board::Column;
use crate::cli_outbound::Cli;
use crate::nav::menu::Seat;
use crate::start::StartInputs;
use std::collections::HashMap;
pub fn board(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
bl: &Cli,
) {
if let Some(failure) = model.last_failure(crate::opslog::Origin::Balls) {
super::banner::failure_banner(ui, model, &failure);
}
if let Some(hint) = model.empty_project_hint() {
ui.weak(hint.lead);
ui.add(
egui::Label::new(egui::RichText::new(hint.command).monospace())
.wrap()
.selectable(true),
)
.on_hover_text(
"The command that registers a project with yog — selectable, so you \
can copy it out and run it in a terminal. No key of its own: Tab reaches \
it, then copy.",
);
}
columns(ui, model, state, lernie, bl);
ui.separator();
let projects = model.project_paths();
for (project, label) in projects.iter().zip(crate::projects::labels(&projects)) {
new_ball_form(ui, model, state, lernie, bl, project, &label);
}
}
fn columns(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
bl: &Cli,
) {
let board = model.board();
for fleet in &board.fleet {
ui.horizontal(|ui| {
ui.label(egui::RichText::new("fleet").strong());
ui.weak(fleet.label());
})
.response
.on_hover_text(format!(
"An armed loop on this workspace, taking ready balls from {}. \
It claims and starts; it releases a claim whose conversations have \
gone quiet past the lease; it never stops anything that is running. \
Every spawn and reap is a row on the ops trail.",
fleet.project.display()
));
if let Some(refusal) = &fleet.ceiling {
ui.colored_label(crate::theme::ICHOR, refusal);
}
}
let affordances = Affordances {
starts: keyed(model.startable()),
resumes: keyed(model.resumable()),
};
for column in Column::ALL {
let rows = board.in_column(column);
if rows.is_empty() {
continue;
}
egui::CollapsingHeader::new(format!("{} ({})", column.word(), rows.len()))
.id_salt(column.word())
.default_open(true)
.show(ui, |ui| {
for row in rows {
board_row(ui, model, state, lernie, bl, &row, &affordances);
}
})
.header_response
.on_hover_text(column_hint(column));
}
}
fn column_hint(column: Column) -> &'static str {
match column {
Column::Ready => "claimable now: unclaimed, with every dependency resolved.",
Column::Gated => {
"claimable, but not deliverable: a close-gate is still open. You can \
start the work; the ball cannot close until the ball named on the row \
closes first."
}
Column::Claimed => {
"someone holds it. The rows beneath name the conversations working \
it and what they have spent."
}
Column::Blocked => "not claimable yet: a dependency is still open.",
}
}
struct Affordances {
starts: HashMap<String, StartInputs>,
resumes: HashMap<String, StartInputs>,
}
fn keyed(inputs: Vec<StartInputs>) -> HashMap<String, StartInputs> {
inputs
.into_iter()
.filter_map(|i| Some((ball_ref(&i.payload)?.1, i)))
.collect()
}
fn board_row(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
bl: &Cli,
row: &crate::board::BoardRow,
affordances: &Affordances,
) {
match (
row.column,
affordances.starts.get(&row.id),
affordances.resumes.get(&row.id),
) {
(Column::Claimed, _, Some(inputs)) => {
continue_row(ui, model, state, lernie, bl, inputs.clone());
}
(Column::Ready | Column::Gated, Some(inputs), _) => {
ready_row(ui, model, state, lernie, bl, inputs.clone());
}
_ => read_row(ui, model, state, lernie, bl, row),
}
ui.indent(("board-facts", row.id.as_str()), |ui| {
for gate in &row.gates {
ui.weak(format!("⊣ gate {}: {}", gate.id, gate.title))
.on_hover_text(
"This ball cannot close until that one does. Closing it is what mints \
the gate — nothing here releases it.",
);
}
for drone in &row.drones {
ui.weak(format!("↳ {}", drone.name)).on_hover_text(
"The conversation working this ball — the same object the conversation list \
above shows. Its goal names this ball.",
);
}
if let Some(figure) = &row.spend {
ui.horizontal(|ui| {
ui.weak("spend:");
crate::spend::render(ui, figure);
});
}
if let Some(figure) = &row.rollup {
ui.horizontal(|ui| {
ui.weak("epic:");
crate::spend::render(ui, figure);
})
.response
.on_hover_text(
"This ball plus its live subtree, summed across every workspace those balls \
are claimed in. A closed child leaves the live set, and its spend leaves \
this figure with it.",
);
}
});
}
fn read_row(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
bl: &Cli,
row: &crate::board::BoardRow,
) {
let label = ui
.add(
egui::Label::new(egui::RichText::new(format!("{}: {}", row.id, row.title)).weak())
.sense(egui::Sense::click()),
)
.on_hover_text(
"Right-click for this ball's actions — Close, Release, Move. Each is also \
a key or a line: (c), (r), `/move [id] <to>`.",
);
let seat = Seat::BallRow {
state: row.state,
assign_to: model.focused_ws_name(),
move_to: model.move_targets(row.claimant.as_deref().unwrap_or_default()),
};
let target = Target::Ball(BallRef {
project: row.project.clone(),
id: row.id.clone(),
owner: row.claimant.clone().unwrap_or_default(),
});
super::menus::attach(&label, seat, &target, model, state, lernie, bl);
}