rafia_stpa_gui/panels/
mod.rs

1// *****************************************************************************
2// Copyright (c) 2025 Codethink
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License, v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is
12// available at https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17use std::path::PathBuf;
18
19use components::{remove_button, IdOnlyLinkReference, LinkList, LinkReference};
20use eframe::egui::{self, Align, Layout};
21use egui_extras::{Column, TableBuilder};
22use indexmap::{IndexMap, IndexSet};
23use strum::IntoEnumIterator;
24
25use rafia_stpa::{
26    construct_link_map,
27    stpa::{
28        ca_analysis::{CaAnalyses, CaAnalysis, CaResult},
29        constraints::{Constraint, ConstraintType, Constraints, UcaLink},
30        control_loop_sequences::{ControlLoopSequence, ControlLoopSequences},
31        control_loops::{ControlLoop, ControlLoops},
32        elements::{Element, Elements},
33        hazards::{Hazard, Hazards, LossLink},
34        interactions::{Interaction, InteractionCategory, InteractionType, Interactions},
35        losses::{Loss, Losses},
36        scenarios::{CausalScenario, CausalScenarioResult, Scenario, Scenarios},
37        uca_contexts::{UcaContext, UcaContexts},
38        ucas::{Uca, Ucas},
39        CausalScenarioLink, ConstraintLink, ControlLoopLink, ControlLoopSequenceLink, ElementLink,
40        HazardLink, InteractionLink, StpaData, StpaProject, UcaContextLink, UcaType,
41    },
42};
43
44mod components;
45pub mod tabs;
46
47pub fn update_diagram(ui: &mut egui::Ui, diagram: Option<&url::Url>) {
48    if let Some(diagram) = diagram {
49        ui.image(diagram.as_str());
50    };
51}
52
53pub fn update_losses(ui: &mut egui::Ui, losses: &mut Losses) {
54    ui.with_layout(Layout::top_down(Align::Min), |ui| {
55        let new = if ui.button("Add loss").clicked() {
56            losses.losses.push(Loss {
57                id: format!("L{}", losses.losses.len() + 1),
58                description: "".to_string(),
59                category: IndexSet::new(),
60            });
61            true
62        } else {
63            false
64        };
65        ui.separator();
66
67        egui::ScrollArea::horizontal()
68            .auto_shrink(false)
69            .show(ui, |ui| {
70                let mut table = TableBuilder::new(ui)
71                    .column(Column::auto().resizable(true))
72                    .column(Column::auto().resizable(true))
73                    .column(Column::remainder().at_least(250.0));
74                if new {
75                    table = table.scroll_to_row(losses.losses.len() - 1, None);
76                }
77                table
78                    .header(20.0, |mut header| {
79                        header.col(|ui| {
80                            ui.heading("Loss ID");
81                        });
82                        header.col(|ui| {
83                            ui.heading("Loss Description");
84                        });
85                        header.col(|ui| {
86                            ui.heading("Loss Categories");
87                        });
88                    })
89                    .body(|mut body| {
90                        let mut remove_loss = None;
91                        for (index, loss) in &mut losses.losses.iter_mut().enumerate() {
92                            body.row(30.0, |mut row| {
93                                row.col(|ui| {
94                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
95                                        if remove_button(ui).clicked() {
96                                            remove_loss = Some(index);
97                                        };
98                                        egui::TextEdit::singleline(&mut loss.id)
99                                            .hint_text("Type something!")
100                                            .show(ui);
101                                    });
102                                });
103
104                                row.col(|ui| {
105                                    egui::TextEdit::singleline(&mut loss.description)
106                                        .hint_text("Type something!")
107                                        .show(ui);
108                                });
109                                row.col(|ui| {
110                                    components::EditableSet(&mut loss.category).show(ui);
111                                });
112                            });
113                        }
114                        if let Some(index) = remove_loss {
115                            losses.losses.remove(index);
116                        };
117                    });
118            });
119    });
120}
121
122pub fn update_hazards(ui: &mut egui::Ui, hazards: &mut Hazards, losses: &Losses) {
123    let loss_map = construct_link_map!(table = losses, link_type = LossLink);
124
125    ui.with_layout(Layout::top_down(Align::Min), |ui| {
126        let new = if ui.button("Add hazard").clicked() {
127            hazards.hazards.push(Hazard {
128                id: format!("H{}", hazards.hazards.len() + 1),
129                description: "".to_string(),
130                losses: vec![],
131                notes: "".to_string(),
132            });
133            true
134        } else {
135            false
136        };
137        ui.separator();
138        egui::ScrollArea::horizontal()
139            .auto_shrink(false)
140            .show(ui, |ui| {
141                let mut table = TableBuilder::new(ui)
142                    .column(Column::auto().resizable(true))
143                    .column(Column::auto().resizable(true))
144                    .column(Column::auto().resizable(true))
145                    .column(Column::remainder());
146                if new {
147                    table = table.scroll_to_row(hazards.hazards.len() - 1, None)
148                };
149                table
150                    .header(20.0, |mut header| {
151                        header.col(|ui| {
152                            ui.heading("ID");
153                        });
154                        header.col(|ui| {
155                            ui.heading("Description");
156                        });
157                        header.col(|ui| {
158                            ui.heading("Losses");
159                        });
160                        header.col(|ui| {
161                            ui.heading("Notes");
162                        });
163                    })
164                    .body(|mut body| {
165                        let mut remove_hazard = None;
166                        for (index, hazards) in hazards.hazards.iter_mut().enumerate() {
167                            body.row(30.0, |mut row| {
168                                row.col(|ui| {
169                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
170                                        if remove_button(ui).clicked() {
171                                            remove_hazard = Some(index);
172                                        };
173                                        egui::TextEdit::singleline(&mut hazards.id)
174                                            .hint_text("Type something!")
175                                            .show(ui);
176                                    });
177                                });
178                                row.col(|ui| {
179                                    egui::TextEdit::singleline(&mut hazards.description)
180                                        .hint_text("Type something!")
181                                        .show(ui);
182                                });
183                                row.col(|ui| {
184                                    components::LinkList {
185                                        links: &mut hazards.losses,
186                                        mapping: &loss_map,
187                                    }
188                                    .show(ui);
189                                });
190                                row.col(|ui| {
191                                    egui::TextEdit::multiline(&mut hazards.notes)
192                                        .hint_text("Type something!")
193                                        .show(ui);
194                                });
195                            });
196                        }
197                        if let Some(index) = remove_hazard {
198                            hazards.hazards.remove(index);
199                        };
200                    });
201            });
202    });
203}
204
205pub fn update_elements(ui: &mut egui::Ui, elements: &mut Elements) {
206    ui.with_layout(Layout::top_down(Align::Min), |ui| {
207        let new = if ui.button("Add element").clicked() {
208            elements.elements.push(Element {
209                id: format!("E{}", elements.elements.len() + 1),
210                name: "".to_string(),
211                responsibilities: vec![],
212                roles: IndexSet::new(),
213                notes: "".to_string(),
214            });
215            true
216        } else {
217            false
218        };
219        ui.separator();
220        egui::ScrollArea::horizontal()
221            .auto_shrink(false)
222            .show(ui, |ui| {
223                let mut table = TableBuilder::new(ui)
224                    .column(Column::auto().resizable(true))
225                    .column(Column::initial(250.0).resizable(true))
226                    .column(Column::initial(300.0).resizable(true))
227                    .column(Column::initial(250.0).resizable(true))
228                    .column(Column::remainder().at_least(250.0));
229                if new {
230                    table = table.scroll_to_row(elements.elements.len() - 1, None)
231                };
232
233                table
234                    .header(40.0, |mut header| {
235                        header.col(|ui| {
236                            ui.heading("ID");
237                        });
238                        header.col(|ui| {
239                            ui.heading("Name");
240                        });
241                        header.col(|ui| {
242                            ui.heading("Responsibilities");
243                        });
244                        header.col(|ui| {
245                            ui.heading("Roles");
246                        });
247                        header.col(|ui| {
248                            ui.heading("Notes");
249                        });
250                    })
251                    .body(|mut body| {
252                        let mut remove_element = None;
253                        for (index, element) in elements.elements.iter_mut().enumerate() {
254                            body.row(30.0, |mut row| {
255                                row.col(|ui| {
256                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
257                                        if remove_button(ui).clicked() {
258                                            remove_element = Some(index);
259                                        };
260                                        egui::TextEdit::singleline(&mut element.id)
261                                            .hint_text("Type something!")
262                                            .show(ui);
263                                    });
264                                });
265                                row.col(|ui| {
266                                    egui::TextEdit::singleline(&mut element.name)
267                                        .hint_text("Type something!")
268                                        .show(ui);
269                                });
270                                row.col(|ui| {
271                                    ui.vertical(|ui| {
272                                        components::TextList(&mut element.responsibilities)
273                                            .show(ui);
274                                    });
275                                });
276                                row.col(|ui| {
277                                    components::EditableSet(&mut element.roles).show(ui);
278                                });
279                                row.col(|ui| {
280                                    egui::TextEdit::multiline(&mut element.notes)
281                                        .hint_text("Type something!")
282                                        .show(ui);
283                                });
284                            });
285                        }
286                        if let Some(index) = remove_element {
287                            elements.elements.remove(index);
288                        };
289                    });
290            });
291    });
292}
293
294pub fn update_constraints(
295    ui: &mut egui::Ui,
296    constraints: &mut Constraints,
297    hazards: &Hazards,
298    ucas: &Ucas,
299    scenarios: &Scenarios,
300) {
301    let constraint_map = construct_link_map!(table = constraints, link_type = ConstraintLink);
302    let hazard_map = construct_link_map!(table = hazards, link_type = HazardLink);
303    let uca_map = construct_link_map!(
304        table = ucas,
305        link_type = UcaLink,
306        linked_to = uca_definition
307    );
308    let cs_map = construct_link_map!(
309        table = scenarios,
310        link_type = CausalScenarioLink,
311        linked_to = causal_scenario_definition
312    );
313
314    ui.with_layout(Layout::top_down(Align::Min), |ui| {
315        let new = if ui.button("Add constraints").clicked() {
316            constraints.constraints.push(Constraint {
317                id: format!("{}", constraints.constraints.len() + 1),
318                description: "".to_string(),
319                constraint_type: ConstraintType::Missing,
320                link_to_constraints: vec![],
321                link_to_hazards: vec![],
322                link_to_uca: vec![],
323                link_to_cs: vec![],
324                link_to_tsf: vec![],
325            });
326            true
327        } else {
328            false
329        };
330        ui.separator();
331        egui::ScrollArea::horizontal()
332            .auto_shrink(false)
333            .show(ui, |ui| {
334                let mut table = TableBuilder::new(ui)
335                    .column(Column::auto().resizable(true))
336                    .column(Column::initial(250.0).resizable(true))
337                    .column(Column::initial(300.0).resizable(true))
338                    .column(Column::initial(250.0).resizable(true))
339                    .column(Column::initial(250.0).resizable(true))
340                    .column(Column::initial(300.0).resizable(true))
341                    .column(Column::initial(250.0).resizable(true))
342                    .column(Column::remainder().at_least(250.0));
343                if new {
344                    table = table.scroll_to_row(constraints.constraints.len() - 1, None)
345                };
346                table
347                    .header(40.0, |mut header| {
348                        header.col(|ui| {
349                            ui.heading("ID");
350                        });
351                        header.col(|ui| {
352                            ui.heading("Description");
353                        });
354                        header.col(|ui| {
355                            ui.heading("Constraint Type");
356                        });
357                        header.col(|ui| {
358                            ui.heading("Constraints");
359                        });
360                        header.col(|ui| {
361                            ui.heading("Hazards");
362                        });
363                        header.col(|ui| {
364                            ui.heading("UCA");
365                        });
366                        header.col(|ui| {
367                            ui.heading("CS");
368                        });
369                        header.col(|ui| {
370                            ui.heading("TSF");
371                        });
372                    })
373                    .body(|mut body| {
374                        let mut remove_constraint = None;
375                        for (index, constraint) in
376                            &mut constraints.constraints.iter_mut().enumerate()
377                        {
378                            body.row(30.0, |mut row| {
379                                row.col(|ui| {
380                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
381                                        if remove_button(ui).clicked() {
382                                            remove_constraint = Some(index);
383                                        };
384                                        egui::TextEdit::singleline(&mut constraint.id)
385                                            .hint_text("Type something!")
386                                            .show(ui);
387                                    });
388                                });
389                                row.col(|ui| {
390                                    egui::TextEdit::singleline(&mut constraint.description)
391                                        .hint_text("Type something!")
392                                        .show(ui);
393                                });
394                                row.col(|ui| {
395                                    components::Dropdown {
396                                        value: &mut constraint.constraint_type,
397                                        id_salt: format!("constraint_type{:?}", constraint.id),
398                                    }
399                                    .show(ui);
400                                });
401                                row.col(|ui| {
402                                    components::LinkList {
403                                        links: &mut constraint.link_to_constraints,
404                                        mapping: &constraint_map,
405                                    }
406                                    .show(ui);
407                                });
408                                row.col(|ui| {
409                                    LinkList {
410                                        links: &mut constraint.link_to_hazards,
411                                        mapping: &hazard_map,
412                                    }
413                                    .show(ui);
414                                });
415                                row.col(|ui| {
416                                    components::LinkList {
417                                        links: &mut constraint.link_to_uca,
418                                        mapping: &uca_map,
419                                    }
420                                    .show(ui);
421                                });
422                                row.col(|ui| {
423                                    components::LinkList {
424                                        links: &mut constraint.link_to_cs,
425                                        mapping: &cs_map,
426                                    }
427                                    .show(ui);
428                                });
429                                row.col(|ui| {
430                                    ui.vertical(|ui| {
431                                        constraint.link_to_tsf.retain_mut(|link| {
432                                            let mut keep = true;
433                                            ui.horizontal(|ui| {
434                                                egui::TextEdit::singleline(link)
435                                                    .hint_text("Type something!")
436                                                    .show(ui);
437                                                ui.with_layout(
438                                                    Layout::right_to_left(Align::Center),
439                                                    |ui| {
440                                                        if ui.small_button("-").clicked() {
441                                                            keep = false;
442                                                        }
443                                                    },
444                                                );
445                                            });
446
447                                            keep
448                                        });
449                                    });
450                                    if ui.button("Add TSF link").clicked() {
451                                        constraint.link_to_tsf.push(String::new());
452                                    }
453                                });
454                            });
455                        }
456                        if let Some(index) = remove_constraint {
457                            constraints.constraints.remove(index);
458                        };
459                    });
460            });
461    });
462}
463
464pub fn update_interactions(
465    ui: &mut egui::Ui,
466    interactions: &mut Interactions,
467    elements: &Elements,
468) {
469    let element_map: IndexMap<ElementLink, String> = elements
470        .elements
471        .iter()
472        .map(|item| (ElementLink::from(item.id.as_ref()), item.name.clone()))
473        .collect();
474
475    let new = if ui.button("Add interactions").clicked() {
476        interactions.interactions.push(Interaction {
477            id: format!("{}", interactions.interactions.len() + 1),
478            category: InteractionCategory::Missing,
479            description: "".to_string(),
480            diagram_label: "".to_string(),
481            start: ElementLink::from(""),
482            end: ElementLink::from(""),
483            note: "".to_string(),
484            interaction_type: InteractionType::Missing,
485        });
486        true
487    } else {
488        false
489    };
490    ui.separator();
491    egui::ScrollArea::horizontal()
492        .auto_shrink(false)
493        .show(ui, |ui| {
494            let mut table = TableBuilder::new(ui)
495                .id_salt("interactions")
496                .column(Column::auto().resizable(true))
497                .column(Column::auto().resizable(true))
498                .column(Column::auto().resizable(true))
499                .column(Column::auto().resizable(true))
500                .column(Column::auto().resizable(true))
501                .column(Column::auto().resizable(true))
502                .column(Column::auto().resizable(true))
503                .column(Column::remainder().at_least(250.0));
504
505            if new {
506                table = table.scroll_to_row(interactions.interactions.len() - 1, None)
507            };
508            table
509                .header(20.0, |mut header| {
510                    header.col(|ui| {
511                        ui.heading("ID");
512                    });
513                    header.col(|ui| {
514                        ui.heading("Diagram ID");
515                    });
516                    header.col(|ui| {
517                        ui.heading("Description");
518                    });
519                    header.col(|ui| {
520                        ui.heading("Type");
521                    });
522                    header.col(|ui| {
523                        ui.heading("Provider");
524                    });
525                    header.col(|ui| {
526                        ui.heading("Receiver");
527                    });
528                    header.col(|ui| {
529                        ui.heading("Category");
530                    });
531                    header.col(|ui| {
532                        ui.heading("Notes");
533                    });
534                })
535                .body(|mut body| {
536                    let mut remove_interaction = None;
537                    for (index, interaction) in
538                        &mut interactions.interactions.iter_mut().enumerate()
539                    {
540                        body.row(30.0, |mut row| {
541                            row.col(|ui| {
542                                ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
543                                    if remove_button(ui).clicked() {
544                                        remove_interaction = Some(index);
545                                    };
546                                    egui::TextEdit::singleline(&mut interaction.id)
547                                        .hint_text("Type something!")
548                                        .show(ui);
549                                });
550                            });
551                            row.col(|ui| {
552                                egui::TextEdit::singleline(&mut interaction.diagram_label)
553                                    .hint_text("Type something!")
554                                    .show(ui);
555                            });
556                            row.col(|ui| {
557                                egui::TextEdit::singleline(&mut interaction.description)
558                                    .hint_text("Type something!")
559                                    .show(ui);
560                            });
561                            row.col(|ui| {
562                                components::Dropdown {
563                                    value: &mut interaction.interaction_type,
564                                    id_salt: format!("interaction_type{:?}", interaction.id),
565                                }
566                                .show(ui);
567                            });
568                            row.col(|ui| {
569                                components::LinkReference {
570                                    id_salt: format!(
571                                        "interaction_start_combobox_{}",
572                                        interaction.id
573                                    ),
574                                    link: &mut interaction.start,
575                                    mapping: &element_map,
576                                }
577                                .show(ui);
578                            });
579                            row.col(|ui| {
580                                components::LinkReference {
581                                    id_salt: format!("interaction_end_combobox_{}", interaction.id),
582                                    link: &mut interaction.end,
583                                    mapping: &element_map,
584                                }
585                                .show(ui)
586                            });
587                            row.col(|ui| {
588                                components::Dropdown {
589                                    value: &mut interaction.category,
590                                    id_salt: format!("interaction_category{:?}", interaction.id),
591                                }
592                                .show(ui);
593                            });
594                            row.col(|ui| {
595                                egui::TextEdit::multiline(&mut interaction.note)
596                                    .hint_text("Type something!")
597                                    .show(ui);
598                            });
599                        });
600                    }
601                    if let Some(index) = remove_interaction {
602                        interactions.interactions.remove(index);
603                    };
604                });
605        });
606}
607
608pub fn update_ca_analyses(
609    ui: &mut egui::Ui,
610    ca_analyses: &mut CaAnalyses,
611    uca_contexts: &UcaContexts,
612    hazards: &Hazards,
613    interactions: &Interactions,
614) {
615    let uca_context_map = construct_link_map!(
616        table = uca_contexts,
617        link_type = UcaContextLink,
618        linked_to = unsafe_context
619    );
620    let hazard_map = construct_link_map!(table = hazards, link_type = HazardLink);
621    let interaction_map = construct_link_map!(table = interactions, link_type = InteractionLink);
622
623    ui.with_layout(Layout::top_down(Align::Min), |ui| {
624        let new = if ui.button("Add control action analyses").clicked() {
625            ca_analyses.ca_analyses.push(CaAnalysis {
626                id: format!("{}", ca_analyses.ca_analyses.len() + 1),
627                analysis_result: CaResult::Missing,
628                ca_id: InteractionLink::from(""),
629                link_to_hazards: vec![],
630                uca_context: UcaContextLink::from(""),
631                uca_type: UcaType::Missing,
632                justification: "".to_string(),
633            });
634            true
635        } else {
636            false
637        };
638        ui.separator();
639        egui::ScrollArea::horizontal()
640            .auto_shrink(false)
641            .show(ui, |ui| {
642                let mut table = TableBuilder::new(ui)
643                    .id_salt("ca_analysis")
644                    .column(Column::auto().resizable(true))
645                    .column(Column::auto().resizable(true))
646                    .column(Column::auto().resizable(true))
647                    .column(Column::auto().resizable(true))
648                    .column(Column::auto().resizable(true))
649                    .column(Column::auto().resizable(true))
650                    .column(Column::remainder().at_least(250.0));
651
652                if new {
653                    table = table.scroll_to_row(ca_analyses.ca_analyses.len() - 1, None)
654                };
655                table
656                    .header(20.0, |mut header| {
657                        header.col(|ui| {
658                            ui.heading("ID");
659                        });
660                        header.col(|ui| {
661                            ui.heading("CA ID");
662                        });
663                        header.col(|ui| {
664                            ui.heading("UCA Type");
665                        });
666                        header.col(|ui| {
667                            ui.heading("UCA Context");
668                        });
669                        header.col(|ui| {
670                            ui.heading("Analyses Results");
671                        });
672                        header.col(|ui| {
673                            ui.heading("Hazards");
674                        });
675                        header.col(|ui| {
676                            ui.heading("justification");
677                        });
678                    })
679                    .body(|mut body| {
680                        let mut remove_ca_analysis = None;
681                        for (index, ca_analysis) in
682                            &mut ca_analyses.ca_analyses.iter_mut().enumerate()
683                        {
684                            body.row(30.0, |mut row| {
685                                row.col(|ui| {
686                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
687                                        if remove_button(ui).clicked() {
688                                            remove_ca_analysis = Some(index);
689                                        };
690                                        egui::TextEdit::singleline(&mut ca_analysis.id)
691                                            .hint_text("Type something!")
692                                            .show(ui);
693                                    });
694                                });
695                                row.col(|ui| {
696                                    LinkReference {
697                                        id_salt: format!(
698                                            "ca_analysis_ca_id_combobox_{}",
699                                            ca_analysis.id
700                                        ),
701                                        link: &mut ca_analysis.ca_id,
702                                        mapping: &interaction_map,
703                                    }
704                                    .show(ui);
705                                });
706                                row.col(|ui| {
707                                    components::Dropdown {
708                                        value: &mut ca_analysis.uca_type,
709                                        id_salt: format!("uca_type{:?}", ca_analysis.id),
710                                    }
711                                    .show(ui);
712                                });
713                                row.col(|ui| {
714                                    LinkReference {
715                                        id_salt: format!(
716                                            "ca_analysis_uca_context_combobox_{}",
717                                            ca_analysis.id
718                                        ),
719                                        link: &mut ca_analysis.uca_context,
720                                        mapping: &uca_context_map,
721                                    }
722                                    .show(ui);
723                                });
724                                row.col(|ui| {
725                                    components::Dropdown {
726                                        value: &mut ca_analysis.analysis_result,
727                                        id_salt: format!("ca_analysis_result{:?}", ca_analysis.id),
728                                    }
729                                    .show(ui);
730                                });
731                                row.col(|ui| {
732                                    LinkList {
733                                        links: &mut ca_analysis.link_to_hazards,
734                                        mapping: &hazard_map,
735                                    }
736                                    .show(ui);
737                                });
738                                row.col(|ui| {
739                                    egui::TextEdit::multiline(&mut ca_analysis.justification)
740                                        .hint_text("Type something!")
741                                        .show(ui);
742                                });
743                            });
744                        }
745                        if let Some(index) = remove_ca_analysis {
746                            ca_analyses.ca_analyses.remove(index);
747                        };
748                    });
749            });
750    });
751}
752
753pub fn update_uca_contexts(ui: &mut egui::Ui, uca_contexts: &mut UcaContexts) {
754    ui.with_layout(Layout::top_down(Align::Min), |ui| {
755        let new = if ui.button("Add unsafe control action context").clicked() {
756            uca_contexts.uca_contexts.push(UcaContext {
757                id: format!("CX{}", uca_contexts.uca_contexts.len() + 1),
758                unsafe_context: "".to_string(),
759                notes: "".to_string(),
760            });
761            true
762        } else {
763            false
764        };
765        ui.separator();
766        egui::ScrollArea::horizontal()
767            .auto_shrink(false)
768            .show(ui, |ui| {
769                let mut table = TableBuilder::new(ui)
770                    .id_salt("uca_contexts")
771                    .column(Column::auto().resizable(true))
772                    .column(Column::auto().resizable(true))
773                    .column(Column::remainder().at_least(250.0));
774
775                if new {
776                    table = table.scroll_to_row(uca_contexts.uca_contexts.len() - 1, None)
777                };
778                table
779                    .header(20.0, |mut header| {
780                        header.col(|ui| {
781                            ui.heading("ID");
782                        });
783                        header.col(|ui| {
784                            ui.heading("Unsafe Context");
785                        });
786                        header.col(|ui| {
787                            ui.heading("Notes");
788                        });
789                    })
790                    .body(|mut body| {
791                        let mut remove_uca_context = None;
792                        for (index, uca_context) in
793                            &mut uca_contexts.uca_contexts.iter_mut().enumerate()
794                        {
795                            body.row(30.0, |mut row| {
796                                row.col(|ui| {
797                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
798                                        if remove_button(ui).clicked() {
799                                            remove_uca_context = Some(index);
800                                        };
801                                        egui::TextEdit::singleline(&mut uca_context.id)
802                                            .hint_text("Type something!")
803                                            .show(ui);
804                                    });
805                                });
806                                row.col(|ui| {
807                                    egui::TextEdit::multiline(&mut uca_context.unsafe_context)
808                                        .hint_text("Type something!")
809                                        .show(ui);
810                                });
811                                row.col(|ui| {
812                                    egui::TextEdit::multiline(&mut uca_context.notes)
813                                        .hint_text("Type something!")
814                                        .show(ui);
815                                });
816                            });
817                        }
818                        if let Some(index) = remove_uca_context {
819                            uca_contexts.uca_contexts.remove(index);
820                        };
821                    });
822            });
823    });
824}
825
826pub fn update_ucas(
827    ui: &mut egui::Ui,
828    ucas: &mut Ucas,
829    uca_contexts: &UcaContexts,
830    constraints: &Constraints,
831    interactions: &Interactions,
832) {
833    let uca_context_map = construct_link_map!(
834        table = uca_contexts,
835        link_type = UcaContextLink,
836        linked_to = unsafe_context
837    );
838    let constraint_map = construct_link_map!(table = constraints, link_type = ConstraintLink);
839    let interaction_map = construct_link_map!(table = interactions, link_type = InteractionLink);
840
841    ui.with_layout(Layout::top_down(Align::Min), |ui| {
842        let new = if ui.button("Add unsafe control action").clicked() {
843            ucas.ucas.push(Uca {
844                id: format!("{}", ucas.ucas.len() + 1),
845                constraint_id: vec![],
846                ca: InteractionLink::from(""),
847                uca_context: UcaContextLink::from(""),
848                uca_definition: "".to_string(),
849                uca_type: UcaType::Missing,
850            });
851            true
852        } else {
853            false
854        };
855        ui.separator();
856        egui::ScrollArea::horizontal()
857            .auto_shrink(false)
858            .show(ui, |ui| {
859                let mut table = TableBuilder::new(ui)
860                    .id_salt("ucas")
861                    .column(Column::auto().resizable(true))
862                    .column(Column::auto().resizable(true))
863                    .column(Column::auto().resizable(true))
864                    .column(Column::auto().resizable(true))
865                    .column(Column::auto().resizable(true))
866                    .column(Column::auto().resizable(true))
867                    .column(Column::remainder().at_least(250.0));
868                if new {
869                    table = table.scroll_to_row(ucas.ucas.len() - 1, None)
870                };
871                table
872                    .header(20.0, |mut header| {
873                        header.col(|ui| {
874                            ui.heading("ID");
875                        });
876                        header.col(|ui| {
877                            ui.heading("Uca Definition");
878                        });
879                        header.col(|ui| {
880                            ui.heading("Ca");
881                        });
882                        header.col(|ui| {
883                            ui.heading("UCA Type");
884                        });
885                        header.col(|ui| {
886                            ui.heading("UCA Context");
887                        });
888                        header.col(|ui| {
889                            ui.heading("Constraint(s)");
890                        });
891                    })
892                    .body(|mut body| {
893                        let mut remove_uca = None;
894                        for (index, uca) in &mut ucas.ucas.iter_mut().enumerate() {
895                            body.row(30.0, |mut row| {
896                                row.col(|ui| {
897                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
898                                        if remove_button(ui).clicked() {
899                                            remove_uca = Some(index);
900                                        };
901                                        egui::TextEdit::singleline(&mut uca.id)
902                                            .hint_text("Type something!")
903                                            .show(ui);
904                                    });
905                                });
906                                row.col(|ui| {
907                                    egui::TextEdit::multiline(&mut uca.uca_definition)
908                                        .hint_text("Type something!")
909                                        .show(ui);
910                                });
911                                row.col(|ui| {
912                                    LinkReference {
913                                        id_salt: format!("uca_ca_id_combobox_{}", uca.id),
914                                        link: &mut uca.ca,
915                                        mapping: &interaction_map,
916                                    }
917                                    .show(ui);
918                                });
919                                row.col(|ui| {
920                                    egui::ComboBox::from_id_salt(format!(
921                                        "uca_uca_type{:?}",
922                                        uca.id
923                                    ))
924                                    .selected_text(format!("{:?}", uca.uca_type))
925                                    .show_ui(ui, |ui| {
926                                        for uca_type in UcaType::iter() {
927                                            ui.selectable_value(
928                                                &mut uca.uca_type,
929                                                uca_type,
930                                                format!("{uca_type:?}"),
931                                            );
932                                        }
933                                    });
934                                });
935                                row.col(|ui| {
936                                    LinkReference {
937                                        id_salt: format!("uca_uca_context_combobox_{}", uca.id),
938                                        link: &mut uca.uca_context,
939                                        mapping: &uca_context_map,
940                                    }
941                                    .show(ui);
942                                });
943
944                                row.col(|ui| {
945                                    components::LinkList {
946                                        links: &mut uca.constraint_id,
947                                        mapping: &constraint_map,
948                                    }
949                                    .show(ui);
950                                });
951                            });
952                        }
953                        if let Some(index) = remove_uca {
954                            ucas.ucas.remove(index);
955                        };
956                    });
957            });
958    });
959}
960
961pub fn update_control_loops(
962    ui: &mut egui::Ui,
963    control_loops: &mut ControlLoops,
964    elements: &Elements,
965    constraints: &Constraints,
966) {
967    let element_map =
968        construct_link_map!(table = elements, link_type = ElementLink, linked_to = name);
969    let constraint_map = construct_link_map!(table = constraints, link_type = ConstraintLink);
970
971    ui.with_layout(Layout::top_down(Align::Min), |ui| {
972        let new = if ui.button("Add control loop").clicked() {
973            control_loops.control_loops.push(ControlLoop {
974                id: format!("CL{}", control_loops.control_loops.len() + 1),
975                controlled_process: ElementLink::from(""),
976                description: "".to_string(),
977                linked_slc: vec![],
978            });
979            true
980        } else {
981            false
982        };
983        ui.separator();
984        egui::ScrollArea::horizontal()
985            .auto_shrink(false)
986            .show(ui, |ui| {
987                let mut table = TableBuilder::new(ui)
988                    .id_salt("control_loops")
989                    .column(Column::auto().resizable(true))
990                    .column(Column::auto().resizable(true))
991                    .column(Column::auto().resizable(true))
992                    .column(Column::remainder().at_least(250.0));
993                if new {
994                    table = table.scroll_to_row(control_loops.control_loops.len() - 1, None)
995                };
996                table
997                    .header(20.0, |mut header| {
998                        header.col(|ui| {
999                            ui.heading("ID");
1000                        });
1001                        header.col(|ui| {
1002                            ui.heading("Description");
1003                        });
1004                        header.col(|ui| {
1005                            ui.heading("Controlled Process");
1006                        });
1007                        header.col(|ui| {
1008                            ui.heading("Linked SLC(s)");
1009                        });
1010                    })
1011                    .body(|mut body| {
1012                        let mut remove_control_loop = None;
1013                        for (index, control_loop) in
1014                            &mut control_loops.control_loops.iter_mut().enumerate()
1015                        {
1016                            body.row(30.0, |mut row| {
1017                                row.col(|ui| {
1018                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1019                                        if remove_button(ui).clicked() {
1020                                            remove_control_loop = Some(index);
1021                                        };
1022
1023                                        egui::TextEdit::singleline(&mut control_loop.id)
1024                                            .hint_text("Type something!")
1025                                            .show(ui);
1026                                    });
1027                                });
1028                                row.col(|ui| {
1029                                    egui::TextEdit::multiline(&mut control_loop.description)
1030                                        .hint_text("Type something!")
1031                                        .show(ui);
1032                                });
1033                                row.col(|ui| {
1034                                    LinkReference {
1035                                        id_salt: format!(
1036                                            "control_loop_controlled_process_combobox_{}",
1037                                            control_loop.id
1038                                        ),
1039                                        link: &mut control_loop.controlled_process,
1040                                        mapping: &element_map,
1041                                    }
1042                                    .show(ui);
1043                                });
1044                                row.col(|ui| {
1045                                    components::LinkList {
1046                                        links: &mut control_loop.linked_slc,
1047                                        mapping: &constraint_map,
1048                                    }
1049                                    .show(ui);
1050                                });
1051                            });
1052                        }
1053                        if let Some(index) = remove_control_loop {
1054                            control_loops.control_loops.remove(index);
1055                        };
1056                    });
1057            });
1058    });
1059}
1060
1061pub fn update_control_loop_sequences(
1062    ui: &mut egui::Ui,
1063    control_loop_sequences: &mut ControlLoopSequences,
1064    control_loops: &ControlLoops,
1065    interactions: &Interactions,
1066) {
1067    let interaction_map = construct_link_map!(table = interactions, link_type = InteractionLink);
1068    let control_loop_map = construct_link_map!(table = control_loops, link_type = ControlLoopLink);
1069    ui.with_layout(Layout::top_down(Align::Min), |ui| {
1070        let new = if ui.button("Add control loop sequences").clicked() {
1071            control_loop_sequences
1072                .control_loop_sequences
1073                .push(ControlLoopSequence {
1074                    id: format!(
1075                        "{}",
1076                        control_loop_sequences.control_loop_sequences.len() + 1
1077                    ),
1078                    control_loop: ControlLoopLink::from(""),
1079                    step: "".to_string(),
1080                    interaction: InteractionLink::from(""),
1081                    provider_process_model: "".to_string(),
1082                    provider_logic: "".to_string(),
1083                    expected_target_behaviour: "".to_string(),
1084                });
1085            true
1086        } else {
1087            false
1088        };
1089        ui.separator();
1090        egui::ScrollArea::horizontal()
1091            .auto_shrink(false)
1092            .show(ui, |ui| {
1093                let mut table = TableBuilder::new(ui)
1094                    .id_salt("control_loop_sequences")
1095                    .column(Column::auto().resizable(true))
1096                    .column(Column::auto().resizable(true))
1097                    .column(Column::auto().resizable(true))
1098                    .column(Column::auto().resizable(true))
1099                    .column(Column::auto().resizable(true))
1100                    .column(Column::auto().resizable(true))
1101                    .column(Column::remainder().at_least(250.0));
1102                if new {
1103                    table = table.scroll_to_row(
1104                        control_loop_sequences.control_loop_sequences.len() - 1,
1105                        None,
1106                    )
1107                };
1108                table
1109                    .header(20.0, |mut header| {
1110                        header.col(|ui| {
1111                            ui.heading("ID");
1112                        });
1113                        header.col(|ui| {
1114                            ui.heading("Loop");
1115                        });
1116                        header.col(|ui| {
1117                            ui.heading("Step");
1118                        });
1119                        header.col(|ui| {
1120                            ui.heading("Interaction");
1121                        });
1122                        header.col(|ui| {
1123                            ui.heading("Provider process model or state");
1124                        });
1125                        header.col(|ui| {
1126                            ui.heading("Provider logic");
1127                        });
1128                        header.col(|ui| {
1129                            ui.heading("Expected target behaviour");
1130                        });
1131                    })
1132                    .body(|mut body| {
1133                        let mut remove_control_loop_sequence = None;
1134                        for (index, control_loop_sequence) in &mut control_loop_sequences
1135                            .control_loop_sequences
1136                            .iter_mut()
1137                            .enumerate()
1138                        {
1139                            body.row(30.0, |mut row| {
1140                                row.col(|ui| {
1141                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1142                                        if remove_button(ui).clicked() {
1143                                            remove_control_loop_sequence = Some(index);
1144                                        };
1145
1146                                        egui::TextEdit::singleline(&mut control_loop_sequence.id)
1147                                            .hint_text("Type something!")
1148                                            .show(ui);
1149                                    });
1150                                });
1151                                row.col(|ui| {
1152                                    LinkReference {
1153                                        id_salt: format!(
1154                                            "control_loop_sequence_control_loop_combobox_{}",
1155                                            control_loop_sequence.id
1156                                        ),
1157                                        link: &mut control_loop_sequence.control_loop,
1158                                        mapping: &control_loop_map,
1159                                    }
1160                                    .show(ui);
1161                                });
1162                                row.col(|ui| {
1163                                    egui::TextEdit::singleline(&mut control_loop_sequence.step)
1164                                        .hint_text("Type something!")
1165                                        .show(ui);
1166                                });
1167                                row.col(|ui| {
1168                                    LinkReference {
1169                                        id_salt: format!(
1170                                            "control_loop_sequence_interaction_combobox_{}",
1171                                            control_loop_sequence.id
1172                                        ),
1173                                        link: &mut control_loop_sequence.interaction,
1174                                        mapping: &interaction_map,
1175                                    }
1176                                    .show(ui);
1177                                });
1178                                row.col(|ui| {
1179                                    egui::TextEdit::multiline(
1180                                        &mut control_loop_sequence.provider_process_model,
1181                                    )
1182                                    .hint_text("Type something!")
1183                                    .show(ui);
1184                                });
1185                                row.col(|ui| {
1186                                    egui::TextEdit::multiline(
1187                                        &mut control_loop_sequence.provider_logic,
1188                                    )
1189                                    .hint_text("Type something!")
1190                                    .show(ui);
1191                                });
1192                                row.col(|ui| {
1193                                    egui::TextEdit::multiline(
1194                                        &mut control_loop_sequence.expected_target_behaviour,
1195                                    )
1196                                    .hint_text("Type something!")
1197                                    .show(ui);
1198                                });
1199                            });
1200                        }
1201                        if let Some(index) = remove_control_loop_sequence {
1202                            control_loop_sequences.control_loop_sequences.remove(index);
1203                        };
1204                    });
1205            });
1206    });
1207}
1208
1209pub fn update_scenarios(
1210    ui: &mut egui::Ui,
1211    scenarios: &mut Scenarios,
1212    control_loop_sequences: &ControlLoopSequences,
1213    ucas: &Ucas,
1214    hazards: &Hazards,
1215    constraints: &Constraints,
1216) {
1217    let sequence_ids: IndexSet<ControlLoopSequenceLink> = control_loop_sequences
1218        .control_loop_sequences
1219        .iter()
1220        .map(|seq| ControlLoopSequenceLink::from(seq.id.as_str()))
1221        .collect();
1222    let uca_map = construct_link_map!(
1223        table = ucas,
1224        link_type = UcaLink,
1225        linked_to = uca_definition
1226    );
1227    let hazard_map = construct_link_map!(table = hazards, link_type = HazardLink);
1228    let constraint_map = construct_link_map!(table = constraints, link_type = ConstraintLink);
1229
1230    ui.with_layout(Layout::top_down(Align::Min), |ui| {
1231        let new = if ui.button("Add scenario").clicked() {
1232            scenarios.scenarios.push(Scenario {
1233                id: format!("{}", scenarios.scenarios.len() + 1),
1234                sequence: ControlLoopSequenceLink::from(""),
1235                causal_scenario: CausalScenario::Missing,
1236                causal_scenario_prompt: "".to_string(),
1237                analysis_result: CausalScenarioResult::Missing,
1238                causal_scenario_definition: "".to_string(),
1239                ucas: vec![],
1240                hazards: vec![],
1241                constraint: vec![],
1242                notes: "".to_string(),
1243            });
1244            true
1245        } else {
1246            false
1247        };
1248        ui.separator();
1249        egui::ScrollArea::horizontal()
1250            .auto_shrink(false)
1251            .show(ui, |ui| {
1252                let mut table = TableBuilder::new(ui)
1253                    .id_salt("uca_contexts")
1254                    .column(Column::auto().resizable(true))
1255                    .column(Column::auto().resizable(true))
1256                    .column(Column::auto().resizable(true))
1257                    .column(Column::auto().resizable(true))
1258                    .column(Column::auto().resizable(true))
1259                    .column(Column::auto().resizable(true))
1260                    .column(Column::auto().resizable(true))
1261                    .column(Column::auto().resizable(true))
1262                    .column(Column::remainder().at_least(250.0));
1263                if new {
1264                    table = table.scroll_to_row(scenarios.scenarios.len() - 1, None)
1265                };
1266                table
1267                    .header(20.0, |mut header| {
1268                        header.col(|ui| {
1269                            ui.heading("ID");
1270                        });
1271                        header.col(|ui| {
1272                            ui.heading("Control Loop Sequence");
1273                        });
1274                        header.col(|ui| {
1275                            ui.heading("Causal Scenario Prompt");
1276                        });
1277                        header.col(|ui| {
1278                            ui.heading("Control Scenario Type");
1279                        });
1280                        header.col(|ui| {
1281                            ui.heading("Causal Scenario Definition");
1282                        });
1283                        header.col(|ui| {
1284                            ui.heading("UCAs");
1285                        });
1286                        header.col(|ui| {
1287                            ui.heading("Hazards");
1288                        });
1289                        header.col(|ui| {
1290                            ui.heading("Constraint(s)");
1291                        });
1292                        header.col(|ui| {
1293                            ui.heading("Notes");
1294                        });
1295                    })
1296                    .body(|mut body| {
1297                        let mut remove_senario = None;
1298                        for (index, scenario) in &mut scenarios.scenarios.iter_mut().enumerate() {
1299                            body.row(30.0, |mut row| {
1300                                row.col(|ui| {
1301                                    ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1302                                        if remove_button(ui).clicked() {
1303                                            remove_senario = Some(index);
1304                                        };
1305
1306                                        egui::TextEdit::singleline(&mut scenario.id)
1307                                            .hint_text("Type something!")
1308                                            .show(ui);
1309                                    });
1310                                });
1311                                row.col(|ui| {
1312                                    IdOnlyLinkReference {
1313                                        id_salt: format!(
1314                                            "scenario_control_loop_sequence_combobox_{}",
1315                                            scenario.id
1316                                        ),
1317                                        link: &mut scenario.sequence,
1318                                        valid_links: &sequence_ids,
1319                                    }
1320                                    .show(ui);
1321                                });
1322                                row.col(|ui| {
1323                                    ui.add_enabled(
1324                                        false,
1325                                        egui::TextEdit::multiline(
1326                                            &mut scenario.causal_scenario_prompt,
1327                                        )
1328                                        .hint_text("Type something!"),
1329                                    )
1330                                    .on_disabled_hover_text("Not yet supported");
1331                                });
1332
1333                                row.col(|ui| {
1334                                    components::Dropdown {
1335                                        value: &mut scenario.causal_scenario,
1336                                        id_salt: format!("causal_scenario{:?}", scenario.id),
1337                                    }
1338                                    .show(ui);
1339                                });
1340                                row.col(|ui| {
1341                                    egui::TextEdit::multiline(
1342                                        &mut scenario.causal_scenario_definition,
1343                                    )
1344                                    .hint_text("Type something!")
1345                                    .show(ui);
1346                                });
1347                                row.col(|ui| {
1348                                    components::LinkList {
1349                                        links: &mut scenario.ucas,
1350                                        mapping: &uca_map,
1351                                    }
1352                                    .show(ui);
1353                                });
1354                                row.col(|ui| {
1355                                    components::LinkList {
1356                                        links: &mut scenario.hazards,
1357                                        mapping: &hazard_map,
1358                                    }
1359                                    .show(ui);
1360                                });
1361                                row.col(|ui| {
1362                                    components::LinkList {
1363                                        links: &mut scenario.constraint,
1364                                        mapping: &constraint_map,
1365                                    }
1366                                    .show(ui);
1367                                });
1368                                row.col(|ui| {
1369                                    egui::TextEdit::multiline(&mut scenario.notes)
1370                                        .hint_text("Type something!")
1371                                        .show(ui);
1372                                });
1373                            });
1374                        }
1375                        if let Some(index) = remove_senario {
1376                            scenarios.scenarios.remove(index);
1377                        };
1378                    });
1379            });
1380    });
1381}
1382
1383pub fn io_options(ui: &mut egui::Ui, stpa_data: &StpaData, project_settings: Option<&StpaProject>) {
1384    if ui.small_button("Save everything as ymal").clicked() {
1385        stpa_data.save_project(&PathBuf::from("."))
1386    }
1387    if let Some(project_settings) = project_settings {
1388        if ui
1389            .small_button("Save everything back to originals")
1390            .clicked()
1391        {
1392            if let Some(losses_file) = &project_settings.losses {
1393                stpa_data.losses.to_file(losses_file).unwrap();
1394            }
1395            if let Some(interactions_file) = &project_settings.interactions {
1396                stpa_data.interactions.to_file(interactions_file).unwrap();
1397            }
1398        }
1399    }
1400}