use std::{ cell::RefCell, fs::File, io::Read, ops::Index, path::Path, rc::Rc, time::{Duration, Instant}, }; use cairo::Context; use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text}; use glib::{GString, Object}; use gtk::{ glib::{self, Propagation}, prelude::*, subclass::prelude::*, EventControllerKey, }; use serde::{Deserialize, Serialize}; const FPS: u64 = 60; const PURPLE: (f64, f64, f64) = (0.7, 0., 1.); #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "lowercase")] enum Position { Top, Middle, Bottom, } #[derive(Serialize, Deserialize, Debug, Clone)] struct Step { text: String, position: Position, transition: Duration, } #[derive(Serialize, Deserialize, Debug, Clone)] struct Script(Vec); impl Script { fn from_file(path: &Path) -> Result { let mut buf: Vec = Vec::new(); let mut f = File::open(path).unwrap(); f.read_to_end(&mut buf).unwrap(); let script = serde_yml::from_slice(&buf)?; Ok(Self(script)) } fn iter<'a>(&'a self) -> impl Iterator { self.0.iter() } fn len(&self) -> usize { self.0.len() } } impl Default for Script { fn default() -> Self { Self(vec![]) } } impl Index for Script { type Output = Step; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } struct Fade { text: String, position: Position, duration: Duration, start_time: Instant, } /* impl Fade { fn render(&self, context: &Context) { let start = Instant::now(); } } */ /* impl Transition { fn render(&self, context: &Context) { match self { Transition::Fade(fade) => fade.render(context), Transition::CrossFade(start, end) => {} } } } */ trait Animation { fn position(&self) -> Position; fn tick(&self, now: Instant, context: &Context) -> bool; } impl Animation for Fade { fn position(&self) -> Position { self.position.clone() } fn tick(&self, now: Instant, context: &Context) -> bool { let total_frames = self.duration.as_secs() * FPS; let alpha_rate: f64 = 1. / total_frames as f64; let frames = (now - self.start_time).as_secs_f64() * FPS as f64; let alpha = alpha_rate * frames as f64; let _ = context.set_source_rgba(PURPLE.0, PURPLE.1, PURPLE.2, alpha); let text_display = Text::new(self.text.clone(), context, 32.); text_display.draw(); false } } #[derive(Debug)] pub struct CyberScreenState { script: Script, idx: Option, top: Option, middle: Option, bottom: Option, } impl Default for CyberScreenState { fn default() -> Self { Self { script: Script(vec![]), idx: None, top: None, middle: None, bottom: None, } } } impl CyberScreenState { fn new(script: Script) -> CyberScreenState { let mut s = CyberScreenState::default(); s.script = script; s } fn next_page(&mut self) -> impl Animation { let idx = match self.idx { None => 0, Some(idx) => { if idx < self.script.len() { idx + 1 } else { idx } } }; self.idx = Some(idx); let step = self.script[idx].clone(); match step.position { Position::Top => { self.top = Some(step.clone()); } Position::Middle => { self.middle = Some(step.clone()); } Position::Bottom => { self.bottom = Some(step.clone()); } } Fade { text: step.text.clone(), position: step.position, duration: step.transition, start_time: Instant::now(), } } } #[derive(Default)] pub struct CyberScreenPrivate { state: Rc>, animations: Rc>>>, } #[glib::object_subclass] impl ObjectSubclass for CyberScreenPrivate { const NAME: &'static str = "CyberScreen"; type Type = CyberScreen; type ParentType = gtk::DrawingArea; } impl ObjectImpl for CyberScreenPrivate {} impl WidgetImpl for CyberScreenPrivate {} impl DrawingAreaImpl for CyberScreenPrivate {} impl CyberScreenPrivate { fn set_script(&self, script: Script) { *self.state.borrow_mut() = CyberScreenState::new(script); } fn next_page(&self) { let transition = self.state.borrow_mut().next_page(); self.animations.borrow_mut().push(Box::new(transition)); } } glib::wrapper! { pub struct CyberScreen(ObjectSubclass) @extends gtk::DrawingArea, gtk::Widget; } impl CyberScreen { pub fn new(script: Script) -> Self { let s: Self = Object::builder().build(); s.imp().set_script(script); s.set_draw_func({ let s = s.clone(); move |_, context, width, height| { let now = Instant::now(); let _ = context.set_source_rgb(0., 0., 0.); let _ = context.paint(); let pen = GlowPen::new(width, height, 2., 8., (0.7, 0., 1.)); /* for i in 0..6 { pen.move_to(0., height as f64 * i as f64 / 5.); pen.line_to(width as f64, height as f64 * i as f64 / 5.); } pen.stroke(); */ let tracery = pen.finish(); let _ = context.set_source(tracery); let _ = context.paint(); let mut animations = s.imp().animations.borrow_mut(); let mut to_remove = vec![]; for (idx, animation) in animations.iter().enumerate() { let y = match animation.position() { Position::Top => height as f64 * 1. / 5., Position::Middle => height as f64 * 2. / 5., Position::Bottom => height as f64 * 3. / 5., }; context.move_to(20., y); let done = animation.tick(now, context); if done { to_remove.push(idx) }; } for idx in to_remove.into_iter() { animations.remove(idx); } } }); s } fn next_page(&self) { println!("next page"); self.imp().next_page(); self.queue_draw(); } } fn main() { let script = Script(vec![Step { text: "The distinguishing thing".to_owned(), position: Position::Top, transition: Duration::from_secs(2), }]); println!("{}", serde_yml::to_string(&script).unwrap()); let script = Script::from_file(Path::new("./script.yml")).unwrap(); for element in script.iter() { println!("{:?}", element); } let app = gtk::Application::builder() .application_id("com.luminescent-dreams.cyberpunk-slideshow") .build(); app.connect_activate(move |app| { let screen = CyberScreen::new(script.clone()); let events = EventControllerKey::new(); events.connect_key_released({ let screen = screen.clone(); move |_, key, _, _| { if key.name() == Some(GString::from("Right")) { screen.next_page(); } } }); let window = gtk::ApplicationWindow::new(app); window.add_controller(events); window.set_child(Some(&screen)); window.set_width_request(800); window.set_height_request(600); window.present(); let _ = glib::spawn_future_local({ let screen = screen.clone(); async move { loop { screen.queue_draw(); async_std::task::sleep(Duration::from_millis(1000 / FPS)).await; }} }); }); app.run(); }