use eframe::{CreationContext, egui};
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
fmt::{Debug, Display},
};
use crate::{
errors::{AppError, ErrorManager},
file_handler::{File, FileHandler},
settings::Settings,
};
pub trait BladvakApp<'a>: Sized {
fn top_panel(&mut self, _ui: &mut egui::Ui, _error_manager: &mut ErrorManager) {}
fn panel_list(&self) -> Vec<Box<dyn BladvakPanel<App = Self>>> {
vec![]
}
fn central_panel(&mut self, ui: &mut egui::Ui, _error_manager: &mut ErrorManager) {
crate::utils::central_ui(ui, |ui| {
ui.heading("Welcome to baldvak");
ui.label("Use the BladvakApp trait to override function and customize the app");
});
}
fn side_panel_frame(&mut self, ui: &mut egui::Ui) -> egui::Frame {
egui::Frame::central_panel(&ui.ctx().global_style())
.inner_margin(0)
.outer_margin(0)
}
fn side_panel(&mut self, ui: &mut egui::Ui, func_ui: impl FnOnce(&mut egui::Ui, &mut Self)) {
egui::Frame::new()
.inner_margin(8)
.fill(ui.ctx().global_style().visuals.panel_fill)
.show(ui, |ui| {
func_ui(ui, self);
});
}
fn window_panel_frame(&mut self, ui: &mut egui::Ui) -> egui::Frame {
egui::Frame::window(&ui.ctx().global_style())
}
fn handle_file(&mut self, _file: File) -> Result<(), AppError> {
Ok(())
}
fn menu_file(&mut self, _ui: &mut egui::Ui, _error_manager: &mut ErrorManager) {}
fn name() -> String;
fn version() -> String;
#[must_use]
fn repo_url() -> String {
String::new()
}
#[must_use]
fn icon() -> &'a [u8] {
&[]
}
fn is_open_button(&self) -> bool {
false
}
fn is_side_panel(&self) -> bool {
true
}
fn panel_options_as_menu(&self) -> bool {
true
}
fn min_width_sidebar(&self) -> f32 {
0.0
}
fn try_new_with_args(
saved_state: Self,
cc: &CreationContext<'_>,
args: &[String],
error_manager: &mut ErrorManager,
) -> Result<Self, AppError>;
fn on_save(&mut self) {}
}
pub trait BladvakPanel: Debug {
type App;
fn name(&self) -> &str;
fn has_settings(&self) -> bool;
fn ui_settings(&self, app: &mut Self::App, ui: &mut egui::Ui, error_manager: &mut ErrorManager);
fn has_ui(&self) -> bool;
fn ui(&self, app: &mut Self::App, ui: &mut egui::Ui, error_manager: &mut ErrorManager);
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PanelOpen {
AsWindows,
AsSideBar,
None,
}
#[allow(clippy::derivable_impls)]
impl Default for PanelOpen {
fn default() -> Self {
#[cfg(not(target_arch = "wasm32"))]
{
Self::AsSideBar
}
#[cfg(target_arch = "wasm32")]
{
Self::AsWindows
}
}
}
impl Display for PanelOpen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AsSideBar => write!(f, "Sidebar"),
Self::AsWindows => write!(f, "Windows"),
Self::None => write!(f, "None"),
}
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct PanelState {
pub(crate) open: PanelOpen,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BladvakSavedState {
pub(crate) settings: Settings,
pub(crate) panel_state: BTreeMap<String, PanelState>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Bladvak<App> {
pub(crate) app: App,
pub(crate) internal: BladvakSavedState,
pub(crate) ignore_saved_state: bool,
#[serde(skip)]
pub(crate) error_manager: ErrorManager,
#[serde(skip)]
pub(crate) file_handler: FileHandler,
#[serde(skip)]
pub(crate) panel_list: Vec<Box<dyn BladvakPanel<App = App>>>,
}
#[cfg(not(target_arch = "wasm32"))]
pub type MainResult = eframe::Result;
#[cfg(target_arch = "wasm32")]
pub type MainResult = ();
impl<M> Bladvak<M>
where
M: for<'a> BladvakApp<'a> + Debug + Default + Serialize + for<'a> Deserialize<'a> + 'static,
{
fn try_new_with_args(cc: &CreationContext<'_>, vec_args: &[String]) -> Self {
let (saved_state_app, saved_internal) = if let Some(saved) = Self::get_saved_app_state(cc) {
if saved.ignore_saved_state {
log::info!("Explicitly ignoring saved state");
(M::default(), None)
} else {
log::info!("Using saved state");
(saved.app, Some(saved.internal))
}
} else {
(M::default(), None)
};
let mut error_manager = ErrorManager::default();
let (app, creation_error) =
match M::try_new_with_args(saved_state_app, cc, vec_args, &mut error_manager) {
Ok(app) => (app, None),
Err(err) => (M::default(), Some(err)),
};
let panel_list = app.panel_list();
let bladvak_internal = if let Some(saved_state) = saved_internal {
let hashet_saved = saved_state
.panel_state
.iter()
.map(|p| p.0.as_str())
.collect::<std::collections::HashSet<_>>();
let hashet_current = panel_list
.iter()
.map(|p| p.name())
.collect::<std::collections::HashSet<_>>();
if hashet_saved == hashet_current {
log::info!("Using saved panels state");
saved_state
} else {
let BladvakSavedState {
settings: old_settings,
panel_state: old_panel_state,
} = saved_state;
log::info!("Trying to use old saved panels state");
let mut new_panel_state = BTreeMap::new();
for one_panel in &panel_list {
let is_panel_present = old_panel_state
.iter()
.find(|panel| panel.0 == one_panel.name());
if let Some(saved_panel_state) = is_panel_present {
new_panel_state
.insert(one_panel.name().to_string(), saved_panel_state.1.clone());
} else {
new_panel_state.insert(one_panel.name().to_string(), PanelState::default());
}
}
BladvakSavedState {
settings: old_settings,
panel_state: new_panel_state,
}
}
} else {
let mut panel_state = BTreeMap::new();
for one_panel in &panel_list {
panel_state.insert(one_panel.name().to_string(), PanelState::default());
}
BladvakSavedState {
settings: Settings::default(),
panel_state,
}
};
if let Some(err) = creation_error {
error_manager.add_error(err);
}
Self {
app,
internal: bladvak_internal,
ignore_saved_state: false,
error_manager,
file_handler: FileHandler::default(),
panel_list,
}
}
pub(crate) fn central_panel(&mut self, ui: &mut egui::Ui) {
egui::CentralPanel::default()
.frame(
egui::Frame::central_panel(&ui.ctx().global_style())
.inner_margin(0)
.outer_margin(0),
)
.show(ui, |ui| {
self.app.central_panel(ui, &mut self.error_manager);
for one_panel in self.panel_list.iter().filter(|p| p.has_ui()) {
let panel_name = one_panel.name();
if let Some(panel_state) = self.internal.panel_state.get_mut(panel_name)
&& let PanelOpen::AsWindows = panel_state.open
{
let mut open = true;
egui::Window::new(panel_name)
.open(&mut open)
.frame(self.app.window_panel_frame(ui))
.show(ui.ctx(), |window_ui| {
one_panel.ui(&mut self.app, window_ui, &mut self.error_manager);
});
if !open {
panel_state.open = PanelOpen::AsSideBar;
}
}
}
});
}
pub(crate) fn top_panel(&mut self, ui: &mut egui::Ui) {
egui::Panel::top("top_panel").show(ui, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
ui.menu_button("File", |ui| {
self.app.menu_file(ui, &mut self.error_manager);
if self.app.is_open_button() && ui.button("Open").clicked() {
ui.close();
self.file_handler.handle_file_open();
}
if self.app.panel_options_as_menu() && !self.internal.panel_state.is_empty() {
ui.menu_button("Panels", |ui| {
ui.menu_button("All", |ui| {
if ui.button(PanelOpen::AsSideBar.to_string()).clicked() {
for one_panel in &mut self.internal.panel_state {
one_panel.1.open = PanelOpen::AsSideBar;
}
}
if ui.button(PanelOpen::AsWindows.to_string()).clicked() {
for one_panel in &mut self.internal.panel_state {
one_panel.1.open = PanelOpen::AsWindows;
}
}
if ui.button(PanelOpen::None.to_string()).clicked() {
for one_panel in &mut self.internal.panel_state {
one_panel.1.open = PanelOpen::None;
}
}
});
for one_panel in &mut self.internal.panel_state {
if let Some(panel) =
self.panel_list.iter().find(|p| p.name() == one_panel.0)
{
if !panel.has_ui() {
continue;
}
} else {
continue;
}
ui.menu_button(one_panel.0, |ui| {
let value = &mut one_panel.1.open;
ui.selectable_value(
value,
PanelOpen::AsSideBar,
PanelOpen::AsSideBar.to_string(),
);
ui.selectable_value(
value,
PanelOpen::AsWindows,
PanelOpen::AsWindows.to_string(),
);
ui.selectable_value(
value,
PanelOpen::None,
PanelOpen::None.to_string(),
);
});
}
});
}
if ui.button("Settings").clicked() {
self.internal.settings.open = true;
}
let is_web = cfg!(target_arch = "wasm32");
if !is_web && ui.button("Quit").clicked() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
}
egui::warn_if_debug_build(ui);
});
self.app.top_panel(ui, &mut self.error_manager);
});
});
}
pub(crate) fn side_panel(&mut self, ui: &mut egui::Ui) {
let is_panels_in_sidebar = self.panel_list.iter().any(|p| {
p.has_ui()
&& self
.internal
.panel_state
.get(p.name())
.is_some_and(|p_state| p_state.open == PanelOpen::AsSideBar)
});
if !is_panels_in_sidebar {
return;
}
egui::Panel::right("my_panel")
.frame(self.app.side_panel_frame(ui))
.min_size(self.app.min_width_sidebar())
.show(ui, |side_panel_ui| {
for (idx, one_panel) in self
.panel_list
.iter()
.filter(|p| {
p.has_ui()
&& self
.internal
.panel_state
.get(p.name())
.is_some_and(|p_state| p_state.open == PanelOpen::AsSideBar)
})
.enumerate()
{
if idx != 0 {
side_panel_ui.separator();
}
self.app.side_panel(side_panel_ui, |ui, app| {
one_panel.ui(app, ui, &mut self.error_manager);
});
}
side_panel_ui.with_layout(
egui::Layout::bottom_up(egui::Align::RIGHT),
|ui: &mut egui::Ui| {
egui::warn_if_debug_build(ui);
},
);
});
}
#[cfg(not(target_arch = "wasm32"))]
pub fn bladvak_main() -> eframe::Result {
use std::env;
use crate::app::Bladvak;
env_logger::init();
let viewport = egui::ViewportBuilder::default()
.with_drag_and_drop(true)
.with_inner_size([400.0, 300.0])
.with_min_inner_size([300.0, 220.0]);
let icon_data = M::icon();
let viewport = if icon_data.is_empty() {
viewport
} else {
let ico = match eframe::icon_data::from_png_bytes(icon_data) {
Ok(ico) => ico,
Err(e) => {
return Err(eframe::Error::AppCreation(
format!("Error loading the ico: {e}").into(),
));
}
};
viewport.with_icon(ico)
};
let native_options = eframe::NativeOptions {
viewport,
..Default::default()
};
let args: Vec<String> = env::args().collect();
eframe::run_native(
&M::name(),
native_options,
Box::new(|cc| Ok(Box::new(Bladvak::<M>::try_new_with_args(cc, &args)))),
)
}
#[cfg(target_arch = "wasm32")]
pub fn bladvak_main() {
use eframe::wasm_bindgen::JsCast as _;
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
let web_options = eframe::WebOptions::default();
wasm_bindgen_futures::spawn_local(async {
let document = eframe::web_sys::window()
.expect("No window")
.document()
.expect("No document");
let canvas = document
.get_element_by_id("the_canvas_id")
.expect("Failed to find the_canvas_id")
.dyn_into::<eframe::web_sys::HtmlCanvasElement>()
.expect("the_canvas_id was not a HtmlCanvasElement");
let start_result = eframe::WebRunner::new()
.start(
canvas,
web_options,
Box::new(|cc| Ok(Box::new(Bladvak::<M>::try_new_with_args(cc, &[])))),
)
.await;
if let Some(loading_text) = document.get_element_by_id("loading_text") {
match start_result {
Ok(_) => {
loading_text.remove();
}
Err(e) => {
loading_text.set_inner_html(
"<p> The app has crashed. See the developer console for details. </p>",
);
panic!("Failed to start app: {e:?}");
}
}
}
});
}
#[must_use]
pub(crate) fn get_saved_app_state(cc: &eframe::CreationContext<'_>) -> Option<Bladvak<M>> {
if let Some(storage) = cc.storage
&& let Some(saved_app_state) = eframe::get_value::<Bladvak<M>>(storage, eframe::APP_KEY)
{
log::info!("Loading saved app state");
return Some(saved_app_state);
}
None
}
}
impl<M> eframe::App for Bladvak<M>
where
M: for<'a> BladvakApp<'a> + Debug + Default + Serialize + for<'a> Deserialize<'a> + 'static,
{
fn save(&mut self, storage: &mut dyn eframe::Storage) {
self.app.on_save();
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
self.top_panel(ui);
if self.app.is_side_panel() {
self.side_panel(ui);
}
self.central_panel(ui);
match self.file_handler.handle_files(ui) {
Ok(Some(file)) => {
if let Err(err) = self.app.handle_file(file) {
self.error_manager.add_error(err);
}
ui.ctx().request_repaint();
}
Ok(None) => {
}
Err(err) => {
self.error_manager.add_error(err);
}
}
self.show_error_manager(ui);
self.show_setting(ui, frame);
}
}