use std::str::FromStr;
use eframe::egui::{Color32, ComboBox, Grid, RichText, TextEdit, Ui, Vec2};
use crate::{
gui::{
COL_SPACING, ROW_SPACING, lin_maps,
theme::{COLOR_ACTION, COLOR_INFO},
},
pcr::{PcrUi, PolymeraseType, TempTime, make_amplicon_tab},
primer::{Primer, PrimerDirection, TM_TARGET},
state::State,
util::RangeIncl,
};
fn temp_time_disp(tt: &TempTime, label: &str, ui: &mut Ui) {
ui.label(&format!("{label}:"));
ui.label(RichText::new(format!("{:.0}°C", tt.temp)).color(COLOR_INFO));
ui.label(RichText::new(format!("{}s", tt.time)).color(COLOR_INFO));
ui.end_row();
}
fn numerical_field<T>(label: &str, val: &mut T, default: T, ui: &mut Ui) -> bool
where
T: ToString + FromStr + Copy,
<T as FromStr>::Err: std::fmt::Debug,
{
let mut changed = false;
ui.label(&format!("{}: ", label));
let mut entry = val.to_string();
if ui
.add(TextEdit::singleline(&mut entry).desired_width(30.))
.changed()
{
*val = entry.parse().unwrap_or(default);
changed = true;
}
changed
}
fn primer_dropdown(
val: &mut usize,
primers: &[Primer],
direction: Option<PrimerDirection>,
id: usize,
ui: &mut Ui,
) {
if *val > primers.len() {
*val = 0;
}
let primer = &primers[*val];
ComboBox::from_id_salt(id)
.width(80.)
.selected_text(&primer.name)
.show_ui(ui, |ui| {
for (i, primer) in primers.iter().enumerate() {
if let Some(dir) = direction {
let mut dir_match = false;
for match_ in &primer.volatile.matches {
if match_.direction == dir {
dir_match = true;
}
}
if !dir_match {
continue;
}
}
ui.selectable_value(val, i, &primer.name);
}
});
}
fn pcr_sim(state: &mut State, ui: &mut Ui) {
let num_primers = state.generic[state.active].primers.len();
if state.ui.pcr.primer_fwd >= num_primers || state.ui.pcr.primer_rev >= num_primers {
state.ui.pcr.primer_fwd = 0;
state.ui.pcr.primer_rev = 0;
}
ui.heading("PCR product generation");
lin_maps::seq_lin_disp(
&state.generic[state.active],
false,
state.ui.selected_item,
&Vec::new(),
None,
&state.ui,
&state.volatile[state.active].restriction_enzyme_matches,
&state.restriction_enzyme_lib,
ui,
);
ui.add_space(ROW_SPACING / 2.);
if num_primers >= 2 {
ui.horizontal(|ui| {
ui.label("Fwd:");
primer_dropdown(
&mut state.ui.pcr.primer_fwd,
&state.generic[state.active].primers,
Some(PrimerDirection::Forward),
2,
ui,
);
ui.add_space(COL_SPACING);
ui.label("Rev:");
primer_dropdown(
&mut state.ui.pcr.primer_rev,
&state.generic[state.active].primers,
Some(PrimerDirection::Reverse),
3,
ui,
);
ui.add_space(COL_SPACING);
if state.ui.pcr.primer_fwd == state.ui.pcr.primer_rev {
ui.label("Select two different primers");
} else if state.generic[state.active].primers[state.ui.pcr.primer_fwd]
.volatile
.matches
.len()
== 1
&& state.generic[state.active].primers[state.ui.pcr.primer_rev]
.volatile
.matches
.len()
== 1
{
if ui
.button(RichText::new("Simulate PCR").color(COLOR_ACTION))
.clicked()
{
let fwd_primer =
state.generic[state.active].primers[state.ui.pcr.primer_fwd].clone();
let rev_primer =
state.generic[state.active].primers[state.ui.pcr.primer_rev].clone();
let range_fwd = fwd_primer.volatile.matches[0].range;
let range_rev = rev_primer.volatile.matches[0].range;
let range_combined = RangeIncl::new(range_fwd.start, range_rev.end);
let product_seq = if range_combined.start > range_combined.end {
range_combined
.index_seq_wrap(&state.generic[state.active].seq)
.unwrap()
} else {
range_combined
.index_seq(&state.generic[state.active].seq)
.unwrap()
.to_vec() };
make_amplicon_tab(state, product_seq, range_combined, fwd_primer, rev_primer);
}
} else {
ui.label("There must be exactly one match for each primer");
}
});
} else {
ui.label("(Add at least 2 primers to generate a PCR product)");
}
}
pub fn pcr_page(state: &mut State, ui: &mut Ui) {
pcr_sim(state, ui);
ui.add_space(ROW_SPACING * 2.);
ui.horizontal(|ui| {
ui.heading("PCR parameters");
if !state.generic[state.active].primers.is_empty() {
ui.add_space(COL_SPACING);
if ui.button("Load from primer: ").clicked() {
let primer = &state.generic[state.active].primers[state.ui.pcr.primer_selected];
if let Some(metrics) = &primer.volatile.metrics {
state.ui.pcr = PcrUi {
primer_tm: metrics.melting_temp,
product_len: state.get_seq().len(),
primer_selected: state.ui.pcr.primer_selected,
..Default::default()
};
}
state.sync_pcr();
}
primer_dropdown(
&mut state.ui.pcr.primer_selected,
&state.generic[state.active].primers,
None,
1,
ui,
);
}
});
ui.horizontal(|ui| {
ui.label("Primer TM");
let mut entry = format!("{:.0}", state.ui.pcr.primer_tm);
let response = ui.add(TextEdit::singleline(&mut entry).desired_width(20.));
if response.changed() {
state.ui.pcr.primer_tm = entry.parse().unwrap_or(TM_TARGET);
state.sync_pcr();
}
if numerical_field(
"Product size (bp)",
&mut state.ui.pcr.product_len,
1_000,
ui,
) || numerical_field("# cycles", &mut state.ui.pcr.num_cycles, 30, ui)
{
state.sync_pcr();
}
ui.label("Polymerase:");
let prev_poly = state.ui.pcr.polymerase_type;
ComboBox::from_id_salt(10)
.width(80.)
.selected_text(state.ui.pcr.polymerase_type.to_str())
.show_ui(ui, |ui| {
ui.selectable_value(
&mut state.ui.pcr.polymerase_type,
PolymeraseType::NormalFidelity,
PolymeraseType::NormalFidelity.to_str(),
);
ui.selectable_value(
&mut state.ui.pcr.polymerase_type,
PolymeraseType::HighFidelity,
PolymeraseType::HighFidelity.to_str(),
);
});
if state.ui.pcr.polymerase_type != prev_poly {
state.sync_pcr();
}
});
ui.add_space(ROW_SPACING);
Grid::new(0).spacing(Vec2::new(60., 0.)).show(ui, |ui| {
temp_time_disp(&state.pcr.initial_denaturation, "Initial denaturation", ui);
temp_time_disp(&state.pcr.denaturation, "Denaturation", ui);
temp_time_disp(&state.pcr.annealing, "Annealing", ui);
temp_time_disp(&state.pcr.extension, "Extension", ui);
temp_time_disp(&state.pcr.final_extension, "Final extension", ui);
ui.label("Number of cycles:".to_string());
ui.label(format!("{}", state.pcr.num_cycles));
ui.end_row();
});
}