use crate::{AppConfig, AppExit, HookFn};
use oxiui_core::UiError;
pub type ContentFn = Box<dyn FnMut(&mut dyn oxiui_core::UiCtx) + Send>;
#[derive(Default)]
pub struct LifecycleConfig {
pub on_close: Vec<HookFn>,
pub on_resize: Vec<HookFn>,
pub on_focus: Vec<HookFn>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LifecycleSnapshot {
pub size: Option<(f32, f32)>,
pub focused: Option<bool>,
pub close_requested: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LifecycleEvent {
Resized(f32, f32),
Focus(bool),
Close,
}
#[derive(Debug, Default)]
pub struct LifecycleTracker {
last_size: Option<(f32, f32)>,
last_focused: Option<bool>,
close_fired: bool,
}
impl LifecycleTracker {
pub fn observe(&mut self, snap: LifecycleSnapshot) -> Vec<LifecycleEvent> {
let mut events = Vec::new();
if let Some(size) = snap.size {
if self.last_size != Some(size) {
self.last_size = Some(size);
events.push(LifecycleEvent::Resized(size.0, size.1));
}
}
if let Some(focused) = snap.focused {
if self.last_focused != Some(focused) {
self.last_focused = Some(focused);
events.push(LifecycleEvent::Focus(focused));
}
}
if snap.close_requested && !self.close_fired {
self.close_fired = true;
events.push(LifecycleEvent::Close);
}
events
}
}
pub trait BackendRunner: Send + 'static {
fn run(
self: Box<Self>,
config: AppConfig,
content: ContentFn,
lifecycle: LifecycleConfig,
) -> Result<AppExit, UiError>;
}
#[cfg(feature = "egui")]
pub struct EguiRunner {
pub(crate) theme: Box<dyn oxiui_core::Theme>,
pub(crate) on_init: Vec<HookFn>,
pub(crate) on_frame: Vec<HookFn>,
pub(crate) plugins: Vec<Box<dyn crate::Plugin>>,
pub(crate) frame_skip: bool,
pub(crate) egui_frame_hooks: Vec<crate::EguiFrameHook>,
}
#[cfg(feature = "egui")]
impl Default for EguiRunner {
fn default() -> Self {
Self {
theme: oxiui_theme::cooljapan_default(),
on_init: Vec::new(),
on_frame: Vec::new(),
plugins: Vec::new(),
frame_skip: false,
egui_frame_hooks: Vec::new(),
}
}
}
#[cfg(feature = "egui")]
impl EguiRunner {
pub fn new() -> Self {
Self::default()
}
pub fn theme(mut self, theme: Box<dyn oxiui_core::Theme>) -> Self {
self.theme = theme;
self
}
}
#[cfg(feature = "egui")]
impl BackendRunner for EguiRunner {
fn run(
self: Box<Self>,
config: AppConfig,
content: ContentFn,
lifecycle: LifecycleConfig,
) -> Result<AppExit, UiError> {
#[cfg(not(target_arch = "wasm32"))]
{
(*self).run_native(config, content, lifecycle)
}
#[cfg(target_arch = "wasm32")]
{
let _ = (self, config, content, lifecycle);
Err(UiError::Unsupported(
"On wasm32, use `oxiui_web::mount(canvas_id)` instead of App::run().".to_string(),
))
}
}
}
#[cfg(all(feature = "egui", not(target_arch = "wasm32")))]
impl EguiRunner {
fn run_native(
self,
mut config: AppConfig,
content: ContentFn,
lifecycle: LifecycleConfig,
) -> Result<AppExit, UiError> {
use eframe::NativeOptions;
use oxiui_egui::palette_to_egui_visuals;
let EguiRunner {
theme,
on_init,
on_frame,
mut plugins,
frame_skip,
egui_frame_hooks,
} = self;
let palette = theme.palette().clone();
let title = config.title.clone();
let width = config.width;
let height = config.height;
let visuals = palette_to_egui_visuals(&palette);
let extra_fonts = std::mem::take(&mut config.extra_fonts);
plugins.sort_by_key(|p| p.priority());
let icon_data: Option<std::sync::Arc<egui::IconData>> =
if let Some(icon_bytes) = &config.icon {
match crate::icon::decode_icon(icon_bytes) {
Ok(data) => Some(std::sync::Arc::new(data)),
Err(e) => {
eprintln!("oxiui: failed to decode window icon: {e}");
None
}
}
} else {
None
};
let mut vp = egui::ViewportBuilder::default()
.with_title(&title)
.with_inner_size([width, height])
.with_resizable(config.resizable)
.with_decorations(config.decorations)
.with_transparent(config.transparent);
if config.always_on_top {
vp = vp.with_always_on_top();
}
if let Some((min_w, min_h)) = config.min_size {
vp = vp.with_min_inner_size([min_w, min_h]);
}
if let Some((max_w, max_h)) = config.max_size {
vp = vp.with_max_inner_size([max_w, max_h]);
}
if let Some((px, py)) = config.position {
vp = vp.with_position([px, py]);
}
if let Some(icon) = icon_data {
vp = vp.with_icon(icon);
}
let native_opts = NativeOptions {
viewport: vp,
..Default::default()
};
let LifecycleConfig {
on_close,
on_resize,
on_focus,
} = lifecycle;
eframe::run_native(
&title,
native_opts,
Box::new(move |cc| {
cc.egui_ctx.set_visuals(visuals.clone());
if !extra_fonts.is_empty() {
let refs: Vec<(&str, Vec<u8>)> = extra_fonts
.iter()
.map(|(n, b)| (n.as_str(), b.clone()))
.collect();
let _ = oxiui_egui::load_fonts_into_egui(&refs, &cc.egui_ctx);
}
Ok(Box::new(crate::egui_backend::OxiEguiApp {
content: Some(content),
on_init,
on_frame,
plugins,
initialised: false,
frame_skip,
egui_frame_hooks,
on_close,
on_resize,
on_focus,
tracker: LifecycleTracker::default(),
}))
}),
)
.map(|()| AppExit::Ok)
.map_err(|e| UiError::Backend(e.to_string()))
}
}
#[cfg(feature = "iced")]
pub struct IcedRunner {
pub(crate) theme: Box<dyn oxiui_core::Theme>,
pub(crate) on_init: Vec<HookFn>,
pub(crate) on_frame: Vec<HookFn>,
pub(crate) plugins: Vec<Box<dyn crate::Plugin>>,
}
#[cfg(feature = "iced")]
impl Default for IcedRunner {
fn default() -> Self {
Self {
theme: oxiui_theme::cooljapan_default(),
on_init: Vec::new(),
on_frame: Vec::new(),
plugins: Vec::new(),
}
}
}
#[cfg(feature = "iced")]
impl IcedRunner {
pub fn new() -> Self {
Self::default()
}
pub fn theme(mut self, theme: Box<dyn oxiui_core::Theme>) -> Self {
self.theme = theme;
self
}
}
#[cfg(feature = "iced")]
impl BackendRunner for IcedRunner {
fn run(
self: Box<Self>,
config: AppConfig,
content: ContentFn,
lifecycle: LifecycleConfig,
) -> Result<AppExit, UiError> {
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use oxiui_iced::palette_to_iced_theme;
let IcedRunner {
theme,
on_init,
on_frame,
mut plugins,
} = *self;
let iced_theme = palette_to_iced_theme(&theme.palette().clone());
plugins.sort_by_key(|p| p.priority());
let LifecycleConfig {
on_close,
on_resize,
on_focus,
} = lifecycle;
let state = crate::iced_backend::OxiIcedState {
title: config.title.clone(),
content: RefCell::new(Some(content)),
pending_clicks: RefCell::new(HashSet::new()),
widget_state: RefCell::new(HashMap::new()),
on_init: RefCell::new(on_init),
on_frame: RefCell::new(on_frame),
plugins: RefCell::new(plugins),
initialised: Cell::new(false),
on_close: RefCell::new(on_close),
on_resize: RefCell::new(on_resize),
on_focus: RefCell::new(on_focus),
tracker: RefCell::new(LifecycleTracker::default()),
};
crate::iced_backend::run(state, iced_theme, config.width, config.height)
.map(|()| AppExit::Ok)
.map_err(|e| UiError::Backend(e.to_string()))
}
}