use cairo::{ Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern, TextExtents, }; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{ cell::RefCell, rc::Rc, time::{Duration, Instant}, }; const WIDTH: i32 = 1600; const HEIGHT: i32 = 600; #[derive(Clone, Copy, Debug)] enum Event { Frames(u8), Time(Duration), } struct TimeoutAnimation { intensity: f64, duration: f64, ascending: bool, } impl TimeoutAnimation { fn tick(&mut self, frames_elapsed: u8) { let step_size = 1. / (self.duration * 60.); if self.ascending { self.intensity = self.intensity + step_size * frames_elapsed as f64; if self.intensity > 1. { self.intensity = 1.0; self.ascending = false; } } else { self.intensity = self.intensity - step_size * frames_elapsed as f64; if self.intensity < 0. { self.intensity = 0.0; self.ascending = true; } } } } pub struct SplashPrivate { text: Rc>, time: Rc>, background: Rc>, time_extents: Rc>>, width: Rc>, height: Rc>, timeout_animation: Rc>>, } impl SplashPrivate { fn set_text(&self, text: String) { *self.text.borrow_mut() = text; } fn set_time(&self, time: Duration) { *self.time.borrow_mut() = time; } fn redraw_background(&self) { let background = ImageSurface::create(Format::Rgb24, *self.width.borrow(), *self.height.borrow()) .unwrap(); let context = Context::new(background).unwrap(); context.push_group(); context.set_source_rgb(0., 0., 0.); let _ = context.paint(); context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold); context.set_font_size(128.); let center_x = *self.width.borrow() as f64 / 2.; let center_y = *self.height.borrow() as f64 / 2.; let title_extents = context.text_extents(&self.text.borrow()).unwrap(); let title_width = title_extents.width(); let title_height = title_extents.height(); { let start_length = center_x - title_width / 2. - title_height - 20.; let title_cutout = AsymLineCutout { orientation: gtk::Orientation::Horizontal, start_x: 20., start_y: center_y - 20. - title_height / 2., start_length, total_length: *self.width.borrow() as f64 - 120., cutout_length: title_width, height: title_height, invert: false, }; context.set_line_cap(LineCap::Round); context.set_source_rgb(0.7, 0., 1.); context.set_line_width(2.); title_cutout.draw(&context); let _ = context.stroke(); } { let title_baseline_x = center_x - title_width / 2.; let title_baseline_y = center_y - 20.; let gradient = LinearGradient::new( title_baseline_x, title_baseline_y - title_height, title_baseline_x, title_baseline_y, ); gradient.add_color_stop_rgb(0.2, 0.7, 0.0, 1.0); gradient.add_color_stop_rgb(0.8, 0.2, 0.0, 1.0); context.move_to(title_baseline_x, title_baseline_y); let _ = context.set_source(gradient); let _ = context.show_text(&self.text.borrow()); } { context.set_source_rgb(0.7, 0., 1.); AsymLine { orientation: gtk::Orientation::Horizontal, start_x: 100., start_y: *self.height.borrow() as f64 / 2. + 100., start_length: 400., height: 50., total_length: 650., invert: true, } .draw(&context); let _ = context.stroke(); } { context.set_source_rgb(0.7, 0., 1.); AsymLine { orientation: gtk::Orientation::Horizontal, start_x: *self.width.borrow() as f64 / 2. + 100., start_y: *self.height.borrow() as f64 / 2. + 200., start_length: 600., height: 50., total_length: 650., invert: false, } .draw(&context); let _ = context.stroke(); } let background = context.pop_group().unwrap(); *self.background.borrow_mut() = background; } } #[glib::object_subclass] impl ObjectSubclass for SplashPrivate { const NAME: &'static str = "Splash"; type Type = Splash; type ParentType = gtk::DrawingArea; fn new() -> SplashPrivate { // Set up a default plain black background let background = ImageSurface::create(Format::Rgb24, WIDTH, HEIGHT).unwrap(); let context = Context::new(background).unwrap(); context.push_group(); context.set_source_rgb(0., 0., 0.); let _ = context.paint(); let background = context.pop_group().unwrap(); SplashPrivate { text: Rc::new(RefCell::new(String::from(""))), time: Rc::new(RefCell::new(Duration::ZERO)), background: Rc::new(RefCell::new(background)), time_extents: Rc::new(RefCell::new(None)), width: Rc::new(RefCell::new(WIDTH)), height: Rc::new(RefCell::new(HEIGHT)), timeout_animation: Rc::new(RefCell::new(None)), } } } impl ObjectImpl for SplashPrivate {} impl WidgetImpl for SplashPrivate {} impl DrawingAreaImpl for SplashPrivate {} glib::wrapper! { pub struct Splash(ObjectSubclass) @extends gtk::DrawingArea, gtk::Widget; } impl Splash { pub fn new(text: String, time: Duration) -> Self { let s: Self = Object::builder().build(); s.set_width_request(WIDTH); s.set_height_request(HEIGHT); s.imp().set_text(text); s.imp().set_time(time); s.imp().redraw_background(); s.set_draw_func({ let s = s.clone(); move |_, context, width, height| { let background = s.imp().background.borrow(); let _ = context.set_source(&*background); let _ = context.paint(); let time = s.imp().time.borrow().clone(); let minutes = time.as_secs() / 60; let seconds = time.as_secs() % 60; let center_x = width as f64 / 2.; let center_y = height as f64 / 2.; {} { context.select_font_face( "Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold, ); context.set_font_size(128.); let time = format!("{:02}' {:02}\"", minutes, seconds); let time_extents = context.text_extents(&time).unwrap(); let mut saved_extents = s.imp().time_extents.borrow_mut(); if saved_extents.is_none() { *saved_extents = Some(time_extents.clone()); } let time_baseline_x = center_x - time_extents.width() / 2.; let time_baseline_y = center_y + 100.; /* match *s.imp().timeout_animation.borrow() { Some(ref animation) => { context.set_source_rgb(animation.intensity / 2., 0., 0.); RoundedRectangle { x: time_baseline_x - 5., y: time_baseline_y + 10., width: time_extents.width() + 15., height: time_extents.height() + 15., } .draw(&context); // let _ = context.fill(); } None => {} } */ let gradient = LinearGradient::new( time_baseline_x, time_baseline_y - time_extents.height(), time_baseline_x, time_baseline_y, ); match *s.imp().timeout_animation.borrow() { Some(ref animation) => { gradient.add_color_stop_rgba(0.2, 0.2, 0.0, 1.0, animation.intensity); gradient.add_color_stop_rgba(0.8, 0.7, 0.0, 1.0, animation.intensity); let _ = context.set_source(gradient); } None => { gradient.add_color_stop_rgb(0.2, 0.2, 0.0, 1.0); gradient.add_color_stop_rgb(0.8, 0.7, 0.0, 1.0); let _ = context.set_source(gradient); } } context.move_to(time_baseline_x, time_baseline_y); let _ = context.show_text(&time); }; match *s.imp().time_extents.borrow() { Some(extents) => { context.set_source_rgb(0.7, 0.0, 1.0); let time_meter = SlashMeter { orientation: gtk::Orientation::Horizontal, start_x: center_x + extents.width() / 2. + 50., start_y: center_y + 100., count: 5, fill_count: minutes as u8, height: 60., length: 100., }; time_meter.draw(&context); } None => {} } } }); s.connect_resize(|s, width, height| { *s.imp().width.borrow_mut() = width; *s.imp().height.borrow_mut() = height; s.imp().redraw_background(); }); s } pub fn set_time(&self, time: Duration) { self.imp().set_time(time); if time == Duration::ZERO { *self.imp().timeout_animation.borrow_mut() = Some(TimeoutAnimation { intensity: 1., duration: 1., ascending: false, }); } self.queue_draw(); } pub fn tick(&self, frames_elapsed: u8) { let mut animation = self.imp().timeout_animation.borrow_mut(); match *animation { Some(ref mut animation) => { animation.tick(frames_elapsed); self.queue_draw(); } None => {} } } } struct AsymLineCutout { orientation: gtk::Orientation, start_x: f64, start_y: f64, start_length: f64, total_length: f64, cutout_length: f64, height: f64, invert: bool, } impl AsymLineCutout { fn draw(&self, context: &Context) { let dodge = if self.invert { self.height } else { -self.height }; match self.orientation { gtk::Orientation::Horizontal => { context.move_to(self.start_x, self.start_y); context.line_to(self.start_x + self.start_length, self.start_y); context.line_to( self.start_x + self.start_length + self.height, self.start_y + dodge, ); context.line_to( self.start_x + self.start_length + self.height + self.cutout_length, self.start_y + dodge, ); context.line_to( self.start_x + self.start_length + self.height + self.cutout_length + (self.height / 2.), self.start_y + dodge / 2., ); context.line_to(self.total_length, self.start_y + dodge / 2.); } gtk::Orientation::Vertical => { context.move_to(self.start_x, self.start_y); context.line_to(self.start_x, self.start_y + self.start_length); context.line_to( self.start_x + dodge, self.start_y + self.start_length + self.height, ); context.line_to( self.start_x + dodge, self.start_y + self.start_length + self.height + self.cutout_length, ); context.line_to( self.start_x + dodge / 2., self.start_y + self.start_length + self.height + self.cutout_length + (self.height / 2.), ); context.line_to(self.start_x + dodge / 2., self.total_length); } _ => panic!("unknown orientation"), } } } struct AsymLine { orientation: gtk::Orientation, start_x: f64, start_y: f64, start_length: f64, height: f64, total_length: f64, invert: bool, } impl AsymLine { fn draw(&self, context: &Context) { let dodge = if self.invert { self.height } else { -self.height }; match self.orientation { gtk::Orientation::Horizontal => { context.move_to(self.start_x, self.start_y); context.line_to(self.start_x + self.start_length, self.start_y); context.line_to( self.start_x + self.start_length + self.height, self.start_y + dodge, ); context.line_to(self.start_x + self.total_length, self.start_y + dodge); } gtk::Orientation::Vertical => {} _ => panic!("unknown orientation"), } } } struct RoundedRectangle { x: f64, y: f64, width: f64, height: f64, } impl RoundedRectangle { fn draw(&self, context: &Context) { context.arc( self.x, self.y - self.height / 2., self.height / 2., 0.5 * std::f64::consts::PI, 1.5 * std::f64::consts::PI, ); let _ = context.fill(); context.arc( self.x + self.width, self.y - self.height / 2., self.height / 2., 1.5 * std::f64::consts::PI, 0.5 * std::f64::consts::PI, ); let _ = context.fill(); context.rectangle(self.x, self.y, self.width, -self.height); let _ = context.fill(); } } struct SlashMeter { orientation: gtk::Orientation, start_x: f64, start_y: f64, count: u8, fill_count: u8, height: f64, length: f64, } impl SlashMeter { fn draw(&self, context: &Context) { match self.orientation { gtk::Orientation::Horizontal => { let angle: f64 = 0.8; let run = self.height / angle.tan(); let width = self.length as f64 / (self.count as f64 * 2.); for c in 0..self.count { context.set_line_width(1.); let start_x = self.start_x + c as f64 * width * 2.; context.move_to(start_x, self.start_y); context.line_to(start_x + run, self.start_y - self.height); context.line_to(start_x + run + width, self.start_y - self.height); context.line_to(start_x + width, self.start_y); context.line_to(start_x, self.start_y); if c < self.fill_count { let _ = context.fill(); } else { let _ = context.stroke(); } } } gtk::Orientation::Vertical => {} _ => panic!("unknown orientation"), } } } fn main() { let app = gtk::Application::builder() .application_id("com.luminescent-dreams.cyberpunk-splash") .build(); app.connect_activate(move |app| { let (gtk_tx, gtk_rx) = gtk::glib::MainContext::channel::(gtk::glib::PRIORITY_DEFAULT); let window = gtk::ApplicationWindow::new(app); window.present(); let mut countdown = Duration::from_secs(5); let mut next_tick = Instant::now() + Duration::from_secs(1); let splash = Splash::new("GTK Kifu".to_owned(), countdown.clone()); window.set_child(Some(&splash)); // window.fullscreen(); window.connect_maximized_notify(|window| { window.fullscreen(); }); gtk_rx.attach(None, move |event| { match event { Event::Frames(frames) => splash.tick(frames), Event::Time(time) => splash.set_time(time), }; Continue(true) }); std::thread::spawn(move || loop { std::thread::sleep(Duration::from_millis(1000 / 60)); let _ = gtk_tx.send(Event::Frames(1)); if Instant::now() >= next_tick && countdown > Duration::from_secs(0) { countdown = countdown - Duration::from_secs(1); let _ = gtk_tx.send(Event::Time(countdown)); next_tick = next_tick + Duration::from_secs(1); } }); }); app.run(); }