Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 15 additions & 41 deletions crates/app/src/shot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//! restrict the run to a single palette. Captures land at
//! `<PLOTX_SHOT>/<theme>/<scene>.png`.

use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};

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

mod capture;
mod craft_shot;

const FIT_LO: f64 = 1.4;
Expand Down Expand Up @@ -88,6 +89,7 @@ enum Op {
RegionResult,
/// Open the result's synchronized read-only values.
RegionData,
History(bool),
XpsSetup,
CraftSetup,
XpsTab(plotx_core::state::XpsWorkbenchTab),
Expand Down Expand Up @@ -136,6 +138,9 @@ const SCENES: &[Scene] = &[
act(2, Op::Zoom(0.75)),
act(2, Op::Setup),
shot(8, "band"),
act(2, Op::History(true)),
shot(8, "operation_history"),
act(2, Op::History(false)),
act(2, Op::LineFit),
shot(10, "fitted"),
// The three widths bracket the Ribbon's width budget: 720 steps the
Expand Down Expand Up @@ -331,7 +336,7 @@ impl ShotDriver {
.collect()
});
for (rel, image) in shots {
if let Err(error) = save_png(&self.dir.join(format!("{rel}.png")), &image) {
if let Err(error) = capture::save_png(&self.dir.join(format!("{rel}.png")), &image) {
self.fail(app, ctx, format!("failed to save {rel}: {error}"));
return;
}
Expand Down Expand Up @@ -373,6 +378,12 @@ fn run_op(op: Op, app: &mut PlotxApp, ctx: &egui::Context) -> Result<(), String>
Op::DeltaCursor => delta_cursor(app)?,
Op::PinSymmetry => pin_symmetry(app)?,
Op::RegionResult => region_result(app),
Op::History(open) => {
app.session.ui.diagnostics_open = open;
ctx.data_mut(|data| {
data.insert_temp(egui::Id::new("operation_history_messages_tab"), true)
});
}
Op::RegionData => {
app.session.ui.sheet_open = Some(1);
app.session.ui.curve_fit_task_collapsed = true;
Expand Down Expand Up @@ -754,43 +765,6 @@ fn synthetic_cosy() -> plotx_io::NmrData2D {
}
}

fn save_png(path: &Path, image: &egui::ColorImage) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| format!("create {}: {error}", parent.display()))?;
}
let [width, height] = image.size;
// egui screenshots are opaque RGBA8, so straight-alpha encoding is exact.
image::save_buffer_with_format(
path,
image.as_raw(),
width as u32,
height as u32,
image::ColorType::Rgba8,
image::ImageFormat::Png,
)
.map_err(|error| format!("encode {}: {error}", path.display()))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn automated_exit_bypasses_dirty_project_prompt() {
let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default());
app.mark_document_dirty();
let ctx = egui::Context::default();

let output = ctx.run_ui(egui::RawInput::default(), |ui| {
request_exit(&mut app, ui.ctx());
});

assert!(app.session.allow_close);
let root = output
.viewport_output
.get(&egui::ViewportId::ROOT)
.expect("root viewport output");
assert!(root.commands.contains(&egui::ViewportCommand::Close));
}
}
#[path = "shot/tests.rs"]
mod tests;
19 changes: 19 additions & 0 deletions crates/app/src/shot/capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::path::Path;

pub(super) fn save_png(path: &Path, image: &egui::ColorImage) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| format!("create {}: {error}", parent.display()))?;
}
let [width, height] = image.size;
// egui screenshots are opaque RGBA8, so straight-alpha encoding is exact.
image::save_buffer_with_format(
path,
image.as_raw(),
width as u32,
height as u32,
image::ColorType::Rgba8,
image::ImageFormat::Png,
)
.map_err(|error| format!("encode {}: {error}", path.display()))
}
19 changes: 19 additions & 0 deletions crates/app/src/shot/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use super::*;

#[test]
fn automated_exit_bypasses_dirty_project_prompt() {
let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default());
app.mark_document_dirty();
let ctx = egui::Context::default();

let output = ctx.run_ui(egui::RawInput::default(), |ui| {
request_exit(&mut app, ui.ctx());
});

assert!(app.session.allow_close);
let root = output
.viewport_output
.get(&egui::ViewportId::ROOT)
.expect("root viewport output");
assert!(root.commands.contains(&egui::ViewportCommand::Close));
}
90 changes: 90 additions & 0 deletions crates/app/src/ui/activity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use egui::Ui;
use egui_phosphor::regular as icon;
use plotx_core::state::PlotxApp;

use super::clipboard_table::ClipboardTablePaste;
use super::commands::{self, CommandId};

pub(super) fn messages_tab_id() -> egui::Id {
egui::Id::new("operation_history_messages_tab")
}

pub(super) fn observe(app: &mut PlotxApp) {
app.session.status_history.observe(&app.session.status);
}

pub(crate) fn open_diagnostics(app: &mut PlotxApp, ctx: &egui::Context) {
app.session.ui.diagnostics_open = true;
ctx.data_mut(|data| data.insert_temp(messages_tab_id(), false));
}

pub(super) fn history_button(app: &mut PlotxApp, clipboard: &mut ClipboardTablePaste, ui: &mut Ui) {
let response = ui
.add_sized(
[
super::ribbon_chrome::SIDEBAR_TOGGLE_WIDTH,
ui.spacing().interact_size.y,
],
egui::Button::new(icon::CLOCK_COUNTER_CLOCKWISE)
.frame_when_inactive(false)
.selected(app.session.ui.diagnostics_open),
)
.on_hover_text(format!("Operation history\n{}", app.session.status));
if super::pending_feedback(app).is_some() {
ui.painter().circle_filled(
response.rect.right_top() + egui::vec2(-5.0, 5.0),
2.5,
ui.visuals().warn_fg_color,
);
}
if response.clicked() {
commands::execute(CommandId::OperationHistory, app, clipboard, ui.ctx());
ui.ctx()
.data_mut(|data| data.insert_temp(messages_tab_id(), true));
}
}

pub(super) fn messages(app: &PlotxApp, ui: &mut Ui) {
let count = app.session.status_history.messages().len();
ui.vertical(|ui| {
if count == 0 {
ui.weak("No messages yet. Open data to begin.");
}
for message in app.session.status_history.messages().rev() {
let elapsed = message.recorded_at.elapsed().unwrap_or_default().as_secs();
ui.horizontal_top(|ui| {
ui.add_sized(
[56.0, ui.spacing().interact_size.y],
egui::Label::new(
crate::typography::caption(format!(
"{}:{:02} ago",
elapsed / 60,
elapsed % 60
))
.color(ui.visuals().weak_text_color()),
),
);
ui.add(egui::Label::new(&message.text).wrap().selectable(true));
});
ui.separator();
}
});
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn diagnostic_links_reveal_details_after_browsing_messages() {
let ctx = egui::Context::default();
let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default());
ctx.data_mut(|data| data.insert_temp(messages_tab_id(), true));
open_diagnostics(&mut app, &ctx);
assert!(app.session.ui.diagnostics_open);
assert_eq!(
ctx.data(|data| data.get_temp::<bool>(messages_tab_id())),
Some(false)
);
}
}
2 changes: 1 addition & 1 deletion crates/app/src/ui/command_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ fn execute_inner(
app.session.updates.check_now();
app.open_settings();
}
CommandId::OperationHistory => app.session.ui.diagnostics_open = true,
CommandId::OperationHistory => super::activity::open_diagnostics(app, ctx),
CommandId::About => app.session.ui.about_open = true,
CommandId::SaveProcessingTemplate | CommandId::ApplyProcessingTemplate => {
if let Some(dataset) = app.active_dataset() {
Expand Down
83 changes: 51 additions & 32 deletions crates/app/src/ui/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ pub(super) fn diagnostic_history_window(app: &mut PlotxApp, ctx: &egui::Context)
if !app.session.ui.diagnostics_open {
return;
}
super::activity::observe(app);

let mut open = true;
let mut clear = false;
let tab_id = super::activity::messages_tab_id();
let mut messages_tab = ctx.data(|data| data.get_temp::<bool>(tab_id).unwrap_or(false));
let copied_text = app.session.sanitized_diagnostics_text();
let window = egui::Window::new("Operation history")
.default_width(620.0)
Expand Down Expand Up @@ -41,44 +44,60 @@ pub(super) fn diagnostic_history_window(app: &mut PlotxApp, ctx: &egui::Context)
});
});
ui.separator();
egui::ScrollArea::vertical().show(ui, |ui| {
let mut any = false;
for operation in app.session.operation_history.operations().rev() {
any = true;
ui.group(|ui| {
ui.horizontal_wrapped(|ui| {
ui.label(crate::typography::headline(format!(
"#{} {}",
operation.id,
operation.kind.as_str()
)));
ui.label(operation.outcome.as_str());
});
ui.label(&operation.summary);
for diagnostic in &operation.diagnostics {
ui.add(egui::Label::new(crate::typography::callout(&app.session.status)).wrap());
if let Some(di) = app.active_dataset() {
ui.weak(app.doc.datasets[di].summary());
}
ui.horizontal(|ui| {
ui.selectable_value(&mut messages_tab, true, "Messages");
ui.selectable_value(&mut messages_tab, false, "Diagnostics");
});
ui.separator();
egui::ScrollArea::vertical()
.id_salt(("history", messages_tab))
.show(ui, |ui| {
if messages_tab {
super::activity::messages(app, ui);
return;
}
let mut any = false;
for operation in app.session.operation_history.operations().rev() {
any = true;
ui.group(|ui| {
ui.horizontal_wrapped(|ui| {
ui.label(crate::typography::headline(format!(
"{} {}",
diagnostic.severity.as_str(),
diagnostic.code.as_str()
"#{} {}",
operation.id,
operation.kind.as_str()
)));
ui.label(&diagnostic.message);
ui.label(operation.outcome.as_str());
});
if let Some(source) = &diagnostic.source {
ui.weak(format!("source: {source}"));
}
for (key, value) in &diagnostic.context {
ui.weak(format!("{key}: {value}"));
ui.label(&operation.summary);
for diagnostic in &operation.diagnostics {
ui.horizontal_wrapped(|ui| {
ui.label(crate::typography::headline(format!(
"{} {}",
diagnostic.severity.as_str(),
diagnostic.code.as_str()
)));
ui.label(&diagnostic.message);
});
if let Some(source) = &diagnostic.source {
ui.weak(format!("source: {source}"));
}
for (key, value) in &diagnostic.context {
ui.weak(format!("{key}: {value}"));
}
}
}
});
ui.add_space(6.0);
}
if !any {
ui.weak("No structured operations have been recorded yet.");
}
});
});
ui.add_space(6.0);
}
if !any {
ui.weak("No structured operations have been recorded yet.");
}
});
});
ctx.data_mut(|data| data.insert_temp(tab_id, messages_tab));

if clear {
app.session.clear_operation_history();
Expand Down
Loading
Loading