use std::{cell::RefCell, fs::File, io::Read, ops::Index, path::Path, rc::Rc, time::Duration}; 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 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)] enum TransitionName { Fade, CrossFade, } struct Fade(Step); enum Transition { Fade(Fade), CrossFade(Step, Step), } #[derive(Serialize, Deserialize, Debug, Clone)] struct Step { text: String, position: Position, transition: TransitionName, } #[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] } } #[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) -> Transition { 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 => { let transition = if let Some(old_step) = self.top.take() { Transition::CrossFade(old_step, step.clone()) } else { Transition::Fade(Fade(step.clone())) }; self.top = Some(step); transition } Position::Middle => { let transition = if let Some(old_step) = self.middle.take() { Transition::CrossFade(old_step, step.clone()) } else { Transition::Fade(Fade(step.clone())) }; self.middle = Some(step); transition } Position::Bottom => { let transition = if let Some(old_step) = self.bottom.take() { Transition::CrossFade(old_step, step.clone()) } else { Transition::Fade(Fade(step.clone())) }; self.bottom = Some(step); transition } } } } #[derive(Default)] pub struct CyberScreenPrivate { state: Rc>, transition_queue: 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.transition_queue.borrow_mut().push(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 _ = 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(); for transition in s.imp().transition_queue.borrow().iter() { if let Some(ref text) = s.imp().state.borrow().top { let text_display = Text::new(text.text.clone(), context, 32.); let y = height as f64 * 1. / 5. + text_display.extents().height(); context.move_to(20., y); let _ = context.set_source_rgb(PURPLE.0, PURPLE.1, PURPLE.2); text_display.draw(); } if let Some(ref text) = s.imp().state.borrow().middle { let text_display = Text::new(text.text.clone(), context, 32.); let y = height as f64 * 2. / 5. + text_display.extents().height(); context.move_to(20., y); let _ = context.set_source_rgb(PURPLE.0, PURPLE.1, PURPLE.2); text_display.draw(); } if let Some(ref text) = s.imp().state.borrow().bottom { let text_display = Text::new(text.text.clone(), context, 32.); let y = height as f64 * 3. / 5. + text_display.extents().height(); context.move_to(20., y); let _ = context.set_source_rgb(PURPLE.0, PURPLE.1, PURPLE.2); text_display.draw(); } } } }); s } fn next_page(&self) { println!("next page"); self.imp().next_page(); self.queue_draw(); } } fn main() { 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(); }); app.run(); }