Create a slideshow application in my cyberpunk style #252

Merged
savanni merged 15 commits from cybperpunk-billboard into main 2024-11-03 21:16:38 +00:00
5 changed files with 923 additions and 782 deletions
Showing only changes of commit 7a7548c78f - Show all commits

1419
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
cairo-rs = "0.20.1" cairo-rs = "0.18"
cyberpunk = { path = "../cyberpunk" } cyberpunk = { path = "../cyberpunk" }
gio = "0.20.4" gio = "0.18"
glib = "0.20.4" glib = "0.18"
gtk = { version = "0.7", package = "gtk4" } gtk = { version = "0.7", package = "gtk4" }
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
serde_yml = "0.0.12" serde_yml = "0.0.12"

View File

@ -1,20 +1,11 @@
- text: The distinguishing thing about magic is that it includes some kind of personal element. The person who is performing the magic is relevant to the magic. -- Ted Chang, Marie Brennan - text: The distinguishing thing about magic is that it includes some kind of personal element. The person who is performing the magic is relevant to the magic. -- Ted Chang, Marie Brennan
position: top position: top
transition_in: !Fade transition: !Fade
duration:
secs: 5
nanos: 0
- text: Any sufficiently advanced technology is indistinguishable from magic. -- Arthur C. Clark. - text: Any sufficiently advanced technology is indistinguishable from magic. -- Arthur C. Clark.
position: middle position: middle
transition_in: !Fade transition: !Fade
duration:
secs: 5
nanos: 0
- text: Electricity is the closest we get to Magic in this world. - text: Electricity is the closest we get to Magic in this world.
position: middle position: middle
transition_in: !Fade transition: !Fade
duration:
secs: 5
nanos: 0

View File

@ -1,42 +1,47 @@
use std::{fs::File, io::Read, path::Path, rc::Rc, time::Duration}; use std::{cell::RefCell, fs::File, io::Read, ops::Index, path::Path, rc::Rc, time::Duration};
use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text}; use cyberpunk::{AsymLine, AsymLineCutout, GlowPen, Pen, Text};
use glib::Object; use glib::{GString, Object};
use gtk::{glib, prelude::*, subclass::prelude::*}; use gtk::{
glib::{self, Propagation},
prelude::*,
subclass::prelude::*,
EventControllerKey,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)] const PURPLE: (f64, f64, f64) = (0.7, 0., 1.);
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
enum TextArea { enum Position {
Top, Top,
Middle, Middle,
Bottom, Bottom,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
struct Fade { enum TransitionName {
duration: Duration, Fade,
CrossFade,
} }
#[derive(Serialize, Deserialize, Debug)] struct Fade(Step);
enum AnimationIn {
enum Transition {
Fade(Fade), Fade(Fade),
CrossFade(Step, Step),
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
enum AnimationOut { struct Step {
Fade(Fade),
}
#[derive(Serialize, Deserialize, Debug)]
struct ScriptElement {
text: String, text: String,
position: TextArea, position: Position,
transition_in: AnimationIn, transition: TransitionName,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
struct Script(Vec<ScriptElement>); struct Script(Vec<Step>);
impl Script { impl Script {
fn from_file(path: &Path) -> Result<Script, serde_yml::Error> { fn from_file(path: &Path) -> Result<Script, serde_yml::Error> {
@ -47,13 +52,106 @@ impl Script {
Ok(Self(script)) Ok(Self(script))
} }
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a ScriptElement> { fn iter<'a>(&'a self) -> impl Iterator<Item = &'a Step> {
self.0.iter() self.0.iter()
} }
fn len(&self) -> usize {
self.0.len()
}
}
impl Default for Script {
fn default() -> Self {
Self(vec![])
}
}
impl Index<usize> for Script {
type Output = Step;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
#[derive(Debug)]
pub struct CyberScreenState {
script: Script,
idx: Option<usize>,
top: Option<Step>,
middle: Option<Step>,
bottom: Option<Step>,
}
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)] #[derive(Default)]
pub struct CyberScreenPrivate {} pub struct CyberScreenPrivate {
state: Rc<RefCell<CyberScreenState>>,
transition_queue: Rc<RefCell<Vec<Transition>>>,
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for CyberScreenPrivate { impl ObjectSubclass for CyberScreenPrivate {
@ -66,74 +164,89 @@ impl ObjectImpl for CyberScreenPrivate {}
impl WidgetImpl for CyberScreenPrivate {} impl WidgetImpl for CyberScreenPrivate {}
impl DrawingAreaImpl 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! { glib::wrapper! {
pub struct CyberScreen(ObjectSubclass<CyberScreenPrivate>) @extends gtk::DrawingArea, gtk::Widget; pub struct CyberScreen(ObjectSubclass<CyberScreenPrivate>) @extends gtk::DrawingArea, gtk::Widget;
} }
impl CyberScreen { impl CyberScreen {
pub fn new() -> Self { pub fn new(script: Script) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.imp().set_script(script);
s.set_draw_func({ s.set_draw_func({
let s = s.clone();
move |_, context, width, height| { move |_, context, width, height| {
let _ = context.set_source_rgb(0., 0., 0.); let _ = context.set_source_rgb(0., 0., 0.);
let _ = context.paint(); let _ = context.paint();
let pen = GlowPen::new(width, height, 2., 8., (0.7, 0., 1.)); let pen = GlowPen::new(width, height, 2., 8., (0.7, 0., 1.));
/*
AsymLine { for i in 0..6 {
orientation: gtk::Orientation::Horizontal, pen.move_to(0., height as f64 * i as f64 / 5.);
start_x: 50., pen.line_to(width as f64, height as f64 * i as f64 / 5.);
start_y: height as f64 / 2.,
start_length: 75.,
height: 50.,
end_length: 75.,
invert: false,
} }
.draw(&pen);
pen.stroke(); pen.stroke();
*/
AsymLineCutout {
orientation: gtk::Orientation::Horizontal,
start_x: 50.,
start_y: height as f64 / 4. * 3.,
start_length: 75.,
cutout_length: 15.,
end_length: 75.,
height: 15.,
invert: true,
}.draw(&pen);
pen.stroke();
let tracery = pen.finish(); let tracery = pen.finish();
let _ = context.set_source(tracery); let _ = context.set_source(tracery);
let _ = context.paint(); let _ = context.paint();
let text = Text::new("Test text".to_owned(), &context, 64.); for transition in s.imp().transition_queue.borrow().iter() {
let text_extents = text.extents(); if let Some(ref text) = s.imp().state.borrow().top {
context.move_to(20., text_extents.height() + 40.); let text_display = Text::new(text.text.clone(), context, 32.);
context.set_source_rgb(0.7, 0., 1.); let y = height as f64 * 1. / 5. + text_display.extents().height();
text.draw();
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 s
} }
fn next_page(&self) {
println!("next page");
self.imp().next_page();
self.queue_draw();
}
} }
fn main() { fn main() {
/*
let script: Script = Script(vec![ScriptElement {
text: "abcdefg".to_owned(),
position: TextArea::Top,
transition_in: AnimationIn::Fade(Fade {
duration: Duration::from_secs(10),
}),
}]);
println!("{}", serde_yml::to_string(&script).unwrap());
*/
let script = Script::from_file(Path::new("./script.yml")).unwrap(); let script = Script::from_file(Path::new("./script.yml")).unwrap();
for element in script.iter() { for element in script.iter() {
println!("{:?}", element); println!("{:?}", element);
@ -143,11 +256,22 @@ fn main() {
.application_id("com.luminescent-dreams.cyberpunk-slideshow") .application_id("com.luminescent-dreams.cyberpunk-slideshow")
.build(); .build();
app.connect_activate(|app| { app.connect_activate(move |app| {
let window = gtk::ApplicationWindow::new(app); let screen = CyberScreen::new(script.clone());
window.present();
let screen = CyberScreen::new(); 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_child(Some(&screen));
window.set_width_request(800); window.set_width_request(800);

View File

@ -1,14 +1,7 @@
use cairo::{ use cairo::{
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern, Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, Pattern,
TextExtents, TextExtents,
}; };
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, RwLock},
time::{Duration, Instant},
};
pub struct AsymLineCutout { pub struct AsymLineCutout {
pub orientation: gtk::Orientation, pub orientation: gtk::Orientation,