use std::sync::Arc;
use egui_winit::winit::event_loop::EventLoop;
use crate::prelude::*;
pub mod about_dialog;
pub(crate) mod error_dialog;
pub struct Sender<M> {
f: Arc<dyn Fn(M) + Send + Sync>,
}
impl<M> Clone for Sender<M> {
fn clone(&self) -> Self {
Self { f: self.f.clone() }
}
}
impl<M: 'static> Sender<M> {
fn new(f: impl Fn(M) + Send + Sync + 'static) -> Self {
Self { f: Arc::new(f) }
}
pub fn emit(&self, msg: M) {
(self.f)(msg)
}
pub fn map<N: 'static>(&self, f: impl Fn(N) -> M + Send + Sync + 'static) -> Sender<N> {
let inner = self.clone();
Sender::new(move |n| inner.emit(f(n)))
}
}
pub struct Context<W: Widget> {
input: Sender<W::Message>,
error: Sender<W::Error>,
output: Option<Sender<W::Output>>,
}
impl<W: Widget> Clone for Context<W> {
fn clone(&self) -> Self {
Self {
error: self.error.clone(),
input: self.input.clone(),
output: self.output.clone(),
}
}
}
impl<W: Widget> std::fmt::Debug for Context<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"Context<Message = {}, Output = {}, Error = {}>",
std::any::type_name::<W::Message>(),
std::any::type_name::<W::Output>(),
std::any::type_name::<W::Error>()
))
}
}
impl<W: Widget + 'static> Context<W> {
pub fn spawn<F, Fut>(&self, f: F)
where
F: FnOnce(Context<W>) -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let ctx = self.clone();
tokio::spawn(async move {
f(ctx).await;
});
}
pub fn emit(&self, msg: W::Message) {
self.input.emit(msg);
}
pub fn output(&self, msg: W::Output) {
if let Some(output) = &self.output {
output.emit(msg);
} else {
tracing::warn!("`output` channel does not exist");
}
}
pub fn error(&self, err: W::Error) {
self.error.emit(err);
}
pub fn error_sender(&self) -> Sender<W::Error> {
self.error.clone()
}
pub fn input_sender(&self) -> Sender<W::Message> {
self.input.clone()
}
}
pub trait Widget: TickChildren + std::fmt::Debug + Sized {
type Message: Send + std::fmt::Debug + 'static;
type Output: Send + 'static;
type Error: std::fmt::Debug + Send + Sync + 'static;
fn view(&mut self, ui: &mut egui::Ui, frame: &mut Frame, ctx: &Context<Self>);
#[expect(unused_variables)]
fn update(&mut self, msg: Self::Message, handle: &Handle, ctx: &Context<Self>) -> Result<(), Self::Error> {
Ok(())
}
#[expect(unused_variables)]
fn tick(&mut self, ctx: &Context<Self>) -> Result<(), Self::Error> {
Ok(())
}
}
pub trait LeafWidget: std::fmt::Debug {
fn render(&mut self, ui: &mut egui::Ui, frame: &mut Frame);
}
impl<T: LeafWidget> TickChildren for T {}
impl<T: LeafWidget> Widget for T {
type Message = ();
type Output = ();
type Error = ();
#[inline(always)]
fn view(&mut self, ui: &mut egui::Ui, frame: &mut Frame, _ctx: &Context<Self>) {
self.render(ui, frame);
}
#[inline(always)]
fn update(&mut self, _msg: (), _handle: &Handle, _ctx: &Context<Self>) -> Result<(), ()> {
Ok(())
}
#[inline(always)]
fn tick(&mut self, _ctx: &Context<Self>) -> Result<(), Self::Error> {
Ok(())
}
}
pub trait RootWidget: Widget + 'static {
fn close(&mut self, frame: &mut Frame) {
frame.exit();
}
#[expect(unused_variables)]
fn error(&mut self, err: &Self::Error) -> (String, Option<String>) {
(
"Unknown error".to_string(),
Some("Error translating is not implemented.\nImplement RootWidget::error in order to view the actual error that happened.".to_string()),
)
}
#[expect(unused_variables)]
fn setup(&mut self, ctx: &egui::Context) {}
fn clear_color(&self) -> [u8; 4] {
[27, 27, 27, 255]
}
}
#[derive(Debug)]
pub struct Managed<T: Widget> {
widget: T,
rx: crossbeam_channel::Receiver<T::Message>,
ctx: Context<T>,
pub(crate) handle: Handle,
}
impl<T: Widget> std::ops::Deref for Managed<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.widget
}
}
impl<T: Widget> std::ops::DerefMut for Managed<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.widget
}
}
impl<T: Widget + 'static> Managed<T> {
pub fn new(output: impl Into<Option<Sender<T::Output>>>, error: Sender<T::Error>, handle: &Handle, widget: T) -> Self {
let (tx, rx) = crossbeam_channel::unbounded();
let wake = handle.clone();
Self {
widget,
rx,
ctx: Context {
input: Sender::new(move |msg| {
if let Err(e) = tx.send(msg).map_err(|_| Error::SendingOverChannel) {
tracing::error!("{e}");
}
wake.request_repaint();
}),
output: output.into(),
error,
},
handle: handle.clone(),
}
}
pub fn render(&mut self, ui: &mut egui::Ui, frame: &mut Frame) {
self.widget.view(ui, frame, &self.ctx);
}
pub fn update_route_error(&mut self) {
while let Ok(msg) = self.rx.try_recv() {
if let Err(e) = self.widget.update(msg, &self.handle, &self.ctx) {
self.ctx.error(e);
}
}
self.widget.tick_children_auto();
if let Err(e) = self.widget.tick(&self.ctx) {
self.ctx.error(e);
}
}
pub fn update(&mut self) -> Result<(), T::Error> {
while let Ok(msg) = self.rx.try_recv() {
self.widget.update(msg, &self.handle, &self.ctx)?;
}
self.widget.tick_children_auto();
self.widget.tick(&self.ctx)
}
}
pub trait TickChildren {
fn tick_children_auto(&mut self) {}
}
pub struct App<T: RootWidget> {
root: Managed<T>,
ctrlc_handler: bool,
error_rx: crossbeam_channel::Receiver<T::Error>,
}
impl<T: RootWidget> App<T> {
pub fn new(root: T) -> Self {
App::new_factory(move |_, _| root)
}
pub fn new_factory<F: FnOnce(&Context<T>, &Handle) -> T>(factory: F) -> Self {
let (error_tx, error_rx) = crossbeam_channel::unbounded::<T::Error>();
let handle = Handle::new();
let (tx, rx) = crossbeam_channel::unbounded();
let ctx = Context {
input: Sender::new({
let handle = handle.clone();
move |msg| {
if let Err(e) = tx.send(msg).map_err(|_| Error::SendingOverChannel) {
tracing::error!("{e}");
}
handle.request_repaint();
}
}),
output: None,
error: Sender::new({
let handle = handle.clone();
move |err| {
if let Err(e) = error_tx.send(err).map_err(|_| Error::SendingOverChannel) {
tracing::error!("{e}");
}
handle.request_repaint();
}
}),
};
Self {
root: Managed {
widget: factory(&ctx, &handle),
rx,
ctx,
handle,
},
ctrlc_handler: true,
error_rx,
}
}
pub fn without_ctrl_handler(mut self) -> Self {
self.ctrlc_handler = false;
self
}
#[cfg(all(feature = "android", target_os = "android"))]
#[tracing::instrument(skip(self, options))]
pub fn run_android(self, android_app: AndroidApp, options: ViewportBuilder) -> Result<(), Error> {
use egui_winit::winit::platform::android::EventLoopBuilderExtAndroid;
let event_loop = EventLoop::<crate::native::UserEvent>::with_user_event()
.with_android_app(android_app)
.build()
.map_err(Error::EventLoopBuildFail)?;
self.run_with_event_loop(event_loop, crate::native::Renderer::Wgpu, options)
}
#[tracing::instrument(skip(self, options))]
pub fn run(self, options: ViewportBuilder) -> Result<(), Error> {
#[cfg(feature = "glow")]
return self.run_with_backend(crate::native::Renderer::Glow, options);
#[cfg(all(not(feature = "glow"), feature = "wgpu"))]
return self.run_with_backend(crate::native::Renderer::Wgpu, options);
}
#[tracing::instrument(skip(self, options))]
pub fn run_with_backend(self, renderer: crate::native::Renderer, options: ViewportBuilder) -> Result<(), Error> {
let event_loop = EventLoop::<crate::native::UserEvent>::with_user_event()
.build()
.map_err(Error::EventLoopBuildFail)?;
self.run_with_event_loop(event_loop, renderer, options)
}
#[tracing::instrument(skip(self, event_loop, options))]
fn run_with_event_loop(self, event_loop: EventLoop<crate::native::UserEvent>, renderer: crate::native::Renderer, options: ViewportBuilder) -> Result<(), Error> {
let proxy = event_loop.create_proxy();
self.root.handle.init(proxy.clone());
let mut runner: Box<dyn crate::native::Runner> = match renderer {
#[cfg(feature = "glow")]
crate::native::Renderer::Glow => {
tracing::info!("using glow renderer");
Box::new(crate::native::_glow::GlowRunner::new(self.root, self.error_rx, options, proxy.clone()))
}
#[cfg(feature = "wgpu")]
crate::native::Renderer::Wgpu => {
tracing::info!("using wgpu renderer");
Box::new(crate::native::_wgpu::WgpuRunner::new(self.root, self.error_rx, options, proxy.clone()))
}
#[cfg(not(feature = "glow"))]
crate::native::Renderer::Glow => return Err(Error::RendererUnavailable("glow")),
#[cfg(not(feature = "wgpu"))]
crate::native::Renderer::Wgpu => return Err(Error::RendererUnavailable("wgpu")),
};
#[cfg(all(feature = "ctrlc", not(target_os = "android")))]
if self.ctrlc_handler {
ctrlc::set_handler(move || {
println!();
if let Err(e) = proxy
.send_event(crate::native::UserEvent::Exit)
.map_err(|_| Error::SendingOverChannel)
{
tracing::error!("{e}");
}
})
.map_err(Error::SetSigHandler)?;
}
event_loop
.run_app(&mut runner)
.map_err(Error::EventLoopFail)
}
}
#[doc(hidden)]
pub fn run_test<F: Fn(&mut egui::Ui)>(func: F) -> Result<(), Error> {
let ctx = egui::Context::default();
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
func(ui);
});
assert!(!output.textures_delta.set.is_empty());
assert!(!output.shapes.is_empty());
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
#[derive(Debug)]
struct TestWidget {
updates: Vec<u32>,
ticks: usize,
output_on_update: bool,
fail_update: bool,
fail_tick: bool,
}
impl TickChildren for TestWidget {}
impl Widget for TestWidget {
type Message = u32;
type Output = String;
type Error = &'static str;
fn view(&mut self, _ui: &mut egui::Ui, _frame: &mut Frame, _ctx: &Context<Self>) {}
fn update(&mut self, msg: Self::Message, _handle: &Handle, ctx: &Context<Self>) -> Result<(), Self::Error> {
self.updates.push(msg);
if self.output_on_update {
ctx.output(format!("updated {msg}"));
}
if self.fail_update {
return Err("update failed");
}
Ok(())
}
fn tick(&mut self, _ctx: &Context<Self>) -> Result<(), Self::Error> {
self.ticks += 1;
if self.fail_tick {
return Err("tick failed");
}
Ok(())
}
}
fn sender<T: Send + 'static>() -> (Sender<T>, crossbeam_channel::Receiver<T>) {
let (tx, rx) = crossbeam_channel::unbounded();
(Sender::new(move |value| tx.send(value).unwrap()), rx)
}
fn managed(widget: TestWidget) -> (Managed<TestWidget>, crossbeam_channel::Receiver<String>, crossbeam_channel::Receiver<&'static str>) {
let (output, output_rx) = sender();
let (error, error_rx) = sender();
(Managed::new(output, error, &Handle::default(), widget), output_rx, error_rx)
}
fn test_widget() -> TestWidget {
TestWidget {
updates: Vec::new(),
ticks: 0,
output_on_update: false,
fail_update: false,
fail_tick: false,
}
}
#[test]
fn sender_map_transforms_and_forwards_values() {
let values = Arc::new(Mutex::new(Vec::new()));
let received = values.clone();
let sender = Sender::new(move |value: usize| received.lock().unwrap().push(value));
let mapped = sender.map(|value: &str| value.len());
mapped.emit("egelm");
assert_eq!(*values.lock().unwrap(), vec![5]);
}
#[test]
fn managed_update_drains_messages_in_order_and_ticks_once() {
let (mut managed, _output_rx, _error_rx) = managed(test_widget());
let input = managed.ctx.input_sender();
input.emit(3);
input.emit(1);
input.emit(4);
managed.update().unwrap();
assert_eq!(managed.updates, [3, 1, 4]);
assert_eq!(managed.ticks, 1);
}
#[test]
fn context_forwards_outputs_and_errors() {
let mut widget = test_widget();
widget.output_on_update = true;
let (mut managed, output_rx, error_rx) = managed(widget);
managed.ctx.input_sender().emit(7);
managed.ctx.error("reported directly");
managed.update().unwrap();
assert_eq!(output_rx.try_recv(), Ok("updated 7".to_owned()));
assert_eq!(error_rx.try_recv(), Ok("reported directly"));
}
#[test]
fn update_route_error_routes_update_errors_and_continues_to_tick() {
let mut widget = test_widget();
widget.fail_update = true;
let (mut managed, _output_rx, error_rx) = managed(widget);
let input = managed.ctx.input_sender();
input.emit(10);
input.emit(20);
managed.update_route_error();
assert_eq!(managed.updates, [10, 20]);
assert_eq!(managed.ticks, 1);
assert_eq!(error_rx.try_iter().collect::<Vec<_>>(), ["update failed", "update failed"]);
}
#[test]
fn update_route_error_routes_tick_errors() {
let mut widget = test_widget();
widget.fail_tick = true;
let (mut managed, _output_rx, error_rx) = managed(widget);
managed.update_route_error();
assert_eq!(managed.ticks, 1);
assert_eq!(error_rx.try_recv(), Ok("tick failed"));
}
}