#[cfg(all(not(feature = "glow"), not(feature = "wgpu")))]
compile_error!("Either glow or wgpu feature must be enabled for eframe to be useful.");
use eframe::egui::Context;
use eframe::{Frame, IntegrationInfo, NativeOptions, Storage};
use egui::{Ui, ViewportBuilder};
use egui_software_backend::{SoftwareBackend, SoftwareBackendAppConfiguration};
use main_thread::IsMainThread;
use std::error::Error;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::Relaxed;
const NUM_BACKENDS: usize = 2;
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum Backend {
SoftwareBackend,
Eframe,
}
static STATE: AtomicUsize = AtomicUsize::new(0);
pub fn overwrite_backend(backend: Backend) {
let state = STATE.load(Relaxed);
if state > NUM_BACKENDS {
return;
}
match backend {
Backend::SoftwareBackend => {
_ = STATE.compare_exchange(state, 1, Relaxed, Relaxed);
}
Backend::Eframe => {
_ = STATE.compare_exchange(state, 2, Relaxed, Relaxed);
}
}
}
pub fn is_launched() -> bool {
STATE.load(Relaxed) > NUM_BACKENDS
}
pub fn get_backend() -> Option<Backend> {
let state = STATE.load(Relaxed);
Some(match state {
2 | 4 => Backend::Eframe,
3 | 1 => Backend::SoftwareBackend,
_ => {
return match determine_backend() {
None => None,
Some(Backend::SoftwareBackend) => {
_ = STATE.compare_exchange(0, 1, Relaxed, Relaxed);
Some(Backend::SoftwareBackend)
}
Some(Backend::Eframe) => {
_ = STATE.compare_exchange(0, 2, Relaxed, Relaxed);
Some(Backend::Eframe)
}
};
}
})
}
#[non_exhaustive]
pub enum BackendInterop<'a> {
SoftwareBackend(SoftwareBackendInterop<'a>),
Eframe(&'a mut Frame),
}
pub struct SoftwareBackendInterop<'a> {
swb: &'a mut SoftwareBackend,
integration_info: &'a mut IntegrationInfo,
storage: &'a mut Option<Box<dyn Storage>>,
}
impl Deref for SoftwareBackendInterop<'_> {
type Target = SoftwareBackend;
fn deref(&self) -> &Self::Target {
self.swb
}
}
impl DerefMut for SoftwareBackendInterop<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.swb
}
}
impl BackendInterop<'_> {
#[must_use]
pub const fn backend(&self) -> Backend {
match self {
BackendInterop::SoftwareBackend(_) => Backend::SoftwareBackend,
BackendInterop::Eframe(_) => Backend::Eframe,
}
}
#[must_use]
pub const fn backend_name(&self) -> &'static str {
match self {
BackendInterop::SoftwareBackend(_) => "Software Backend",
BackendInterop::Eframe(_) => "eframe",
}
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn is_web(&self) -> bool {
false
}
#[must_use]
pub fn into(&self) -> &IntegrationInfo {
match self {
BackendInterop::SoftwareBackend(swbi) => swbi.integration_info,
BackendInterop::Eframe(efr) => efr.info(),
}
}
pub fn storage(&self) -> Option<&dyn Storage> {
match self {
BackendInterop::SoftwareBackend(swbi) => swbi.storage.as_ref().map(Box::as_ref),
BackendInterop::Eframe(efr) => efr.storage(),
}
}
pub fn storage_mut(&mut self) -> Option<&mut (dyn Storage + 'static)> {
match self {
BackendInterop::SoftwareBackend(swbi) => swbi.storage.as_mut().map(Box::as_mut),
BackendInterop::Eframe(efr) => efr.storage_mut(),
}
}
#[cfg(feature = "glow")]
pub fn gl(&mut self) -> Option<&std::sync::Arc<eframe::glow::Context>> {
match self {
BackendInterop::SoftwareBackend(_) => None,
BackendInterop::Eframe(efr) => efr.gl(),
}
}
#[cfg(feature = "glow")]
pub fn register_native_glow_texture(
&mut self,
native: eframe::glow::Texture,
) -> egui::TextureId {
match self {
BackendInterop::SoftwareBackend(_) => egui::TextureId::User(0), BackendInterop::Eframe(efr) => efr.register_native_glow_texture(native),
}
}
}
pub trait App {
fn ui(&mut self, context: &mut Ui, backend: BackendInterop<'_>);
fn on_exit(&mut self) {}
fn save(&mut self, storage: &mut dyn Storage) {
_ = storage;
}
}
struct AppWrapper<T: App>(T, Option<Box<dyn Storage>>, IntegrationInfo);
impl<T: App> eframe::App for AppWrapper<T> {
fn ui(&mut self, ui: &mut Ui, frame: &mut Frame) {
self.0.ui(ui, BackendInterop::Eframe(frame));
}
fn save(&mut self, storage: &mut dyn Storage) {
self.0.save(storage);
}
#[cfg(feature = "glow")]
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
self.0.on_exit();
}
#[cfg(not(feature = "glow"))]
fn on_exit(&mut self) {
self.0.on_exit()
}
}
impl<T: App> egui_software_backend::App for AppWrapper<T> {
fn ui(&mut self, ui: &mut Ui, software_backend: &mut SoftwareBackend) {
self.2.cpu_usage = software_backend.last_frame_time().map(|a| a.as_secs_f32());
self.0.ui(
ui,
BackendInterop::SoftwareBackend(SoftwareBackendInterop {
swb: software_backend,
integration_info: &mut self.2,
storage: &mut self.1,
}),
);
}
fn on_exit(&mut self, _ctx: &Context) {
if let Some(store) = self.1.as_mut() {
self.0.save(store.as_mut());
store.flush();
}
self.0.on_exit();
}
}
#[derive(Clone)]
pub struct BackendConfiguration {
viewport: ViewportBuilder,
eframe_options: Option<NativeOptions>,
software_backend_options: Option<SoftwareBackendAppConfiguration>,
}
impl Default for BackendConfiguration {
fn default() -> Self {
Self {
viewport: ViewportBuilder::default(),
eframe_options: None,
software_backend_options: Some(SoftwareBackendAppConfiguration {
caching: false,
..Default::default()
}),
}
}
}
impl BackendConfiguration {
#[must_use]
pub const fn new(
viewport_builder: ViewportBuilder,
native_options: NativeOptions,
software_backend_options: SoftwareBackendAppConfiguration,
) -> Self {
Self {
viewport: viewport_builder,
eframe_options: Some(native_options),
software_backend_options: Some(software_backend_options),
}
}
}
impl From<ViewportBuilder> for BackendConfiguration {
fn from(value: ViewportBuilder) -> Self {
Self {
viewport: value,
eframe_options: None,
software_backend_options: None,
}
}
}
impl From<NativeOptions> for BackendConfiguration {
fn from(value: NativeOptions) -> Self {
Self {
viewport: value.viewport.clone(),
eframe_options: Some(value),
software_backend_options: None,
}
}
}
impl From<SoftwareBackendAppConfiguration> for BackendConfiguration {
fn from(value: SoftwareBackendAppConfiguration) -> Self {
Self {
viewport: value.viewport_builder.clone(),
eframe_options: None,
software_backend_options: Some(value),
}
}
}
#[cfg(feature = "persistence")]
struct KVStorage {
ron_file: std::path::PathBuf,
kv: std::collections::HashMap<String, String>,
dirty: bool,
}
#[cfg(feature = "persistence")]
impl KVStorage {
pub fn new(app_name: &str) -> Option<Self> {
let data_dir = eframe::storage_dir(app_name)?;
let ron_file = data_dir.join("app.ron");
let initial_data = if ron_file.exists() {
let file = std::fs::File::open(&ron_file)
.inspect_err(|e| {
log::error!(
"Failed to read application state. Could not read file {} err={e}",
ron_file.display()
);
})
.ok()?;
let reader = std::io::BufReader::new(file);
ron::de::from_reader(reader)
.inspect_err(|e| {
log::error!(
"Failed to read application state. File contains invalid data {} err={e}",
ron_file.display()
);
})
.ok()?
} else {
std::collections::HashMap::new()
};
Some(Self {
ron_file,
kv: initial_data,
dirty: false,
})
}
}
#[cfg(feature = "persistence")]
impl Storage for KVStorage {
fn get_string(&self, key: &str) -> Option<String> {
self.kv.get(key).cloned()
}
fn set_string(&mut self, key: &str, value: String) {
self.kv.insert(key.to_string(), value);
self.dirty = true;
}
fn flush(&mut self) {
if !self.dirty {
return;
}
let rp = self.ron_file.as_path();
if let Some(parent) = rp.parent()
&& !parent.exists()
{
_ = std::fs::create_dir_all(parent);
}
let Ok(file) = std::fs::File::create(rp).inspect_err(|e| {
log::error!(
"Failed to save application state. Could not create file {} err={e}",
rp.display()
);
}) else {
return;
};
let mut writer = std::io::BufWriter::new(file);
if let Err(e) = ron::Options::default().to_io_writer_pretty(
&mut writer,
&self.kv,
ron::ser::PrettyConfig::default(),
) {
log::error!(
"Failed to save application state. Could not write file {} err={e}",
rp.display()
);
return;
}
self.dirty = false;
}
}
pub fn run_app<T: App>(
app_name: &str,
backend_configuration: impl Into<BackendConfiguration>,
mut app_factory: impl FnMut(Context, Option<&dyn Storage>) -> T,
) -> Result<(), Box<dyn Error>> {
if IsMainThread::OtherThread == main_thread::is_main_thread() {
return Err("Current thread is not the main thread".into());
}
if is_launched() {
return Err("Application already launched".into());
}
let config = backend_configuration.into();
match get_backend() {
None | Some(Backend::SoftwareBackend) => {
STATE.store(3, Relaxed);
let mut cfg_to_use = config.software_backend_options.unwrap_or_default();
cfg_to_use.viewport_builder = config.viewport;
let app_name = app_name.to_string();
if let Err(e) =
egui_software_backend::run_app_with_software_backend(cfg_to_use, move |ctx| {
#[cfg(feature = "persistence")]
let storage: Option<Box<dyn Storage>> =
KVStorage::new(&app_name).map(|a| Box::new(a) as Box<dyn Storage>);
#[cfg(not(feature = "persistence"))]
let storage: Option<Box<dyn Storage>> = None;
#[cfg(not(feature = "persistence"))]
let _ignored = &app_name;
let integration_info = IntegrationInfo { cpu_usage: None };
AppWrapper(
app_factory(ctx, storage.as_ref().map(Box::as_ref)),
storage,
integration_info,
)
})
{
return Err(Box::new(e));
}
Ok(())
}
Some(Backend::Eframe) => {
STATE.store(4, Relaxed);
let mut cfg_to_use = config.eframe_options.unwrap_or_default();
cfg_to_use.viewport = config.viewport;
let integration_info = IntegrationInfo { cpu_usage: None };
if let Err(e) = eframe::run_native(
app_name,
cfg_to_use,
Box::new(move |ctx| {
Ok(Box::new(AppWrapper(
app_factory(ctx.egui_ctx.clone(), ctx.storage),
None,
integration_info,
)))
}),
) {
return Err(Box::new(e));
}
Ok(())
}
}
}
#[cfg(all(not(windows), not(target_os = "linux")))]
fn determine_backend() -> Option<Backend> {
Some(Backend::Eframe)
}
#[cfg(target_os = "linux")]
#[allow(clippy::unnecessary_wraps)]
fn determine_backend() -> Option<Backend> {
let Ok(display) = std::env::var("DISPLAY") else {
return Some(Backend::Eframe);
};
if !display.starts_with(':') && !display.contains("/unix:") {
return Some(Backend::SoftwareBackend);
}
Some(Backend::Eframe)
}
#[cfg(windows)]
fn determine_backend() -> Option<Backend> {
if IsMainThread::OtherThread == main_thread::is_main_thread() {
return None;
}
if unsafe {
windows_sys::Win32::UI::WindowsAndMessaging::GetSystemMetrics(
windows_sys::Win32::UI::WindowsAndMessaging::SM_REMOTESESSION,
) != 0
} {
return Some(Backend::SoftwareBackend);
}
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
{
if raw_cpuid::CpuId::new().get_hypervisor_info().is_some() {
if std::fs::exists("C:\\Windows\\System32\\vm3dgl64.dll").unwrap_or(false) {
return Some(Backend::SoftwareBackend);
}
if std::fs::exists("C:\\Windows\\System32\\VBoxGL.dll").unwrap_or(false) {
return Some(Backend::SoftwareBackend);
}
}
}
let Ok(mut glfw) = glfw::init::<()>(None) else {
return Some(Backend::SoftwareBackend);
};
glfw.window_hint(glfw::WindowHint::ContextVersion(3, 2));
glfw.window_hint(glfw::WindowHint::Visible(false));
let Some((wnd, events)) = glfw.create_window(
128,
128,
"opengl version detector",
glfw::WindowMode::Windowed,
) else {
return Some(Backend::SoftwareBackend);
};
drop(events);
drop(wnd);
Some(Backend::Eframe)
}