Create a cyberpunk-style splash screen with a title and a countdown #39

Merged
savanni merged 11 commits from cyberpunk-display into main 2023-04-13 23:14:29 +00:00
1 changed files with 123 additions and 67 deletions
Showing only changes of commit 8f4a8b77bc - Show all commits

View File

@ -7,6 +7,7 @@ use gtk::{gdk::Key, prelude::*, subclass::prelude::*, EventControllerKey};
use std::{ use std::{
cell::RefCell, cell::RefCell,
rc::Rc, rc::Rc,
sync::{Arc, RwLock},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -19,7 +20,85 @@ enum Event {
Time(Duration), Time(Duration),
} }
struct TimeoutAnimation { #[derive(Clone, Copy, Debug)]
pub enum State {
Running {
last_update: Instant,
deadline: Instant,
timeout: Option<TimeoutAnimation>,
},
Paused {
time_remaining: Duration,
timeout: Option<TimeoutAnimation>,
},
}
impl State {
fn new(countdown: Duration) -> Self {
Self::Paused {
time_remaining: countdown,
timeout: None,
}
}
fn start(&mut self) {
if let Self::Paused {
time_remaining,
timeout,
} = self
{
*self = Self::Running {
last_update: Instant::now(),
deadline: Instant::now() + *time_remaining,
timeout: timeout.clone(),
};
}
}
fn pause(&mut self) {
if let Self::Running {
deadline, timeout, ..
} = self
{
*self = Self::Paused {
time_remaining: *deadline - Instant::now(),
timeout: timeout.clone(),
}
}
}
fn start_pause(&mut self) {
match self {
Self::Running { .. } => self.pause(),
Self::Paused { .. } => self.start(),
}
}
fn run(&mut self, now: Instant) {
if let Self::Running {
last_update,
deadline,
timeout,
} = self
{
*last_update = now;
if let Some(ref mut timeout) = timeout {
// TODO: figure out the actual number of frames
timeout.tick(1);
}
if *last_update > *deadline && timeout.is_none() {
*timeout = Some(TimeoutAnimation {
intensity: 1.,
duration: 1.,
ascending: false,
});
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TimeoutAnimation {
intensity: f64, intensity: f64,
duration: f64, duration: f64,
ascending: bool, ascending: bool,
@ -46,13 +125,12 @@ impl TimeoutAnimation {
pub struct SplashPrivate { pub struct SplashPrivate {
text: Rc<RefCell<String>>, text: Rc<RefCell<String>>,
time: Rc<RefCell<Duration>>,
background: Rc<RefCell<Pattern>>, background: Rc<RefCell<Pattern>>,
time_extents: Rc<RefCell<Option<TextExtents>>>, time_extents: Rc<RefCell<Option<TextExtents>>>,
width: Rc<RefCell<i32>>, width: Rc<RefCell<i32>>,
height: Rc<RefCell<i32>>, height: Rc<RefCell<i32>>,
timeout_animation: Rc<RefCell<Option<TimeoutAnimation>>>, state: Rc<RefCell<State>>,
} }
impl SplashPrivate { impl SplashPrivate {
@ -60,8 +138,8 @@ impl SplashPrivate {
*self.text.borrow_mut() = text; *self.text.borrow_mut() = text;
} }
fn set_time(&self, time: Duration) { fn set_state(&self, state: State) {
*self.time.borrow_mut() = time; *self.state.borrow_mut() = state;
} }
fn redraw_background(&self) { fn redraw_background(&self) {
@ -174,12 +252,12 @@ impl ObjectSubclass for SplashPrivate {
SplashPrivate { SplashPrivate {
text: Rc::new(RefCell::new(String::from(""))), text: Rc::new(RefCell::new(String::from(""))),
time: Rc::new(RefCell::new(Duration::ZERO)),
background: Rc::new(RefCell::new(background)), background: Rc::new(RefCell::new(background)),
time_extents: Rc::new(RefCell::new(None)), time_extents: Rc::new(RefCell::new(None)),
width: Rc::new(RefCell::new(WIDTH)), width: Rc::new(RefCell::new(WIDTH)),
height: Rc::new(RefCell::new(HEIGHT)), height: Rc::new(RefCell::new(HEIGHT)),
timeout_animation: Rc::new(RefCell::new(None)),
state: Rc::new(RefCell::new(State::new(Duration::ZERO))),
} }
} }
} }
@ -192,13 +270,13 @@ glib::wrapper! {
} }
impl Splash { impl Splash {
pub fn new(text: String, time: Duration) -> Self { pub fn new(text: String, state: State) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_width_request(WIDTH); s.set_width_request(WIDTH);
s.set_height_request(HEIGHT); s.set_height_request(HEIGHT);
s.imp().set_text(text); s.imp().set_text(text);
s.imp().set_time(time); s.imp().set_state(state);
s.imp().redraw_background(); s.imp().redraw_background();
s.set_draw_func({ s.set_draw_func({
@ -208,15 +286,18 @@ impl Splash {
let _ = context.set_source(&*background); let _ = context.set_source(&*background);
let _ = context.paint(); let _ = context.paint();
let time = s.imp().time.borrow().clone(); let state = s.imp().state.borrow().clone();
let time = match state {
State::Running { deadline, .. } => deadline - Instant::now(),
State::Paused { time_remaining, .. } => time_remaining,
};
let minutes = time.as_secs() / 60; let minutes = time.as_secs() / 60;
let seconds = time.as_secs() % 60; let seconds = time.as_secs() % 60;
let center_x = width as f64 / 2.; let center_x = width as f64 / 2.;
let center_y = height as f64 / 2.; let center_y = height as f64 / 2.;
{}
{ {
context.select_font_face( context.select_font_face(
"Alegreya Sans SC", "Alegreya Sans SC",
@ -237,39 +318,30 @@ impl Splash {
let time_baseline_x = center_x - time_extents.width() / 2.; let time_baseline_x = center_x - time_extents.width() / 2.;
let time_baseline_y = center_y + 100.; 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( let gradient = LinearGradient::new(
time_baseline_x, time_baseline_x,
time_baseline_y - time_extents.height(), time_baseline_y - time_extents.height(),
time_baseline_x, time_baseline_x,
time_baseline_y, time_baseline_y,
); );
match *s.imp().timeout_animation.borrow() { let (running, timeout_animation) = match state {
State::Running { timeout, .. } => (true, timeout.clone()),
State::Paused { timeout, .. } => (false, timeout.clone()),
};
match timeout_animation {
Some(ref animation) => { Some(ref animation) => {
gradient.add_color_stop_rgba(0.2, 0.2, 0.0, 1.0, animation.intensity); 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); gradient.add_color_stop_rgba(0.8, 0.7, 0.0, 1.0, animation.intensity);
let _ = context.set_source(gradient); let _ = context.set_source(gradient);
} }
None => { None => {
if running {
gradient.add_color_stop_rgb(0.2, 0.2, 0.0, 1.0); 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); gradient.add_color_stop_rgb(0.8, 0.7, 0.0, 1.0);
let _ = context.set_source(gradient); let _ = context.set_source(gradient);
} else {
context.set_source_rgb(0.3, 0.3, 0.3);
}
} }
} }
context.move_to(time_baseline_x, time_baseline_y); context.move_to(time_baseline_x, time_baseline_y);
@ -304,28 +376,10 @@ impl Splash {
s s
} }
pub fn set_time(&self, time: Duration) { pub fn set_state(&self, state: State) {
self.imp().set_time(time); self.imp().set_state(state);
if time == Duration::ZERO {
*self.imp().timeout_animation.borrow_mut() = Some(TimeoutAnimation {
intensity: 1.,
duration: 1.,
ascending: false,
});
}
self.queue_draw(); 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 { struct AsymLineCutout {
@ -504,14 +558,13 @@ fn main() {
app.connect_activate(move |app| { app.connect_activate(move |app| {
let (gtk_tx, gtk_rx) = let (gtk_tx, gtk_rx) =
gtk::glib::MainContext::channel::<Event>(gtk::glib::PRIORITY_DEFAULT); gtk::glib::MainContext::channel::<State>(gtk::glib::PRIORITY_DEFAULT);
let window = gtk::ApplicationWindow::new(app); let window = gtk::ApplicationWindow::new(app);
window.present(); window.present();
let mut countdown = Duration::from_secs(5); let state = State::new(Duration::from_secs(5 * 60));
let mut next_tick = Instant::now() + Duration::from_secs(1); let splash = Splash::new("GTK Kifu".to_owned(), state.clone());
let state = Arc::new(RwLock::new(state));
let splash = Splash::new("GTK Kifu".to_owned(), countdown.clone());
window.set_child(Some(&splash)); window.set_child(Some(&splash));
@ -522,6 +575,7 @@ fn main() {
let keyboard_events = EventControllerKey::new(); let keyboard_events = EventControllerKey::new();
keyboard_events.connect_key_released({ keyboard_events.connect_key_released({
let window = window.clone(); let window = window.clone();
let state = state.clone();
move |_, key, _, _| { move |_, key, _, _| {
let name = key let name = key
.name() .name()
@ -529,28 +583,30 @@ fn main() {
.unwrap_or("".to_owned()); .unwrap_or("".to_owned());
match name.as_ref() { match name.as_ref() {
"Escape" => window.unfullscreen(), "Escape" => window.unfullscreen(),
"space" => println!("space pressed"), "space" => state.write().unwrap().start_pause(),
_ => {} _ => {}
} }
} }
}); });
window.add_controller(keyboard_events); window.add_controller(keyboard_events);
gtk_rx.attach(None, move |event| { gtk_rx.attach(None, move |state| {
/*
match event { match event {
Event::Frames(frames) => splash.tick(frames), Event::Frames(frames) => splash.tick(frames),
Event::Time(time) => splash.set_time(time), Event::Time(time) => splash.set_time(time),
}; };
*/
splash.set_state(state);
Continue(true) Continue(true)
}); });
std::thread::spawn(move || loop { std::thread::spawn(move || {
state.write().unwrap().start();
loop {
std::thread::sleep(Duration::from_millis(1000 / 60)); std::thread::sleep(Duration::from_millis(1000 / 60));
let _ = gtk_tx.send(Event::Frames(1)); state.write().unwrap().run(Instant::now());
if Instant::now() >= next_tick && countdown > Duration::from_secs(0) { let _ = gtk_tx.send(state.read().unwrap().clone());
countdown = countdown - Duration::from_secs(1);
let _ = gtk_tx.send(Event::Time(countdown));
next_tick = next_tick + Duration::from_secs(1);
} }
}); });
}); });