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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ These instructions apply to the entire repository.
`docs/dist/`, or `docs/node_modules/`.
- Keep public documentation and user-visible behavior in sync.
- Never add credentials or confidential or proprietary data.
- Until the first stable release, persisted formats remain fixed at v1. Do not
add migrations, legacy readers, compatibility fallbacks, old serialization
adapters, or development-draft format handling. Framework-native default
filling such as Serde `#[serde(default)]` is sufficient.

## Product name

Expand Down
3 changes: 3 additions & 0 deletions crates/app/src/ui/canvas/board_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fn screen() -> egui::Rect {
fn workspace(board_rect: egui::Rect) -> crate::ui::workspace_geometry::WorkspaceGeometry {
crate::ui::workspace_geometry::WorkspaceGeometry {
board_rect,
task_card_bounds: board_rect,
fit_occluders: Vec::new(),
revision: 0,
}
Expand Down Expand Up @@ -235,6 +236,7 @@ fn fit_uses_the_open_space_below_a_floating_task_card_when_it_is_better() {
let board = egui::Rect::from_min_size(Pos2::ZERO, egui::vec2(1000.0, 700.0));
let geometry = crate::ui::workspace_geometry::WorkspaceGeometry {
board_rect: board,
task_card_bounds: board,
fit_occluders: vec![egui::Rect::from_min_max(
egui::pos2(680.0, 0.0),
egui::pos2(1000.0, 420.0),
Expand Down Expand Up @@ -263,6 +265,7 @@ fn floating_task_cards_do_not_change_a_manual_camera() {
let board = screen();
let geometry = crate::ui::workspace_geometry::WorkspaceGeometry {
board_rect: board,
task_card_bounds: board,
fit_occluders: vec![egui::Rect::from_min_max(
egui::pos2(1500.0, 0.0),
egui::pos2(2000.0, 900.0),
Expand Down
6 changes: 1 addition & 5 deletions crates/app/src/ui/canvas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,7 @@ pub fn render_central(app: &mut PlotxApp, ui: &mut Ui) {
// Raw-pointer gestures must not start through UI layered over the canvas.
let pointer_hits_canvas_layer = ui
.input(|input| input.pointer.hover_pos())
.is_none_or(|pos| {
ui.ctx()
.layer_id_at(pos)
.is_none_or(|layer| layer == ui.layer_id())
});
.is_none_or(|pos| super::tools::task_card::pointer_allows_canvas(app, ui, pos));
let pointer_owned = app.session.ui.interaction.is_active()
|| (ui.rect_contains_pointer(rect) && pointer_hits_canvas_layer);

Expand Down
79 changes: 37 additions & 42 deletions crates/app/src/ui/tools/craft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,14 @@ pub(crate) fn render_task(app: &mut PlotxApp, host: &mut Ui) {
let TaskCardGeometry {
pos,
width,
min_body_height,
max_body_height,
} = task_card::geometry_with_width(host, 420.0, 820.0);
body_height,
} = task_card::geometry(
app,
host,
TaskDockTab::Craft,
420.0,
app.session.ui.craft_task_collapsed,
);
let collapsed = app.session.ui.craft_task_collapsed;
let dark = host.visuals().dark_mode;
let mut close = false;
Expand All @@ -200,52 +205,42 @@ pub(crate) fn render_task(app: &mut PlotxApp, host: &mut Ui) {
task_card::area(host, area_id, pos).show(host.ctx(), |ui| {
ui.set_width(width);
crate::ui::card_frame(dark, egui::Margin::ZERO).show(ui, |ui| {
task_card::header(ui, area_id, "CRAFT", None::<&str>, |ui| {
if ui
.small_button(icon::X)
.on_hover_text("Close CRAFT")
.clicked()
{
close = true;
}
let glyph = if collapsed {
icon::CARET_DOWN
} else {
icon::CARET_UP
};
if ui
.small_button(glyph)
.on_hover_text(if collapsed {
"Expand CRAFT"
} else {
"Collapse CRAFT"
})
.clicked()
{
toggle_collapse = true;
}
});
if task_card::tab_bar(app, TaskDockTab::Craft, ui) {
ui.separator();
}
let nmr = app.doc.datasets[index].as_nmr().unwrap();
let runs = nmr.craft_runs.len();
task_card::header(ui, area_id, |ui| {
ui.label(crate::typography::headline("CRAFT"));
ui.weak(format!("{} points · {runs} run(s)", nmr.data.points.len()));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.small_button(icon::X)
.on_hover_text("Close CRAFT")
.clicked()
{
close = true;
}
let glyph = if collapsed {
icon::CARET_DOWN
} else {
icon::CARET_UP
};
if ui
.small_button(glyph)
.on_hover_text(if collapsed {
"Expand CRAFT"
} else {
"Collapse CRAFT"
})
.clicked()
{
toggle_collapse = true;
}
});
});
if !collapsed {
ui.separator();
task_card::resizable_body(
ui,
"craft_task_body_resize",
650.0,
min_body_height,
max_body_height,
|ui| body(app, index, ui),
);
task_card::sized_body(ui, body_height, |ui| body(app, index, ui));
}
});
if !collapsed {
task_card::resize_handles(app, ui, area_id, TaskDockTab::Craft, width, body_height);
}
});
if toggle_collapse {
app.session.ui.craft_task_collapsed = !collapsed;
Expand Down
4 changes: 2 additions & 2 deletions crates/app/src/ui/tools/craft/results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ pub(super) fn show(app: &mut PlotxApp, index: usize, ui: &mut Ui) {
}
});

ui.horizontal(|ui| {
ui.horizontal_wrapped(|ui| {
ui.selectable_value(
&mut app.session.ui.craft_result_tab,
CraftResultTab::Overview,
Expand Down Expand Up @@ -304,7 +304,7 @@ fn components(
.filter(|summary| summary.component_count > 0)
.map(|summary| summary.region)
.collect::<Vec<_>>();
ui.horizontal(|ui| {
ui.horizontal_wrapped(|ui| {
egui::ComboBox::from_id_salt(("craft_component_sort", run.id.0))
.selected_text(match app.session.ui.craft_component_sort {
CraftComponentSort::ChemicalShift => "Sort: ppm",
Expand Down
177 changes: 101 additions & 76 deletions crates/app/src/ui/tools/craft/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,90 +182,101 @@ fn settings(app: &mut PlotxApp, index: usize, invocation: &CraftInvocation, ui:
"3. Confirm acquisition and fit settings",
));
ui.collapsing("Advanced fit settings", |ui| {
egui::Grid::new("craft_advanced_settings")
.num_columns(4)
.show(ui, |ui| {
ui.label("Minimum A/N");
let mut value = invocation.params.min_amplitude_to_noise;
if ui
.add(DragValue::new(&mut value).range(0.1..=100.0).speed(0.1))
let mut value = invocation.params.min_amplitude_to_noise;
if setting_row(
ui,
"Minimum A/N",
invocation.sources.min_amplitude_to_noise,
&nmr,
&mut overrides.min_amplitude_to_noise,
|ui| {
ui.add(DragValue::new(&mut value).range(0.1..=100.0).speed(0.1))
.changed()
{
overrides.min_amplitude_to_noise = Some(value);
}
source_cell(invocation.sources.min_amplitude_to_noise, &nmr, ui);
reset_button(&mut overrides.min_amplitude_to_noise, ui);
ui.end_row();
},
) {
overrides.min_amplitude_to_noise = Some(value);
}

ui.label("Max components / fit window");
let mut value = invocation.params.max_components_per_fit_window;
if ui.add(DragValue::new(&mut value).range(1..=64)).changed() {
overrides.max_components_per_fit_window = Some(value);
}
source_cell(invocation.sources.max_components_per_fit_window, &nmr, ui);
reset_button(&mut overrides.max_components_per_fit_window, ui);
ui.end_row();
let mut value = invocation.params.max_components_per_fit_window;
if setting_row(
ui,
"Max components / fit window",
invocation.sources.max_components_per_fit_window,
&nmr,
&mut overrides.max_components_per_fit_window,
|ui| ui.add(DragValue::new(&mut value).range(1..=64)).changed(),
) {
overrides.max_components_per_fit_window = Some(value);
}

ui.label("Linewidth range (Hz)");
let mut value = invocation.params.linewidth_hz;
if ui
.horizontal(|ui| {
let first = ui
.add(DragValue::new(&mut value.0).range(0.001..=1_000.0))
.changed();
ui.label("to");
first
| ui.add(DragValue::new(&mut value.1).range(0.002..=2_000.0))
.changed()
})
.inner
{
overrides.linewidth_hz = Some(value);
}
source_cell(invocation.sources.linewidth_hz, &nmr, ui);
reset_button(&mut overrides.linewidth_hz, ui);
ui.end_row();
let mut value = invocation.params.linewidth_hz;
if setting_row(
ui,
"Linewidth range (Hz)",
invocation.sources.linewidth_hz,
&nmr,
&mut overrides.linewidth_hz,
|ui| {
let first = ui
.add(DragValue::new(&mut value.0).range(0.001..=1_000.0))
.changed();
ui.label("to");
first
| ui.add(DragValue::new(&mut value.1).range(0.002..=2_000.0))
.changed()
},
) {
overrides.linewidth_hz = Some(value);
}

ui.label("Fit window width (Hz)");
let mut value = invocation.params.max_fit_window_width_hz;
if ui
.add(DragValue::new(&mut value).range(10.0..=10_000.0))
let mut value = invocation.params.max_fit_window_width_hz;
if setting_row(
ui,
"Fit window width (Hz)",
invocation.sources.max_fit_window_width_hz,
&nmr,
&mut overrides.max_fit_window_width_hz,
|ui| {
ui.add(DragValue::new(&mut value).range(10.0..=10_000.0))
.changed()
{
overrides.max_fit_window_width_hz = Some(value);
}
source_cell(invocation.sources.max_fit_window_width_hz, &nmr, ui);
reset_button(&mut overrides.max_fit_window_width_hz, ui);
ui.end_row();
},
) {
overrides.max_fit_window_width_hz = Some(value);
}

ui.label("FIR taps");
let mut value = invocation.params.filter_taps;
if ui
.add(DragValue::new(&mut value).range(3..=4_095))
let mut value = invocation.params.filter_taps;
if setting_row(
ui,
"FIR taps",
invocation.sources.filter_taps,
&nmr,
&mut overrides.filter_taps,
|ui| {
ui.add(DragValue::new(&mut value).range(3..=4_095))
.changed()
{
overrides.filter_taps = Some(value | 1);
}
source_cell(invocation.sources.filter_taps, &nmr, ui);
reset_button(&mut overrides.filter_taps, ui);
ui.end_row();
},
) {
overrides.filter_taps = Some(value | 1);
}

ui.label("Max downsampled points");
let mut value = invocation.params.max_downsampled_points;
if ui
.add(DragValue::new(&mut value).range(64..=65_536))
let mut value = invocation.params.max_downsampled_points;
if setting_row(
ui,
"Max downsampled points",
invocation.sources.max_downsampled_points,
&nmr,
&mut overrides.max_downsampled_points,
|ui| {
ui.add(DragValue::new(&mut value).range(64..=65_536))
.changed()
{
overrides.max_downsampled_points = Some(value);
}
source_cell(invocation.sources.max_downsampled_points, &nmr, ui);
reset_button(&mut overrides.max_downsampled_points, ui);
ui.end_row();
});
},
) {
overrides.max_downsampled_points = Some(value);
}

if invocation.params.profile == CraftProfile::Ssfp {
let mut skip_ms = invocation.params.skip_duration_s * 1_000.0;
ui.horizontal(|ui| {
ui.horizontal_wrapped(|ui| {
ui.label("Skip initial");
if ui
.add(
Expand Down Expand Up @@ -350,7 +361,7 @@ fn regions(
let mut changed = false;
let mut remove = None;
for (position, region) in regions.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.horizontal_wrapped(|ui| {
changed |= ui
.add(DragValue::new(&mut region.start_ppm).suffix(" ppm"))
.changed();
Expand Down Expand Up @@ -431,7 +442,7 @@ fn regions(
fn run_controls(app: &mut PlotxApp, index: usize, ui: &mut Ui) {
let dataset = app.doc.datasets[index].resource_id();
if let Some(elapsed) = app.session.compute.progress(dataset, ComputeKind::Craft) {
ui.horizontal(|ui| {
ui.horizontal_wrapped(|ui| {
ui.spinner();
ui.label(format!("Running… {:.1} s", elapsed.as_secs_f32()));
});
Expand Down Expand Up @@ -466,8 +477,22 @@ fn run_controls(app: &mut PlotxApp, index: usize, ui: &mut Ui) {
}
}

fn source_cell(source: CraftParamSource, nmr: &plotx_core::state::NmrDataset, ui: &mut Ui) {
ui.weak(source_text(source, nmr));
fn setting_row<T>(
ui: &mut Ui,
label: &str,
source: CraftParamSource,
nmr: &plotx_core::state::NmrDataset,
reset: &mut Option<T>,
add_control: impl FnOnce(&mut Ui) -> bool,
) -> bool {
ui.horizontal_wrapped(|ui| {
ui.label(label);
let changed = add_control(ui);
ui.weak(source_text(source, nmr));
reset_button(reset, ui);
changed
})
.inner
}

fn source_text(source: CraftParamSource, _nmr: &plotx_core::state::NmrDataset) -> String {
Expand Down
Loading
Loading