forked from 1b-agent/fieldcraft-upstream
ui: jobs with progress, refusals listed, datum preview, form filters and defaults
- A solve or a mesher runs as a job with one progress mechanism: the overlay shows the step, stage, count and elapsed time with a bar when bounded, OK waits while it runs, and the status sums it up (elements, smallest angle, time) (#322, #357). - Over the control channel such a command is a job too: answered when it ends however long it runs, or at once with wait: false, then job.status, job.wait, job.cancel; ui.inspect gives job; MCP waits for it (#367). - A command that cannot run yet lists what it misses in the context panel, each with a button to the command that provides it (#323). - A cancelled or refused solve stays in its workspace (#324, test). - A mesher form starts from the parameters of the mesh it would replace (#343). - Seed Edges and edge/vertex/face datum points switch the selection filter while open and restore it (#341). - Datum forms draw the datum live, the Partition form shows its datum, and new datums and partitions get the next free name (#347). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
3eaca26246
commit
4ee855bb27
11 files changed
+447
-28
No files matched your search
@@ -17,7 +17,12 @@ pub const WIDTH: f32 = 310.0;
|
||||
|
||||
/// Whether the panel has anything to show.
|
||||
pub fn has_context(app: &mut ModelApp) -> bool {
|
||||
app.dialog.is_some() || app.selected.is_some() || app.selected_seed.is_some() || app.reply.is_some() || app.plot().is_some()
|
||||
app.dialog.is_some()
|
||||
|| app.refused.is_some()
|
||||
|| app.selected.is_some()
|
||||
|| app.selected_seed.is_some()
|
||||
|| app.reply.is_some()
|
||||
|| app.plot().is_some()
|
||||
}
|
||||
|
||||
pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
@@ -37,6 +42,10 @@ pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
form(app, ui, &t);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
if app.refused.is_some() {
|
||||
refused(app, ui, &t);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
if let Some((kind, name)) = app.selected.clone() {
|
||||
properties(app, ui, &t, kind, &name);
|
||||
ui.add_space(10.0);
|
||||
@@ -67,6 +76,35 @@ pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// A command that cannot run yet: what it misses, each with a button that starts the command
|
||||
/// that provides it (as the browser's ghost rows do).
|
||||
fn refused(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
let Some(u) = app.refused.clone() else { return };
|
||||
let mut close = false;
|
||||
heading(ui, t, Level::Title, |ui| {
|
||||
ui.label(RichText::new(format!("{} — NOT YET", crate::command_label(u.command).to_uppercase())).font(label_font(ui.ctx(), 12.0)));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
close = ui.small_button("×").on_hover_text("Dismiss").clicked();
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.label(RichText::new("It needs:").color(t.text_dim));
|
||||
let mut start = None;
|
||||
for fieldcraft_engine::requirement::Missing(r) in &u.missing {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(RichText::new(format!("• {}", r.what())).color(t.text));
|
||||
if ui.button(format!("{}…", r.place())).on_hover_text(r.provider()).clicked() {
|
||||
start = Some(r.provider());
|
||||
}
|
||||
});
|
||||
}
|
||||
if close {
|
||||
app.refused = None;
|
||||
} else if let Some(command) = start {
|
||||
let _ = app.start(command);
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON shorter than this is shown pretty-printed in the JSON view.
|
||||
const MAX_PRETTY: usize = 100_000;
|
||||
|
||||
@@ -76,6 +114,8 @@ const MAX_PRETTY: usize = 100_000;
|
||||
/// coupling) has a Target row that takes the viewport's selection.
|
||||
fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
let selected = (!app.pick.selection.is_empty()).then(|| app.pick.selection.summary());
|
||||
// While a job runs (this form's mesher, a solve) OK waits for it.
|
||||
let running = app.busy().map(str::to_string);
|
||||
let units = app.session.model.units();
|
||||
let session = &app.session;
|
||||
// The set Use selection will make on OK is offered with the others.
|
||||
@@ -191,9 +231,12 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
ui.label(RichText::new(spec.params).small().color(t.text_dim));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let ready = invalid == 0 && missing.is_empty();
|
||||
let not_ready =
|
||||
|| if invalid > 0 { format!("{missing}\nA field holds something that is not a value").trim().to_string() } else { missing.clone() };
|
||||
let ready = invalid == 0 && missing.is_empty() && running.is_none();
|
||||
let not_ready = || match &running {
|
||||
Some(job) => format!("{job} is running: wait for it or cancel it"),
|
||||
None if invalid > 0 => format!("{missing}\nA field holds something that is not a value").trim().to_string(),
|
||||
None => missing.clone(),
|
||||
};
|
||||
let mut attempted = false;
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
@@ -201,7 +244,7 @@ fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
let ok =
|
||||
ui.add_enabled(ready, egui::Button::new(RichText::new("OK").color(egui::Color32::WHITE)).fill(t.accent).min_size(vec2(64.0, 0.0)));
|
||||
// A click on the greyed OK shows every problem, beside its field.
|
||||
attempted = !ready && ok.contains_pointer() && ui.input(|i| i.pointer.primary_clicked());
|
||||
attempted = !ready && running.is_none() && ok.contains_pointer() && ui.input(|i| i.pointer.primary_clicked());
|
||||
run = ok
|
||||
.on_disabled_hover_ui(|ui| {
|
||||
ui.label(not_ready());
|
||||
|
||||
Reference in new issue
Block a user