1
// This is a GUI application
2
#![windows_subsystem = "windows"]
3

            
4
slint::include_modules!();
5

            
6
use std::error::Error;
7
use std::fs::File;
8
use std::ops::Deref;
9
use std::path::Path;
10
use std::process::ExitCode;
11
use std::sync::Arc;
12
use std::sync::Mutex;
13
use std::sync::RwLock;
14
use std::thread;
15
use std::time::Duration;
16
use std::time::Instant;
17

            
18
use clap::Parser;
19

            
20
use gui::console;
21
use log::debug;
22
use log::info;
23
use slint::invoke_from_event_loop;
24
use slint::Image;
25
use slint::Rgba8Pixel;
26
use slint::SharedPixelBuffer;
27

            
28
use io::io_aut::read_aut;
29
use ltsgraph_lib::GraphLayout;
30
use ltsgraph_lib::Viewer;
31
use pauseable_thread::PauseableThread;
32

            
33
mod error_dialog;
34
mod pauseable_thread;
35

            
36
#[derive(Parser, Debug)]
37
#[command(name = "Maurice Laveaux", about = "A lts viewing tool")]
38
pub struct Cli {
39
    #[arg(value_name = "FILE")]
40
    labelled_transition_system: Option<String>,
41
}
42

            
43
/// Contains all the GUI related state information.
44
struct GuiState {
45
    graph_layout: Mutex<GraphLayout>,
46
    viewer: Mutex<(Viewer, SharedPixelBuffer<Rgba8Pixel>)>,
47
}
48

            
49
#[derive(Clone, Default)]
50
pub struct GuiSettings {
51
    // Layout related settings
52
    pub handle_length: f32,
53
    pub repulsion_strength: f32,
54
    pub delta: f32,
55

            
56
    // View related settings
57
    pub width: u32,
58
    pub height: u32,
59
    pub state_radius: f32,
60
    pub label_text_size: f32,
61
    pub draw_action_labels: bool,
62

            
63
    pub zoom_level: f32,
64
    pub view_x: f32,
65
    pub view_y: f32,
66
}
67

            
68
impl GuiSettings {
69
    pub fn new() -> GuiSettings {
70
        GuiSettings {
71
            width: 1,
72
            height: 1,
73
            zoom_level: 1.0,
74
            view_x: 500.0,
75
            view_y: 500.0,
76
            ..Default::default()
77
        }
78
    }
79
}
80

            
81
// Initialize a tokio runtime for async calls
82
#[tokio::main(flavor = "current_thread")]
83
async fn main() -> Result<ExitCode, Box<dyn Error>> {
84
    // Attach the standard output to the command line.
85
    let _console = console::init()?;
86

            
87
    // Parse the command line arguments and enable the logger.
88
    env_logger::init();
89

            
90
    let cli = Cli::parse();
91

            
92
    // Stores the shared state of the GUI components.
93
    let state = Arc::new(RwLock::new(None::<GuiState>));
94
    let settings = Arc::new(Mutex::new(GuiSettings::new()));
95
    let canvas = Arc::new(Mutex::new(SharedPixelBuffer::new(1, 1)));
96

            
97
    // Initialize the GUI, but show it later.
98
    let app = Application::new()?;
99

            
100
    {
101
        let app_weak = app.as_weak();
102
        let settings = settings.clone();
103

            
104
        app.on_settings_changed(move || {
105
            // Request the settings for the next simulation tick.
106
            if let Some(app) = app_weak.upgrade() {
107
                let mut settings = settings.lock().unwrap();
108
                settings.handle_length = app.global::<Settings>().get_handle_length();
109
                settings.repulsion_strength = app.global::<Settings>().get_repulsion_strength();
110
                settings.delta = app.global::<Settings>().get_timestep();
111
                settings.state_radius = app.global::<Settings>().get_state_radius();
112

            
113
                settings.draw_action_labels = app.global::<Settings>().get_draw_action_labels();
114
                settings.zoom_level = app.global::<Settings>().get_zoom_level();
115
                settings.view_x = app.global::<Settings>().get_view_x();
116
                settings.view_y = app.global::<Settings>().get_view_y();
117
                settings.label_text_size = app.global::<Settings>().get_label_text_height();
118
            }
119
        });
120
    };
121

            
122
    // Trigger it once to set the default values.
123
    app.invoke_settings_changed();
124

            
125
    // Render the view continuously, but only update the canvas when necessary
126
    let render_handle = {
127
        let state = state.clone();
128
        let app_weak: slint::Weak<Application> = app.as_weak();
129
        let settings = settings.clone();
130
        let canvas = canvas.clone();
131

            
132
        Arc::new(PauseableThread::new("ltsgraph canvas worker", move || {
133
            if let Some(state) = state.read().unwrap().deref() {
134
                // Render a new frame...
135
                let start = Instant::now();
136
                let (ref mut viewer, ref mut pixel_buffer) = *state.viewer.lock().unwrap();
137

            
138
                // Resize the canvas when necessary
139
                let settings_clone = settings.lock().unwrap().clone();
140
                if pixel_buffer.width() != settings_clone.width || pixel_buffer.height() != settings_clone.height {
141
                    *pixel_buffer = SharedPixelBuffer::<Rgba8Pixel>::new(settings_clone.width, settings_clone.height);
142
                }
143

            
144
                viewer.render(
145
                    &mut tiny_skia::PixmapMut::from_bytes(
146
                        pixel_buffer.make_mut_bytes(),
147
                        settings_clone.width,
148
                        settings_clone.height,
149
                    )
150
                    .unwrap(),
151
                    settings_clone.draw_action_labels,
152
                    settings_clone.state_radius,
153
                    settings_clone.view_x,
154
                    settings_clone.view_y,
155
                    settings_clone.width,
156
                    settings_clone.height,
157
                    settings_clone.zoom_level,
158
                    settings_clone.label_text_size,
159
                );
160

            
161
                debug!(
162
                    "Rendering step ({} by {}) took {} ms",
163
                    settings_clone.width,
164
                    settings_clone.height,
165
                    (Instant::now() - start).as_millis()
166
                );
167
                *canvas.lock().unwrap() = pixel_buffer.clone();
168
            }
169

            
170
            // Request the to be updated.
171
            let app_weak = app_weak.clone();
172
            invoke_from_event_loop(move || {
173
                if let Some(app) = app_weak.upgrade() {
174
                    // Update the canvas
175
                    app.global::<Settings>()
176
                        .set_refresh(!app.global::<Settings>().get_refresh());
177
                };
178
            })
179
            .unwrap();
180

            
181
            false
182
        })?)
183
    };
184

            
185
    // Run the graph layout algorithm in a separate thread to avoid blocking the UI.
186
    let layout_handle = {
187
        let state = state.clone();
188
        let settings = settings.clone();
189
        let render_handle = render_handle.clone();
190

            
191
        Arc::new(PauseableThread::new("ltsgraph layout worker", move || {
192
            let start = Instant::now();
193
            let mut is_stable = true;
194

            
195
            if let Some(state) = state.read().unwrap().deref() {
196
                // Read the settings and free the lock since otherwise the callback above blocks.
197
                let settings = settings.lock().unwrap().clone();
198
                let mut layout = state.graph_layout.lock().unwrap();
199

            
200
                is_stable = layout.update(settings.handle_length, settings.repulsion_strength, settings.delta);
201
                if is_stable {
202
                    info!("Layout is stable!");
203
                }
204

            
205
                // Copy layout into the view.
206
                let (ref mut viewer, _) = *state.viewer.lock().unwrap();
207
                viewer.update(&layout);
208

            
209
                // Request a redraw (if not already in progress).
210
                render_handle.resume();
211
            }
212

            
213
            // Keep at least 16 milliseconds between two layout runs.
214
            let duration = Instant::now() - start;
215
            debug!("Layout step took {} ms", duration.as_millis());
216
            thread::sleep(Duration::from_millis(16).saturating_sub(duration));
217

            
218
            // If stable pause the thread.
219
            !is_stable
220
        })?)
221
    };
222

            
223
    // Load an LTS from the given path and updates the state.
224
    let load_lts = {
225
        let state = state.clone();
226
        let layout_handle = layout_handle.clone();
227
        let render_handle = render_handle.clone();
228

            
229
        move |path: &Path| {
230
            debug!("Loading LTS {} ...", path.to_string_lossy());
231

            
232
            match File::open(path) {
233
                Ok(file) => {
234
                    match read_aut(file, vec![]) {
235
                        Ok(lts) => {
236
                            let lts = Arc::new(lts);
237
                            info!("Loaded lts {}", lts);
238

            
239
                            // Create the layout and viewer separately to make the initial state sensible.
240
                            let layout = GraphLayout::new(&lts);
241
                            let mut viewer = Viewer::new(&lts);
242

            
243
                            viewer.update(&layout);
244

            
245
                            *state.write().unwrap() = Some(GuiState {
246
                                graph_layout: Mutex::new(layout),
247
                                viewer: Mutex::new((viewer, SharedPixelBuffer::new(1, 1))),
248
                            });
249

            
250
                            // Enable the layout and rendering threads.
251
                            layout_handle.resume();
252
                            render_handle.resume();
253
                        }
254
                        Err(x) => {
255
                            error_dialog::show_error_dialog("Failed to load LTS!", &format!("{}", x));
256
                        }
257
                    }
258
                }
259
                Err(x) => {
260
                    error_dialog::show_error_dialog("Failed to load LTS!", &format!("{}", x));
261
                }
262
            }
263
        }
264
    };
265

            
266
    // When the simulation is toggled enable the layout thread.
267
    {
268
        let layout_handle = layout_handle.clone();
269
        app.on_run_simulation(move |enabled| {
270
            if enabled {
271
                layout_handle.resume();
272
            } else {
273
                layout_handle.pause();
274
            }
275
        })
276
    }
277

            
278
    // Simply return the current canvas, can be updated in the meantime.
279
    {
280
        let canvas = canvas.clone();
281
        let settings = settings.clone();
282
        let render_handle = render_handle.clone();
283

            
284
        app.on_update_canvas(move |width, height, _| {
285
            let mut settings = settings.lock().unwrap();
286
            settings.width = width as u32;
287
            settings.height = height as u32;
288

            
289
            let buffer = canvas.lock().unwrap().clone();
290
            if buffer.width() != settings.width || buffer.height() != settings.height {
291
                // Request another redraw when the size has changed.
292
                debug!(
293
                    "Canvas size changed from {}x{} to {width}x{height}",
294
                    buffer.width(),
295
                    buffer.height()
296
                );
297
                render_handle.resume();
298
            }
299

            
300
            debug!("Updating canvas");
301
            Image::from_rgba8_premultiplied(buffer)
302
        });
303
    }
304

            
305
    // If a redraw was requested resume the render thread.
306
    {
307
        let render_handle = render_handle.clone();
308
        app.on_request_redraw(move || {
309
            render_handle.resume();
310
        })
311
    }
312

            
313
    // Open the file dialog and load another LTS if necessary.
314
    {
315
        let load_lts = load_lts.clone();
316
        app.on_open_filedialog(move || {
317
            let load_lts = load_lts.clone();
318

            
319
            invoke_from_event_loop(move || {
320
                slint::spawn_local(async move {
321
                    if let Some(handle) = rfd::AsyncFileDialog::new().add_filter("", &["aut"]).pick_file().await {
322
                        load_lts(handle.path());
323
                    }
324
                })
325
                .unwrap();
326
            })
327
            .unwrap();
328
        });
329
    }
330

            
331
    // Focus on the graph
332
    {
333
        let settings = settings.clone();
334
        let state = state.clone();
335
        let render_handle = render_handle.clone();
336
        let app_weak = app.as_weak();
337
        let settings = settings.clone();
338

            
339
        app.on_focus_view(move || {
340
            if let Some(app) = app_weak.upgrade() {
341
                if let Some(state) = state.read().unwrap().deref() {
342
                    debug!("Centering view on graph.");
343

            
344
                    let (ref viewer, _) = *state.viewer.lock().unwrap();
345

            
346
                    let center = viewer.center();
347

            
348
                    // Change the view to show the LTS in full.
349
                    app.global::<Settings>().set_view_x(center.x);
350
                    app.global::<Settings>().set_view_y(center.y);
351

            
352
                    let mut settings = settings.lock().unwrap();
353
                    settings.view_x = center.x;
354
                    settings.view_y = center.y;
355

            
356
                    render_handle.resume();
357
                }
358
            }
359
        });
360
    }
361

            
362
    // Loads the LTS given on the command line.
363
    if let Some(path) = &cli.labelled_transition_system {
364
        load_lts(Path::new(path));
365
    }
366

            
367
    app.run()?;
368

            
369
    // Stop the layout and quit.
370
    layout_handle.stop();
371
    render_handle.stop();
372

            
373
    Ok(ExitCode::SUCCESS)
374
}