Skip to content

Commit 3caeb25

Browse files
committed
feat(ui): replace the status bar with operation history
1 parent 5964140 commit 3caeb25

34 files changed

Lines changed: 323 additions & 212 deletions

crates/app/src/shot.rs

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//! restrict the run to a single palette. Captures land at
1414
//! `<PLOTX_SHOT>/<theme>/<scene>.png`.
1515
16-
use std::path::{Path, PathBuf};
16+
use std::path::PathBuf;
1717
use std::sync::Arc;
1818
use std::time::{Duration, Instant};
1919

@@ -31,6 +31,7 @@ use plotx_io::xps::{
3131
};
3232
use plotx_io::{AxisSource, Dim, Domain, NmrData, NmrData2D, PseudoAxis, PseudoKind, QuadMode};
3333

34+
mod capture;
3435
mod craft_shot;
3536

3637
const FIT_LO: f64 = 1.4;
@@ -88,6 +89,7 @@ enum Op {
8889
RegionResult,
8990
/// Open the result's synchronized read-only values.
9091
RegionData,
92+
History(bool),
9193
XpsSetup,
9294
CraftSetup,
9395
XpsTab(plotx_core::state::XpsWorkbenchTab),
@@ -136,6 +138,9 @@ const SCENES: &[Scene] = &[
136138
act(2, Op::Zoom(0.75)),
137139
act(2, Op::Setup),
138140
shot(8, "band"),
141+
act(2, Op::History(true)),
142+
shot(8, "operation_history"),
143+
act(2, Op::History(false)),
139144
act(2, Op::LineFit),
140145
shot(10, "fitted"),
141146
// The three widths bracket the Ribbon's width budget: 720 steps the
@@ -331,7 +336,7 @@ impl ShotDriver {
331336
.collect()
332337
});
333338
for (rel, image) in shots {
334-
if let Err(error) = save_png(&self.dir.join(format!("{rel}.png")), &image) {
339+
if let Err(error) = capture::save_png(&self.dir.join(format!("{rel}.png")), &image) {
335340
self.fail(app, ctx, format!("failed to save {rel}: {error}"));
336341
return;
337342
}
@@ -373,6 +378,12 @@ fn run_op(op: Op, app: &mut PlotxApp, ctx: &egui::Context) -> Result<(), String>
373378
Op::DeltaCursor => delta_cursor(app)?,
374379
Op::PinSymmetry => pin_symmetry(app)?,
375380
Op::RegionResult => region_result(app),
381+
Op::History(open) => {
382+
app.session.ui.diagnostics_open = open;
383+
ctx.data_mut(|data| {
384+
data.insert_temp(egui::Id::new("operation_history_messages_tab"), true)
385+
});
386+
}
376387
Op::RegionData => {
377388
app.session.ui.sheet_open = Some(1);
378389
app.session.ui.curve_fit_task_collapsed = true;
@@ -754,43 +765,6 @@ fn synthetic_cosy() -> plotx_io::NmrData2D {
754765
}
755766
}
756767

757-
fn save_png(path: &Path, image: &egui::ColorImage) -> Result<(), String> {
758-
if let Some(parent) = path.parent() {
759-
std::fs::create_dir_all(parent)
760-
.map_err(|error| format!("create {}: {error}", parent.display()))?;
761-
}
762-
let [width, height] = image.size;
763-
// egui screenshots are opaque RGBA8, so straight-alpha encoding is exact.
764-
image::save_buffer_with_format(
765-
path,
766-
image.as_raw(),
767-
width as u32,
768-
height as u32,
769-
image::ColorType::Rgba8,
770-
image::ImageFormat::Png,
771-
)
772-
.map_err(|error| format!("encode {}: {error}", path.display()))
773-
}
774-
775768
#[cfg(test)]
776-
mod tests {
777-
use super::*;
778-
779-
#[test]
780-
fn automated_exit_bypasses_dirty_project_prompt() {
781-
let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default());
782-
app.mark_document_dirty();
783-
let ctx = egui::Context::default();
784-
785-
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
786-
request_exit(&mut app, ui.ctx());
787-
});
788-
789-
assert!(app.session.allow_close);
790-
let root = output
791-
.viewport_output
792-
.get(&egui::ViewportId::ROOT)
793-
.expect("root viewport output");
794-
assert!(root.commands.contains(&egui::ViewportCommand::Close));
795-
}
796-
}
769+
#[path = "shot/tests.rs"]
770+
mod tests;

crates/app/src/shot/capture.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use std::path::Path;
2+
3+
pub(super) fn save_png(path: &Path, image: &egui::ColorImage) -> Result<(), String> {
4+
if let Some(parent) = path.parent() {
5+
std::fs::create_dir_all(parent)
6+
.map_err(|error| format!("create {}: {error}", parent.display()))?;
7+
}
8+
let [width, height] = image.size;
9+
// egui screenshots are opaque RGBA8, so straight-alpha encoding is exact.
10+
image::save_buffer_with_format(
11+
path,
12+
image.as_raw(),
13+
width as u32,
14+
height as u32,
15+
image::ColorType::Rgba8,
16+
image::ImageFormat::Png,
17+
)
18+
.map_err(|error| format!("encode {}: {error}", path.display()))
19+
}

crates/app/src/shot/tests.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use super::*;
2+
3+
#[test]
4+
fn automated_exit_bypasses_dirty_project_prompt() {
5+
let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default());
6+
app.mark_document_dirty();
7+
let ctx = egui::Context::default();
8+
9+
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
10+
request_exit(&mut app, ui.ctx());
11+
});
12+
13+
assert!(app.session.allow_close);
14+
let root = output
15+
.viewport_output
16+
.get(&egui::ViewportId::ROOT)
17+
.expect("root viewport output");
18+
assert!(root.commands.contains(&egui::ViewportCommand::Close));
19+
}

crates/app/src/ui/activity.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use egui::Ui;
2+
use egui_phosphor::regular as icon;
3+
use plotx_core::state::PlotxApp;
4+
5+
use super::clipboard_table::ClipboardTablePaste;
6+
use super::commands::{self, CommandId};
7+
8+
pub(super) fn messages_tab_id() -> egui::Id {
9+
egui::Id::new("operation_history_messages_tab")
10+
}
11+
12+
pub(super) fn observe(app: &mut PlotxApp) {
13+
app.session.status_history.observe(&app.session.status);
14+
}
15+
16+
pub(crate) fn open_diagnostics(app: &mut PlotxApp, ctx: &egui::Context) {
17+
app.session.ui.diagnostics_open = true;
18+
ctx.data_mut(|data| data.insert_temp(messages_tab_id(), false));
19+
}
20+
21+
pub(super) fn history_button(app: &mut PlotxApp, clipboard: &mut ClipboardTablePaste, ui: &mut Ui) {
22+
let response = ui
23+
.add_sized(
24+
[
25+
super::ribbon_chrome::SIDEBAR_TOGGLE_WIDTH,
26+
ui.spacing().interact_size.y,
27+
],
28+
egui::Button::new(icon::CLOCK_COUNTER_CLOCKWISE)
29+
.frame_when_inactive(false)
30+
.selected(app.session.ui.diagnostics_open),
31+
)
32+
.on_hover_text(format!("Operation history\n{}", app.session.status));
33+
if super::pending_feedback(app).is_some() {
34+
ui.painter().circle_filled(
35+
response.rect.right_top() + egui::vec2(-5.0, 5.0),
36+
2.5,
37+
ui.visuals().warn_fg_color,
38+
);
39+
}
40+
if response.clicked() {
41+
commands::execute(CommandId::OperationHistory, app, clipboard, ui.ctx());
42+
ui.ctx()
43+
.data_mut(|data| data.insert_temp(messages_tab_id(), true));
44+
}
45+
}
46+
47+
pub(super) fn messages(app: &PlotxApp, ui: &mut Ui) {
48+
let count = app.session.status_history.messages().len();
49+
ui.vertical(|ui| {
50+
if count == 0 {
51+
ui.weak("No messages yet. Open data to begin.");
52+
}
53+
for message in app.session.status_history.messages().rev() {
54+
let elapsed = message.recorded_at.elapsed().unwrap_or_default().as_secs();
55+
ui.horizontal_top(|ui| {
56+
ui.add_sized(
57+
[56.0, ui.spacing().interact_size.y],
58+
egui::Label::new(
59+
crate::typography::caption(format!(
60+
"{}:{:02} ago",
61+
elapsed / 60,
62+
elapsed % 60
63+
))
64+
.color(ui.visuals().weak_text_color()),
65+
),
66+
);
67+
ui.add(egui::Label::new(&message.text).wrap().selectable(true));
68+
});
69+
ui.separator();
70+
}
71+
});
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use super::*;
77+
78+
#[test]
79+
fn diagnostic_links_reveal_details_after_browsing_messages() {
80+
let ctx = egui::Context::default();
81+
let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default());
82+
ctx.data_mut(|data| data.insert_temp(messages_tab_id(), true));
83+
open_diagnostics(&mut app, &ctx);
84+
assert!(app.session.ui.diagnostics_open);
85+
assert_eq!(
86+
ctx.data(|data| data.get_temp::<bool>(messages_tab_id())),
87+
Some(false)
88+
);
89+
}
90+
}

crates/app/src/ui/command_exec.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ fn execute_inner(
164164
app.session.updates.check_now();
165165
app.open_settings();
166166
}
167-
CommandId::OperationHistory => app.session.ui.diagnostics_open = true,
167+
CommandId::OperationHistory => super::activity::open_diagnostics(app, ctx),
168168
CommandId::About => app.session.ui.about_open = true,
169169
CommandId::SaveProcessingTemplate | CommandId::ApplyProcessingTemplate => {
170170
if let Some(dataset) = app.active_dataset() {

crates/app/src/ui/diagnostics.rs

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ pub(super) fn diagnostic_history_window(app: &mut PlotxApp, ctx: &egui::Context)
44
if !app.session.ui.diagnostics_open {
55
return;
66
}
7+
super::activity::observe(app);
78

89
let mut open = true;
910
let mut clear = false;
11+
let tab_id = super::activity::messages_tab_id();
12+
let mut messages_tab = ctx.data(|data| data.get_temp::<bool>(tab_id).unwrap_or(false));
1013
let copied_text = app.session.sanitized_diagnostics_text();
1114
let window = egui::Window::new("Operation history")
1215
.default_width(620.0)
@@ -41,44 +44,60 @@ pub(super) fn diagnostic_history_window(app: &mut PlotxApp, ctx: &egui::Context)
4144
});
4245
});
4346
ui.separator();
44-
egui::ScrollArea::vertical().show(ui, |ui| {
45-
let mut any = false;
46-
for operation in app.session.operation_history.operations().rev() {
47-
any = true;
48-
ui.group(|ui| {
49-
ui.horizontal_wrapped(|ui| {
50-
ui.label(crate::typography::headline(format!(
51-
"#{} {}",
52-
operation.id,
53-
operation.kind.as_str()
54-
)));
55-
ui.label(operation.outcome.as_str());
56-
});
57-
ui.label(&operation.summary);
58-
for diagnostic in &operation.diagnostics {
47+
ui.add(egui::Label::new(crate::typography::callout(&app.session.status)).wrap());
48+
if let Some(di) = app.active_dataset() {
49+
ui.weak(app.doc.datasets[di].summary());
50+
}
51+
ui.horizontal(|ui| {
52+
ui.selectable_value(&mut messages_tab, true, "Messages");
53+
ui.selectable_value(&mut messages_tab, false, "Diagnostics");
54+
});
55+
ui.separator();
56+
egui::ScrollArea::vertical()
57+
.id_salt(("history", messages_tab))
58+
.show(ui, |ui| {
59+
if messages_tab {
60+
super::activity::messages(app, ui);
61+
return;
62+
}
63+
let mut any = false;
64+
for operation in app.session.operation_history.operations().rev() {
65+
any = true;
66+
ui.group(|ui| {
5967
ui.horizontal_wrapped(|ui| {
6068
ui.label(crate::typography::headline(format!(
61-
"{} {}",
62-
diagnostic.severity.as_str(),
63-
diagnostic.code.as_str()
69+
"#{} {}",
70+
operation.id,
71+
operation.kind.as_str()
6472
)));
65-
ui.label(&diagnostic.message);
73+
ui.label(operation.outcome.as_str());
6674
});
67-
if let Some(source) = &diagnostic.source {
68-
ui.weak(format!("source: {source}"));
69-
}
70-
for (key, value) in &diagnostic.context {
71-
ui.weak(format!("{key}: {value}"));
75+
ui.label(&operation.summary);
76+
for diagnostic in &operation.diagnostics {
77+
ui.horizontal_wrapped(|ui| {
78+
ui.label(crate::typography::headline(format!(
79+
"{} {}",
80+
diagnostic.severity.as_str(),
81+
diagnostic.code.as_str()
82+
)));
83+
ui.label(&diagnostic.message);
84+
});
85+
if let Some(source) = &diagnostic.source {
86+
ui.weak(format!("source: {source}"));
87+
}
88+
for (key, value) in &diagnostic.context {
89+
ui.weak(format!("{key}: {value}"));
90+
}
7291
}
73-
}
74-
});
75-
ui.add_space(6.0);
76-
}
77-
if !any {
78-
ui.weak("No structured operations have been recorded yet.");
79-
}
80-
});
92+
});
93+
ui.add_space(6.0);
94+
}
95+
if !any {
96+
ui.weak("No structured operations have been recorded yet.");
97+
}
98+
});
8199
});
100+
ctx.data_mut(|data| data.insert_temp(tab_id, messages_tab));
82101

83102
if clear {
84103
app.session.clear_operation_history();

0 commit comments

Comments
 (0)