use crate::{
error::{FrameworkError, Severity},
sub::Sub,
App, Cmd, ViewCtx,
};
use std::collections::{HashMap, HashSet};
use std::sync::mpsc;
use std::time::Duration;
use tokio::runtime::Runtime as TokioRuntime;
use tokio::task::JoinHandle;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum RepaintMode {
#[default]
Reactive,
FixedFps(u32),
VSync,
}
pub struct RunConfig {
pub title: String,
pub initial_size: Option<[f32; 2]>,
pub persistence: bool,
pub repaint_mode: RepaintMode,
}
impl Default for RunConfig {
fn default() -> Self {
Self {
title: "egui-cha App".to_string(),
initial_size: Some([800.0, 600.0]),
persistence: false,
repaint_mode: RepaintMode::default(),
}
}
}
impl RunConfig {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
..Default::default()
}
}
pub fn with_size(mut self, width: f32, height: f32) -> Self {
self.initial_size = Some([width, height]);
self
}
pub fn with_persistence(mut self) -> Self {
self.persistence = true;
self
}
pub fn with_repaint_mode(mut self, mode: RepaintMode) -> Self {
self.repaint_mode = mode;
self
}
}
pub fn run<A: App>(config: RunConfig) -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size(config.initial_size.unwrap_or([800.0, 600.0])),
..Default::default()
};
let repaint_mode = config.repaint_mode;
eframe::run_native(
&config.title,
options,
Box::new(move |cc| Ok(Box::new(TeaRuntime::<A>::new(cc, repaint_mode)))),
)
}
struct TeaRuntime<A: App> {
model: A::Model,
pending_msgs: Vec<A::Msg>,
msg_receiver: mpsc::Receiver<A::Msg>,
msg_sender: mpsc::Sender<A::Msg>,
err_receiver: mpsc::Receiver<FrameworkError>,
err_sender: mpsc::Sender<FrameworkError>,
tokio_runtime: TokioRuntime,
active_intervals: HashMap<&'static str, IntervalHandle>,
repaint_mode: RepaintMode,
}
struct IntervalHandle {
handle: JoinHandle<()>,
}
const PHOSPHOR_FONT: &[u8] = include_bytes!("../assets/fonts/Phosphor.ttf");
pub fn setup_icon_fonts(ctx: &egui::Context) {
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"phosphor".to_owned(),
egui::FontData::from_static(PHOSPHOR_FONT).into(),
);
fonts.families.insert(
egui::FontFamily::Name("icons".into()),
vec!["phosphor".to_owned()],
);
ctx.set_fonts(fonts);
}
impl<A: App> TeaRuntime<A> {
fn new(cc: &eframe::CreationContext<'_>, repaint_mode: RepaintMode) -> Self {
setup_icon_fonts(&cc.egui_ctx);
let (model, init_cmd) = A::init();
let (msg_sender, msg_receiver) = mpsc::channel();
let (err_sender, err_receiver) = mpsc::channel();
let tokio_runtime = TokioRuntime::new().expect("Failed to create tokio runtime");
let runtime = Self {
model,
pending_msgs: Vec::new(),
msg_receiver,
msg_sender,
err_receiver,
err_sender,
tokio_runtime,
active_intervals: HashMap::new(),
repaint_mode,
};
runtime.execute_cmd(init_cmd);
runtime
}
fn execute_cmd(&self, cmd: Cmd<A::Msg>) {
match cmd {
Cmd::None => {}
Cmd::Batch(cmds) => {
for c in cmds {
self.execute_cmd(c);
}
}
Cmd::Task(future) => {
let msg_sender = self.msg_sender.clone();
let err_sender = self.err_sender.clone();
self.tokio_runtime.spawn(async move {
let result = tokio::task::spawn(future).await;
match result {
Ok(msg) => {
let _ = msg_sender.send(msg);
}
Err(join_error) => {
let err = if join_error.is_panic() {
FrameworkError::command(
Severity::Error,
format!("Task panicked: {}", join_error),
)
} else {
FrameworkError::command(
Severity::Warn,
"Task was cancelled".to_string(),
)
};
let _ = err_sender.send(err);
}
}
});
}
Cmd::Msg(msg) => {
let _ = self.msg_sender.send(msg);
}
}
}
fn process_pending_messages(&mut self) {
while let Ok(msg) = self.msg_receiver.try_recv() {
self.pending_msgs.push(msg);
}
let msgs = std::mem::take(&mut self.pending_msgs);
for msg in msgs {
let cmd = A::update(&mut self.model, msg);
self.execute_cmd(cmd);
}
}
fn process_framework_errors(&mut self) {
while let Ok(err) = self.err_receiver.try_recv() {
let cmd = A::on_framework_error(&mut self.model, err);
self.execute_cmd(cmd);
}
}
fn process_subscriptions(&mut self, sub: Sub<A::Msg>) {
let mut current_ids = HashSet::new();
sub.collect_interval_ids(&mut current_ids);
let to_stop: Vec<_> = self
.active_intervals
.keys()
.filter(|id| !current_ids.contains(*id))
.copied()
.collect();
for id in to_stop {
self.stop_interval(id);
}
for (id, duration, msg) in sub.intervals() {
if !self.active_intervals.contains_key(id) {
self.start_interval(id, duration, msg);
}
}
}
fn start_interval(&mut self, id: &'static str, duration: Duration, msg: A::Msg) {
let sender = self.msg_sender.clone();
let handle = self.tokio_runtime.spawn(async move {
let mut interval = tokio::time::interval(duration);
interval.tick().await;
loop {
interval.tick().await;
if sender.send(msg.clone()).is_err() {
break; }
}
});
self.active_intervals.insert(id, IntervalHandle { handle });
}
fn stop_interval(&mut self, id: &'static str) {
if let Some(handle) = self.active_intervals.remove(id) {
handle.handle.abort();
}
}
}
impl<A: App> eframe::App for TeaRuntime<A> {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.process_pending_messages();
self.process_framework_errors();
let sub = A::subscriptions(&self.model);
self.process_subscriptions(sub);
let mut view_msgs = Vec::new();
egui::CentralPanel::default().show(ctx, |ui| {
let mut view_ctx = ViewCtx::new(ui, &mut view_msgs);
A::view(&self.model, &mut view_ctx);
});
self.pending_msgs.extend(view_msgs);
match self.repaint_mode {
RepaintMode::Reactive => {
if !self.pending_msgs.is_empty() || !self.active_intervals.is_empty() {
ctx.request_repaint();
}
}
RepaintMode::FixedFps(fps) => {
let interval = Duration::from_secs_f64(1.0 / fps as f64);
ctx.request_repaint_after(interval);
}
RepaintMode::VSync => {
ctx.request_repaint();
}
}
}
}