Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract parts of gui into smaller components #7

Merged
119 changes: 10 additions & 109 deletions src/gui.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Main GUI code

use std::io::Write;
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, Sender};
use std::io::Write;

#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
Expand All @@ -11,14 +11,15 @@ use web_time::Instant;

use eframe::egui;
use egui::FontFamily::Proportional;
use egui::{TextStyle::*, Align2, ProgressBar, Image};
use egui::{Align, Button, CollapsingHeader, Color32, FontFamily, FontId, Key, Layout, Modifiers, Vec2};
use egui::TextStyle::*;
use egui::{Align, CollapsingHeader, Color32, FontFamily, FontId, Key, Layout, Modifiers, Vec2};

use futures::StreamExt;
use log::*;

use mithril::telemetry::*;

mod components;
mod fc_settings;
mod map;
mod maxi_grid;
Expand All @@ -28,41 +29,20 @@ mod simulation_settings;
mod tabs;
mod theme;
mod top_bar;
pub mod windows; // TODO: make this private (it is public because it has ARCHIVE)

use crate::data_source::*;
use crate::file::*;
use crate::settings::AppSettings;
use crate::simulation::*;
use crate::state::*;

use crate::gui::components::top_menu_bar::create_top_menu_bar;
use crate::gui::fc_settings::*;
use crate::gui::simulation_settings::*;
use crate::gui::tabs::*;
use crate::gui::theme::*;
use crate::gui::top_bar::*;

// Log files included with the application. These should probably be fetched
// if necessary to reduce application size.
// TODO: migrate old launches
pub const ARCHIVE: [(&str, Option<&'static str>, Option<&'static str>); 5] = [
("Zülpich #1", None, None),
("Zülpich #2", None, None),
(
"DARE (FC A)",
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/dare_launch_a_telem_filtered.json"),
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/dare_launch_a_flash_filtered.json"),
),
(
"DARE (FC B)",
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/dare_launch_b_telem_filtered.json"),
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/dare_launch_b_flash_filtered.json"),
),
(
"EuRoC 2023 (ÆSIR Signý)",
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/euroc_2023_telem_filtered.json"),
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/euroc_2023_flash_filtered.json"),
),
];
use crate::gui::windows::archive::open_archive_window;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely a fan of moving this somewhere else. In the future, I'd like to have some sort of Archive struct that is a bit smarter about things like caching, but that would probably be independent of the Window. Also like the structure with a separate windows folder. We might have more of these in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally agree, it's more predictable and more similar to how big UI are built


#[derive(Debug)]
enum ArchiveLoadProgress {
Expand Down Expand Up @@ -153,7 +133,7 @@ impl Sam {
Ok(chunk) => {
cursor.write_all(&chunk).unwrap();
progress = u64::min(progress + chunk.len() as u64, total_size);
if progress == total_size || progress > last_progress + 256*1024 {
if progress == total_size || progress > last_progress + 256 * 1024 {
let _ = progress_sender.send(ArchiveLoadProgress::Progress((progress, total_size)));
last_progress = progress;
}
Expand Down Expand Up @@ -394,91 +374,12 @@ impl Sam {
}

// Top menu bar
egui::TopBottomPanel::top("menubar").min_height(30.0).max_height(30.0).show(ctx, |ui| {
ui.set_enabled(!self.archive_window_open);
ui.horizontal_centered(|ui| {
let image = if ui.style().visuals.dark_mode {
Image::new(egui::include_image!("../assets/logo_dark_mode.png"))
} else {
Image::new(egui::include_image!("../assets/logo_light_mode.png"))
};
ui.add(image.max_size(Vec2::new(ui.available_width(), 20.0)));

ui.separator();
egui::widgets::global_dark_light_mode_switch(ui);
ui.separator();

ui.selectable_value(&mut self.tab, GuiTab::Launch, "🚀 Launch (F1)");
ui.selectable_value(&mut self.tab, GuiTab::Plot, "📈 Plot (F2)");
ui.selectable_value(&mut self.tab, GuiTab::Configure, "⚙ Configure (F3)");

ui.separator();

// Opening files manually is not available on web assembly
#[cfg(target_arch = "x86_64")]
if ui.selectable_label(false, "🗁 Open Log File").clicked() {
if let Some(data_source) = open_log_file() {
self.open_log_file(data_source);
}
}

// Toggle archive panel
ui.toggle_value(&mut self.archive_window_open, "🗄 Flight Archive");

// Toggle archive panel
if ui.selectable_label(self.data_source.simulation_settings().is_some(), "💻 Simulate").clicked() {
self.data_source = Box::new(SimulationDataSource::default());
}

// Show a button to the right to close the current log/simulation and go back to
// live view
ui.allocate_ui_with_layout(ui.available_size(), Layout::right_to_left(Align::Center), |ui| {
if self.data_source.is_log_file() || self.data_source.simulation_settings().is_some() {
if ui.button("❌").clicked() {
self.close_data_source();
}
}
});
});
});
create_top_menu_bar(self, ctx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to say I'm not the biggest fan of passing self around to other files because that might make it more difficult in the future to tell where the contents of Sam are being modified. On the other hand I can see why it would be easier this way.

I think we could pass in something for set_enabled (and this would probably be done similarly for the other panels), and a reference to the data source, but for everything where we change state from within the panel, we might have to implement a response system similar to other egui widgets. So for instance having the function/method return some sort of enum like:

enum TopBarResponse {
    ArchiveWindowOpened,
    TabSelected(GuiTab),
    // etc
}

That's a bit more involved, so I think it may not be necessary to do that right now, but I think long-term this would be the cleanest solution, even though It would mean a bit more code in src/gui.rs.


// A window to open archived logs directly in the application
let mut archive_open = self.archive_window_open; // necessary to avoid mutably borrowing self
egui::Window::new("Flight Archive").open(&mut archive_open).min_width(300.0).anchor(Align2::CENTER_CENTER, [0.0, 0.0]).resizable(false).collapsible(false).show(ctx, |ui| {
ui.add_space(10.0);
open_archive_window(ctx, &mut archive_open, self);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, great fan of moving some of the more specific UI stuff out of here though. For windows it might be interesting to have a system where windows can return values, in this case a flight log, to src/gui.rs. Something along the lines of:

if let Some(log) = ui.add(archive_window).result() {
    self.open_archive_log(log);
}

Same as above though, this isn't necessarily urgent.


for (i, (title, telem, flash)) in ARCHIVE.iter().enumerate() {
if i != 0 {
ui.separator();
}

ui.horizontal(|ui| {
ui.label(*title);
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.add_enabled(flash.is_some(), Button::new("🖴 Flash")).clicked() {
self.open_archive_log(flash.unwrap());
}

if ui.add_enabled(telem.is_some(), Button::new("📡 Telemetry")).clicked() {
self.open_archive_log(telem.unwrap());
}
});
});
}

ui.add_space(10.0);
ui.horizontal(|ui| {
ui.add_visible_ui(self.archive_progress.is_some(), |ui| {
let (done, total) = self.archive_progress.unwrap_or((0, 0));
let f = (total > 0).then(|| done as f32 / total as f32).unwrap_or(0.0);
let text = format!("{:.2}MiB / {:.2}MiB", done as f32 / (1024.0*1024.0), total as f32 / (1024.0*1024.0));
ui.add_sized([ui.available_width(), 20.0], ProgressBar::new(f).text(text));
});
});
ui.add_space(10.0);

ui.checkbox(&mut self.replay_logs, "Replay logs");
});
self.archive_window_open = archive_open;

if self.data_source.simulation_settings().is_some() {
Expand Down
1 change: 1 addition & 0 deletions src/gui/components/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod top_menu_bar;
57 changes: 57 additions & 0 deletions src/gui/components/top_menu_bar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::data_source::SimulationDataSource;
use crate::file::open_log_file;
use crate::gui::tabs::GuiTab;
use crate::gui::Sam;

use eframe::egui;
use egui::{Align, Context, Layout, Vec2};

pub fn create_top_menu_bar(sam: &mut Sam, ctx: &Context) {
egui::TopBottomPanel::top("menubar").min_height(30.0).max_height(30.0).show(ctx, |ui| {
ui.set_enabled(!sam.archive_window_open);
ui.horizontal_centered(|ui| {
let image = if ui.style().visuals.dark_mode {
egui::Image::new(egui::include_image!("../../../assets/logo_dark_mode.png"))
} else {
egui::Image::new(egui::include_image!("../../../assets/logo_light_mode.png"))
};
ui.add(image.max_size(Vec2::new(ui.available_width(), 20.0)));

ui.separator();
egui::widgets::global_dark_light_mode_switch(ui);
ui.separator();

ui.selectable_value(&mut sam.tab, GuiTab::Launch, "🚀 Launch (F1)");
ui.selectable_value(&mut sam.tab, GuiTab::Plot, "📈 Plot (F2)");
ui.selectable_value(&mut sam.tab, GuiTab::Configure, "⚙ Configure (F3)");

ui.separator();

// Opening files manually is not available on web assembly
#[cfg(target_arch = "x86_64")]
if ui.selectable_label(false, "🗁 Open Log File").clicked() {
if let Some(data_source) = open_log_file() {
sam.open_log_file(data_source);
}
}

// Toggle archive panel
ui.toggle_value(&mut sam.archive_window_open, "🗄 Flight Archive");

// Toggle archive panel
if ui.selectable_label(sam.data_source.simulation_settings().is_some(), "💻 Simulate").clicked() {
sam.data_source = Box::new(SimulationDataSource::default());
}

// Show a button to the right to close the current log/simulation and go back to
// live view
ui.allocate_ui_with_layout(ui.available_size(), Layout::right_to_left(Align::Center), |ui| {
if sam.data_source.is_log_file() || sam.data_source.simulation_settings().is_some() {
if ui.button("❌").clicked() {
sam.close_data_source();
}
}
});
});
});
}
4 changes: 1 addition & 3 deletions src/gui/simulation_settings.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use egui::{DragValue, InnerResponse, Ui};

use crate::gui::windows::archive::ARCHIVE;
use crate::simulation::SimulationSettings;

use super::ARCHIVE;

pub trait SimulationSettingsUiExt {
fn ui(&mut self, ui: &mut Ui) -> InnerResponse<()>;
}
Expand Down
74 changes: 74 additions & 0 deletions src/gui/windows/archive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use crate::gui::Sam;
use eframe::egui;
use egui::{Align, Align2, Button, Layout, ProgressBar};

// Log files included with the application. These should probably be fetched
// if necessary to reduce application size.
// TODO: migrate old launches
pub const ARCHIVE: [(&str, Option<&'static str>, Option<&'static str>); 5] = [
("Zülpich #1", None, None),
("Zülpich #2", None, None),
(
"DARE (FC A)",
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/dare_launch_a_telem_filtered.json"),
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/dare_launch_a_flash_filtered.json"),
),
(
"DARE (FC B)",
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/dare_launch_b_telem_filtered.json"),
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/dare_launch_b_flash_filtered.json"),
),
(
"EuRoC 2023 (ÆSIR Signý)",
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/euroc_2023_telem_filtered.json"),
Some("https://raw.githubusercontent.com/tudsat-rocket/sam/main/archive/euroc_2023_flash_filtered.json"),
),
];

pub fn open_archive_window(ctx: &egui::Context, archive_open: &mut bool, sam: &mut Sam) {
egui::Window::new("Flight Archive")
.open(archive_open)
.min_width(300.0)
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
.resizable(false)
.collapsible(false)
.show(ctx, |ui| {
ui.add_space(10.0);

for (i, (title, telem, flash)) in ARCHIVE.iter().enumerate() {
if i != 0 {
ui.separator();
}

ui.horizontal(|ui| {
ui.label(*title);
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.add_enabled(flash.is_some(), Button::new("🖴 Flash")).clicked() {
sam.open_archive_log(flash.unwrap());
}

if ui.add_enabled(telem.is_some(), Button::new("📡 Telemetry")).clicked() {
sam.open_archive_log(telem.unwrap());
}
});
});
}

ui.add_space(10.0);
ui.horizontal(|ui| {
ui.add_visible_ui(sam.archive_progress.is_some(), |ui| {
let (done, total) = sam.archive_progress.unwrap_or((0, 0));
let f = (total > 0).then(|| done as f32 / total as f32).unwrap_or(0.0);
let text = format!(
"{:.2}MiB / {:.2}MiB",
done as f32 / (1024.0 * 1024.0),
total as f32 / (1024.0 * 1024.0)
);
ui.add_sized([ui.available_width(), 20.0], ProgressBar::new(f).text(text));
});
});
ui.add_space(10.0);

ui.checkbox(&mut sam.replay_logs, "Replay logs");
});
}
1 change: 1 addition & 0 deletions src/gui/windows/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod archive;
Loading