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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ These instructions apply to the entire repository.
- Encoding applicability is attached to field rendering capabilities, never to
a data-domain enum. New domains gain an encoding by exposing its capability;
do not add domain branches or domain names to encoding/property registries.
- Scientific summaries take identity from importers, observation from field
descriptors, and context from active bindings; panel notes are user-authored.
- A persisted default, an invocation input, and result provenance are lifecycle
copies of one value, not parallel sources of state. Resolve them in priority
order: explicit input, target provenance, then default.
Expand Down
24 changes: 12 additions & 12 deletions crates/app/src/ui/canvas/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,26 +339,26 @@ pub(crate) fn paint_frame_captions(
) {
let bt = BoardTransform::from_board(app.session.board, screen);
let color = ui.visuals().text_color();
for canvas in &app.doc.canvases {
for (ci, canvas) in app.doc.canvases.iter().enumerate() {
if !canvas.caption_visible {
continue;
}
let mut lines: Vec<String> = Vec::new();
if !canvas.caption.trim().is_empty() {
lines.push(canvas.caption.clone());
}
lines.extend(
canvas
.panel_notes()
.into_iter()
.map(|(letter, note)| format!("{letter} — {note}")),
);
let lines = frame_caption_lines(app, ci);
if lines.is_empty() {
continue;
}
let page = bt.page_screen_rect(canvas);
let font = egui::FontId::proportional((11.0 * bt.zoom).clamp(7.0, 28.0));
let galley = painter.layout(lines.join("\n"), font, color, page.width().max(1.0));
let galley = painter.layout(
lines
.into_iter()
.map(|line| line.text)
.collect::<Vec<_>>()
.join("\n"),
font,
color,
page.width().max(1.0),
);
let top_left = Pos2::new(page.left(), page.bottom() + CAPTION_GAP_PX);
if !screen.intersects(egui::Rect::from_min_size(top_left, galley.size())) {
continue;
Expand Down
44 changes: 44 additions & 0 deletions crates/app/src/ui/canvas/board_caption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use super::*;

#[derive(Clone)]
pub(crate) struct FrameCaptionLine {
pub text: String,
pub panel_note: Option<ObjectId>,
}

/// One canonical board representation: derived scientific summary first,
/// followed by explicitly authored page and panel notes.
pub(crate) fn frame_caption_lines(app: &PlotxApp, ci: usize) -> Vec<FrameCaptionLine> {
let Some(canvas) = app.doc.canvases.get(ci) else {
return Vec::new();
};
let mut lines = app
.canvas_scientific_summary(ci)
.formatted_lines()
.into_iter()
.map(|text| FrameCaptionLine {
text,
panel_note: None,
})
.collect::<Vec<_>>();
if !canvas.caption.trim().is_empty() {
lines.push(FrameCaptionLine {
text: format!("Note: {}", canvas.caption.trim()),
panel_note: None,
});
}
lines.extend(
canvas
.panel_note_entries()
.into_iter()
.map(|(id, letter, note)| FrameCaptionLine {
text: if letter.is_empty() {
format!("Note: {note}")
} else {
format!("{letter} note — {note}")
},
panel_note: Some(id),
}),
);
lines
}
44 changes: 13 additions & 31 deletions crates/app/src/ui/canvas/board_notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,16 @@ pub(crate) fn handle_frame_caption_interactions(
let page = bt.page_screen_rect(canvas);
let font = egui::FontId::proportional((11.0 * bt.zoom).clamp(7.0, 28.0));
let mut y = page.bottom() + CAPTION_GAP_PX;
if !canvas.caption.trim().is_empty() {
let galley = ui.painter().layout(
canvas.caption.clone(),
font.clone(),
color,
page.width().max(1.0),
);
y += galley.size().y;
}

let entries = canvas.panel_note_entries();
for (object_id, letter, note) in entries {
let text = format!("{letter} - {note}");
for line in frame_caption_lines(app, ci) {
let text = line.text;
let galley = ui
.painter()
.layout(text, font.clone(), color, page.width().max(1.0));
let row = egui::Rect::from_min_size(Pos2::new(page.left(), y), galley.size());
y += galley.size().y;
let Some(object_id) = line.panel_note else {
continue;
};
if !screen.intersects(row) {
continue;
}
Expand All @@ -43,7 +35,7 @@ pub(crate) fn handle_frame_caption_interactions(
ui.id().with(("panel_note_row", ci, object_id)),
Sense::click(),
)
.on_hover_text("Click to edit this panel description.");
.on_hover_text("Click to edit this panel note.");
if resp.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
consumed = true;
Expand All @@ -57,11 +49,11 @@ pub(crate) fn handle_frame_caption_interactions(
resp.context_menu(|ui| {
app.session.active_canvas = Some(ci);
app.select_object(ci, object_id);
if ui.button("Edit description in place").clicked() {
if ui.button("Edit note in place").clicked() {
open_inline_panel_note_editor(app, ci, object_id);
ui.close();
}
if ui.button("Edit description in dialog").clicked() {
if ui.button("Edit note in dialog").clicked() {
app.session.ui.panel_note_inline_edit = None;
open_panel_note_editor(app, ci, object_id);
ui.close();
Expand Down Expand Up @@ -159,23 +151,13 @@ fn panel_note_row_rect(
let color = ui.visuals().text_color();
let font = egui::FontId::proportional((11.0 * bt.zoom).clamp(7.0, 28.0));
let mut y = page.bottom() + CAPTION_GAP_PX;
if !canvas.caption.trim().is_empty() {
let galley = ui.painter().layout(
canvas.caption.clone(),
font.clone(),
color,
page.width().max(1.0),
);
y += galley.size().y;
}

for (id, letter, note) in canvas.panel_note_entries() {
let text = format!("{letter} - {note}");
for line in frame_caption_lines(app, ci) {
let text = line.text;
let galley = ui
.painter()
.layout(text, font.clone(), color, page.width().max(1.0));
let row = egui::Rect::from_min_size(Pos2::new(page.left(), y), galley.size());
if id == object_id {
if line.panel_note == Some(object_id) {
return Some(row);
}
y += galley.size().y;
Expand All @@ -197,7 +179,7 @@ fn commit_inline_panel_note_edit(app: &mut PlotxApp) {
return;
};
app.execute_action(Action::set_panel_meta(ci, id, before, after));
app.session.status = "Panel description updated.".to_owned();
app.session.status = "Panel note updated.".to_owned();
}

fn cancel_inline_panel_note_edit(app: &mut PlotxApp) {
Expand All @@ -208,5 +190,5 @@ fn cancel_inline_panel_note_edit(app: &mut PlotxApp) {
if let Some(canvas) = app.doc.canvases.get_mut(ci) {
canvas.set_panel_meta_for_content(id, before);
}
app.session.status = "Panel description edit cancelled.".to_owned();
app.session.status = "Panel note edit cancelled.".to_owned();
}
2 changes: 2 additions & 0 deletions crates/app/src/ui/canvas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const SNAP_PX: f32 = 6.0;

mod authoring;
mod board;
mod board_caption;
mod board_marquee;
mod board_notes;
mod breadcrumb;
Expand Down Expand Up @@ -60,6 +61,7 @@ mod tiling;

pub(crate) use authoring::*;
pub(crate) use board::*;
pub(crate) use board_caption::*;
pub(crate) use board_notes::*;
pub(crate) use breadcrumb::*;
pub(crate) use chrome::*;
Expand Down
7 changes: 3 additions & 4 deletions crates/app/src/ui/canvas/panel_notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub(crate) fn handle_panel_label_interactions(
let id = ui.id().with(("panel_label", ci, object_id));
let resp = ui
.interact(label_rect, id, Sense::click_and_drag())
.on_hover_text("Double-click to edit this panel description");
.on_hover_text("Double-click to edit this panel note");
let mut consumed =
label_hovered || resp.hovered() || matches!(app.interaction(), Interaction::PanelLabel(_));

Expand All @@ -41,8 +41,7 @@ pub(crate) fn handle_panel_label_interactions(
if matches!(app.interaction(), Interaction::Object(_)) {
app.reset_interaction();
}
app.session.status =
"Panel letter selected. Double-click to edit its description.".to_owned();
app.session.status = "Panel letter selected. Double-click to edit its note.".to_owned();
if let (Some(pointer), Some(panel)) = (
hover,
app.doc.canvases[ci].panel_meta_for_content(object_id),
Expand All @@ -65,7 +64,7 @@ pub(crate) fn handle_panel_label_interactions(

resp.context_menu(|ui| {
app.select_panel_label(ci, object_id);
if ui.button("Edit panel description").clicked() {
if ui.button("Edit panel note").clicked() {
open_panel_note_editor(app, ci, object_id);
ui.close();
}
Expand Down
6 changes: 3 additions & 3 deletions crates/app/src/ui/properties/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub const PRESENTATIONS: &[PropertyPresentation] = &[
object_entry(object::CHART_COLORMAP, "Colormap", CHART_HOME),
object_entry(object::CHART_VIEW_AZIMUTH, "Azimuth", CHART_HOME),
object_entry(object::CHART_VIEW_ELEVATION, "Elevation", CHART_HOME),
object_entry(object::PANEL_USER_NOTE, "Description", PANEL_HOME),
object_entry(object::PANEL_USER_NOTE, "Note", PANEL_HOME),
object_entry(object::PANEL_VISIBLE, "Show letter", PANEL_HOME),
object_entry(object::TEXT, "Text", TEXT_HOME),
object_entry(object::TEXT_FONT_SIZE, "Size", TEXT_HOME),
Expand Down Expand Up @@ -397,8 +397,8 @@ pub const PRESENTATIONS: &[PropertyPresentation] = &[
},
PropertyPresentation {
id: canvas::CAPTION_VISIBLE,
localized_label: LocalizedText("Show caption below page"),
localized_aliases: &[LocalizedText("caption visibility")],
localized_label: LocalizedText("Show summary below page"),
localized_aliases: &[LocalizedText("summary visibility")],
home_route: CANVAS_CAPTION_HOME,
canvas_step: false,
uses_canvas_length_unit: false,
Expand Down
10 changes: 6 additions & 4 deletions crates/app/src/ui/windows/canvas_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,15 @@ pub(in crate::ui) fn canvas_settings_window(app: &mut PlotxApp, ctx: &egui::Cont
});

crate::ui::properties::panel::canvas_caption_section(app, &target, ui);
ui.weak("Shown below the page on the board only — not exported or presented.");
ui.weak(
"The scientific summary is generated automatically. This optional page note is shown after it on the board only.",
);

let resp = ui.add(
egui::TextEdit::multiline(&mut app.doc.canvases[ci].caption)
.desired_width(340.0)
.desired_rows(3)
.hint_text("e.g. Figure 1. Concentration vs. time…"),
.hint_text("Optional page note…"),
);
if resp.gained_focus() {
app.session.ui.caption_edit_before = Some((
Expand All @@ -97,12 +99,12 @@ pub(in crate::ui) fn canvas_settings_window(app: &mut PlotxApp, ctx: &egui::Cont
}
}

/// Notes are auto-listed below the page on the board.
/// User notes are listed after the automatic scientific summary.
fn panels_section(app: &mut PlotxApp, ci: usize, ui: &mut Ui) {
ui.label(crate::typography::headline("Panels"));
ui.add_space(6.0);

ui.weak("Letters are top-left in each plot; notes list below the page (board only).");
ui.weak("Letters identify multi-panel plots; optional notes follow the scientific summary.");
ui.add_space(4.0);

let order = app.doc.canvases[ci].plot_reading_order();
Expand Down
6 changes: 1 addition & 5 deletions crates/core/src/actions/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ fn insert_dataset_existing_canvas_does_not_select_inserted_object() {
let inserted_id = app.doc.canvases[0].next_object_id;
let dataset_index = app.doc.datasets.len();
let dataset = Dataset::Nmr(Box::new(NmrDataset::load(synthetic_1d())));
let expected_note = crate::workflow::dataset_title(&dataset);

app.execute_action(Action::InsertDatasetWithCanvas {
dataset_index,
Expand All @@ -159,10 +158,7 @@ fn insert_dataset_existing_canvas_does_not_select_inserted_object() {
assert_eq!(app.doc.canvases[0].objects.len(), 2);
assert_eq!(app.doc.canvases[0].selected_object, None);
let panel = app.doc.canvases[0].parent_panel(inserted_id).unwrap();
assert_eq!(
app.doc.canvases[0].panel(panel).unwrap().note,
expected_note
);
assert!(app.doc.canvases[0].panel(panel).unwrap().note.is_empty());
app.doc.canvases[0].selected_object = Some(inserted_id);

app.undo();
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/actions/tests/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ fn plain_then_ctrl_click_selects_two_datasets_for_stacking() {
canvas.panel_letter(canvas.objects[0].id).as_deref(),
Some("a")
);
assert_eq!(canvas.panel_notes().len(), 1);
assert!(canvas.panel_notes().is_empty());
let plot = canvas.objects[0].plot().unwrap();
assert_ne!(
plot.binding.series[0].primary_color(),
Expand Down
6 changes: 5 additions & 1 deletion crates/core/src/figures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ pub fn build_stack_figure(stack: &StackSpectrum) -> Figure {
}

pub(crate) fn axis_label(nucleus: &str) -> String {
format!("{} chemical shift (ppm)", format_nucleus(nucleus))
}

pub(crate) fn format_nucleus(nucleus: &str) -> String {
let mut formatted = String::new();
let mut chars = nucleus.chars().peekable();
while chars.peek().is_some_and(char::is_ascii_digit) {
Expand All @@ -235,7 +239,7 @@ pub(crate) fn axis_label(nucleus: &str) -> String {
});
}
formatted.extend(chars);
format!("{formatted} chemical shift (ppm)")
formatted
}

/// Build a DOSY contour figure from a per-column diffusion map: x = chemical
Expand Down
Loading
Loading