rafia_stpa_gui/
lib.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// *****************************************************************************
16use std::path::PathBuf;
17
18use clap::Parser;
19use eframe::egui;
20use panels::tabs::{create_tree, TreeBehavior};
21use strum::EnumIter;
22
23use rafia_stpa::stpa::{
24    ca_analysis::CaAnalyses, constraints::Constraints,
25    control_loop_sequences::ControlLoopSequences, control_loops::ControlLoops, elements::Elements,
26    hazards::Hazards, interactions::Interactions, losses::Losses, scenarios::Scenarios,
27    uca_contexts::UcaContexts, ucas::Ucas, StpaData, StpaProject,
28};
29
30pub mod panels;
31
32#[derive(EnumIter, Debug, PartialEq, Eq)]
33pub enum StpaView {
34    IoOptions,
35    Diagram,
36    Losses,
37    Hazards,
38    Elements,
39    Interactions,
40    UcaContexts,
41    CaAnalyses,
42    Constraints,
43    Ucas,
44    ControlLoops,
45    ControlLoopSequences,
46    Scenarios,
47}
48impl StpaView {
49    fn tab_name(&self) -> &'static str {
50        match self {
51            StpaView::IoOptions => "Import / Export",
52            StpaView::Diagram => "Scope / Diagram",
53            StpaView::Losses => "Losses",
54            StpaView::Hazards => "Hazards",
55            StpaView::Elements => "Elements",
56            StpaView::Interactions => "Interactions",
57            StpaView::UcaContexts => "Contexts",
58            StpaView::CaAnalyses => "Control Action Analyses",
59            StpaView::Constraints => "Constraints",
60            StpaView::Ucas => "UCAs",
61            StpaView::ControlLoops => "Control Loops",
62            StpaView::ControlLoopSequences => "Control Loop Sequences",
63            StpaView::Scenarios => "Scenarios",
64        }
65    }
66}
67
68#[derive(Parser)]
69#[command(version, about, long_about = None)]
70pub struct Cli {
71    #[arg(long)]
72    losses: Option<PathBuf>,
73    #[arg(long)]
74    hazards: Option<PathBuf>,
75    #[arg(long)]
76    elements: Option<PathBuf>,
77    #[arg(long)]
78    interactions: Option<PathBuf>,
79    #[arg(long)]
80    constraints: Option<PathBuf>,
81    #[arg(long)]
82    ca_analysis: Option<PathBuf>,
83    #[arg(long)]
84    uca_contexts: Option<PathBuf>,
85    #[arg(long)]
86    uca: Option<PathBuf>,
87    #[arg(long)]
88    control_loops: Option<PathBuf>,
89    #[arg(long)]
90    cl_sequences: Option<PathBuf>,
91    #[arg(long)]
92    scenarios: Option<PathBuf>,
93    #[arg(long)]
94    project: Option<PathBuf>,
95    #[arg(long)]
96    diagram: Option<PathBuf>,
97}
98
99pub struct StpaApp {
100    tree: TreeBehavior,
101    panels: egui_tiles::Tree<StpaView>,
102}
103
104impl StpaApp {
105    pub fn new(cli: Cli, _cc: &eframe::CreationContext<'_>) -> Self {
106        let (mut stpa_data, project) = if let Some(project_file) = cli.project {
107            let project = StpaProject::from_file(&project_file).unwrap();
108            (project.clone().try_into().unwrap(), Some(project))
109        } else {
110            (StpaData::default(), None)
111        };
112
113        if let Some(losses) = cli.losses {
114            stpa_data.losses = Losses::from_file(&losses).unwrap();
115        }
116        if let Some(elements) = cli.elements {
117            stpa_data.elements = Elements::from_file(&elements).unwrap();
118        };
119        if let Some(interactions) = cli.interactions {
120            stpa_data.interactions = Interactions::from_file(&interactions).unwrap()
121        };
122        if let Some(hazards) = cli.hazards {
123            stpa_data.hazards = Hazards::from_file(&hazards).unwrap();
124        };
125        if let Some(constraints) = cli.constraints {
126            stpa_data.constraints = Constraints::from_file(&constraints).unwrap();
127        };
128        if let Some(ca_analyses) = cli.ca_analysis {
129            stpa_data.ca_analyses = CaAnalyses::from_file(&ca_analyses).unwrap();
130        };
131        if let Some(uca_contexts) = cli.uca_contexts {
132            stpa_data.uca_contexts = UcaContexts::from_file(&uca_contexts).unwrap();
133        };
134        if let Some(ucas) = cli.uca {
135            stpa_data.ucas = Ucas::from_file(&ucas).unwrap();
136        };
137        if let Some(control_loops) = cli.control_loops {
138            stpa_data.control_loops = ControlLoops::from_file(&control_loops).unwrap();
139        };
140        if let Some(control_loop_sequences) = cli.cl_sequences {
141            stpa_data.control_loop_sequences =
142                ControlLoopSequences::from_file(&control_loop_sequences).unwrap();
143        };
144        if let Some(scenarios) = cli.scenarios {
145            stpa_data.scenarios = Scenarios::from_file(&scenarios).unwrap();
146        };
147        StpaApp {
148            tree: TreeBehavior {
149                redo: vec![],
150                stpa_data: stpa_data.clone(),
151                project,
152                undo: vec![stpa_data],
153            },
154            panels: create_tree(),
155        }
156    }
157}
158
159impl eframe::App for StpaApp {
160    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
161        egui::TopBottomPanel::top("title_bar").show(ctx, |ui| {
162            ui.horizontal(|ui| {
163                ui.label("Rafia Stpa");
164                ui.separator();
165
166                egui::global_theme_preference_switch(ui);
167                ui.separator();
168
169                let undo_button = egui::Button::new("Undo");
170                if ui
171                    .add_enabled(self.tree.undo.len() > 1, undo_button)
172                    .clicked()
173                {
174                    let redo = self.tree.undo.pop().unwrap();
175                    self.tree.stpa_data = self.tree.undo.last().unwrap().clone();
176                    self.tree.redo.push(redo);
177                }
178                let redo_button = egui::Button::new("Redo");
179                if ui
180                    .add_enabled(!self.tree.redo.is_empty(), redo_button)
181                    .clicked()
182                {
183                    let redo = self.tree.redo.pop().unwrap();
184                    self.tree.undo.push(redo.clone());
185                    self.tree.stpa_data = redo;
186                }
187            });
188        });
189
190        egui::CentralPanel::default().show(ctx, |ui| {
191            self.panels.ui(&mut self.tree, ui);
192        });
193
194        if &self.tree.stpa_data != self.tree.undo.last().unwrap() {
195            self.tree.undo.push(self.tree.stpa_data.clone());
196            self.tree.redo.clear();
197        }
198    }
199}