#![cfg(feature = "gui")] use anyhow::{anyhow, Result}; use gtk4::{self as gtk, glib, cairo, gio}; use gtk4::prelude::*; use glib::{clone, spawn_future_local}; use std::thread; use std::sync::{Arc, Mutex}; use std::rc::Rc; use std::cell::Cell; use crate::sync::Signal; use std::future::poll_fn; use std::task::Poll; pub struct FbPool{ size: usize, pool: Vec, } impl FbPool{ pub fn new(size: usize) -> Self{ FbPool{ size, pool: Vec::with_capacity(size), } } pub fn put(&mut self, buf: cairo::ImageSurfaceDataOwned){ if self.pool.len() < self.size{ self.pool.push(buf); } } pub fn get(&mut self, w: usize, h: usize) -> Result{ while let Some(i) = self.pool.pop(){ let i = i.into_inner(); if i.width() as usize == w && i.height() as usize == h{ return Ok(i); } } Ok(cairo::ImageSurface::create( cairo::Format::Rgb24, w.try_into()?, h.try_into()?)?) } } pub trait FbSourceOnce{ fn get(self, w: usize, h: usize) -> Result; } impl FbSourceOnce for &Mutex{ fn get(self, w: usize, h: usize) -> Result{ self.lock().map_err(|e| anyhow!("{}", e))?.get(w, h) } } pub struct Packet{ pub image: cairo::ImageSurfaceDataOwned, pub attr: T, } pub trait Overlay: Send + 'static{ type Widget: glib::object::IsA; fn empty() -> Result; fn activate(_widget: &Self::Widget) -> Result<()>{ Ok(()) } fn update(&self, _widget: &Self::Widget) -> Result<()>{ Ok(()) } } pub trait Source: Send + 'static{ type Attr: Overlay; fn next(&mut self, fbpool: impl FbSourceOnce) -> Result>; } impl Overlay for (){ type Widget = gtk::Box; fn empty() -> Result{ Ok(gtk::Box::builder() .halign(gtk::Align::Start) .valign(gtk::Align::Start) .visible(false) .build()) } } struct AppState{ next: Mutex>>, update: Mutex, abort: Mutex, fbpool: Mutex, } fn sourcing_loop( apps: &AppState, src: &mut impl Source ) -> Result<()>{ loop{ let p = src.next(&apps.fbpool)?; let old = apps.next.lock() .map_err(|e| anyhow!("{}", e))? .replace(p); apps.update.lock().map_err(|e| anyhow!("{}", e))?.wake(); if let Some(old) = old{ apps.fbpool.lock() .map_err(|e| anyhow!("{}", e))? .put(old.image); } } } fn activate(app: >k::Application, apps: Arc>){ let draw = gtk::DrawingArea::new(); let overlay = Attr::empty().unwrap(); let frame_cache: Rc>> = Rc::new(Cell::new(None)); let attr_cache: Rc>> = Rc::new(Cell::new(None)); draw.set_draw_func(clone!{ #[strong] frame_cache, move |_draw, ctx, canvas_w, canvas_h|{ ctx.set_source_rgb(0., 0., 0.); ctx.paint().unwrap(); if let Some(image) = frame_cache.take(){ let ipat = cairo::SurfacePattern::create(&image); let scale = ((canvas_w as f64) / (image.width() as f64)).min( (canvas_h as f64) / (image.height() as f64)); ctx.scale(scale, scale); ctx.set_source(&ipat).unwrap(); ctx.paint().unwrap(); frame_cache.replace(Some(image)); } } }); overlay.add_tick_callback(clone!{ #[strong] attr_cache, move |overlay, _clock|{ if let Some(attr) = attr_cache.take(){ attr.update(overlay).unwrap(); attr_cache.replace(Some(attr)); } glib::ControlFlow::Continue } }); spawn_future_local(poll_fn(clone!{ #[strong] apps, #[strong] draw, #[strong] frame_cache, #[strong] attr_cache, #[strong] overlay, move |ctx|{ loop{ match apps.update.lock().unwrap().poll(ctx){ Poll::Ready(_) => { if let Some(newfb) = apps.next.lock().unwrap().take(){ if let Some(lastframe) = frame_cache.take(){ apps.fbpool.lock().unwrap() .put(lastframe.take_data().unwrap()); } frame_cache.replace(Some(newfb.image.into_inner())); newfb.attr.update(&overlay).unwrap(); attr_cache.replace(Some(newfb.attr)); } draw.queue_draw(); }, pending => return pending, } } } })); spawn_future_local(poll_fn(clone!{ #[strong] apps, #[strong] app, move |ctx|{ loop{ match apps.abort.lock().unwrap().poll(ctx){ Poll::Ready(_) => { app.quit(); }, pending => return pending, } } } })); let olcontainer = gtk::Overlay::builder() .child(&draw) .build(); olcontainer.add_overlay(&overlay); let win = gtk::ApplicationWindow::builder() .application(app) .child(&olcontainer) .build(); win.present(); Attr::activate(&overlay).unwrap(); } pub fn main(src: impl Source + 'static) -> Result{ let apps = Arc::new(AppState{ next: Mutex::new(None), update: Mutex::new(Signal::new()), abort: Mutex::new(Signal::new()), fbpool: Mutex::new(FbPool::new(4)), }); thread::spawn(clone!{ #[strong] apps, move ||{ let mut src = src; let res = sourcing_loop(&apps, &mut src); apps.abort.lock().unwrap().wake(); res.unwrap(); } }); let app = gtk::Application::builder() .flags(gio::ApplicationFlags::HANDLES_COMMAND_LINE) .build(); app.connect_command_line(clone!{ #[strong] apps, move |app, _| { activate(app, apps.clone()); 0 } }); Ok(app.run()) }