feat: add ahfail-ui crate with animation, audio, display, config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,3 +2,18 @@
|
||||
name = "ahfail-ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "ahfail_ui"
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[dependencies]
|
||||
gtk = { version = "0.15", package = "gtk", features = ["v3_24"] }
|
||||
gdk = { version = "0.15", package = "gdk", features = ["v3_24"] }
|
||||
gstreamer = { version = "0.18", package = "gstreamer", features = ["v1_18"] }
|
||||
gstreamer-player = { version = "0.18", package = "gstreamer-player" }
|
||||
glib = { version = "0.15", package = "glib" }
|
||||
gio = { version = "0.15", package = "gio" }
|
||||
gdk-pixbuf = "0.15"
|
||||
rand = "0.8"
|
||||
ureq = "2"
|
||||
|
||||
43
crates/ahfail-ui/src/animation.rs
Normal file
43
crates/ahfail-ui/src/animation.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use gtk::{gdk_pixbuf, gio};
|
||||
use gdk_pixbuf::InterpType;
|
||||
use glib::Cast;
|
||||
|
||||
const SPRITE_SCALE: f64 = 0.6;
|
||||
|
||||
extern "C" {
|
||||
pub fn ahfail_get_resource() -> *mut gio::ffi::GResource;
|
||||
}
|
||||
|
||||
/// Registers GResources and loads all sprite frames into a looping PixbufSimpleAnim.
|
||||
/// Returns None if GResources unavailable or no frames found.
|
||||
pub unsafe fn load_animation() -> Option<gdk_pixbuf::PixbufAnimation> {
|
||||
use glib::translate::from_glib_none;
|
||||
|
||||
let resource_ptr = ahfail_get_resource();
|
||||
if resource_ptr.is_null() { return None; }
|
||||
let resource = from_glib_none::<_, gio::Resource>(resource_ptr);
|
||||
gio::resources_register(&resource);
|
||||
|
||||
let mut frames = gio::resources_enumerate_children(
|
||||
"/ahfail/sprites", gio::ResourceLookupFlags::NONE,
|
||||
).ok()?;
|
||||
frames.sort();
|
||||
|
||||
let mut loaded: Vec<gdk_pixbuf::Pixbuf> = Vec::new();
|
||||
for name in frames {
|
||||
let path = format!("/ahfail/sprites/{}", name);
|
||||
if let Ok(pb) = gdk_pixbuf::Pixbuf::from_resource(&path) {
|
||||
let w = (pb.width() as f64 * SPRITE_SCALE) as i32;
|
||||
let h = (pb.height() as f64 * SPRITE_SCALE) as i32;
|
||||
let scaled = pb.scale_simple(w, h, InterpType::Bilinear).unwrap_or(pb);
|
||||
loaded.push(scaled);
|
||||
}
|
||||
}
|
||||
if loaded.is_empty() { return None; }
|
||||
|
||||
let first = &loaded[0];
|
||||
let anim = gdk_pixbuf::PixbufSimpleAnim::new(first.width(), first.height(), 12.0);
|
||||
anim.set_loop(true);
|
||||
for frame in loaded { anim.add_frame(&frame); }
|
||||
Some(anim.upcast())
|
||||
}
|
||||
12
crates/ahfail-ui/src/audio.rs
Normal file
12
crates/ahfail-ui/src/audio.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use gstreamer_player as gst_player;
|
||||
use gtk::glib;
|
||||
|
||||
pub fn create_player(uri: &str) -> gst_player::Player {
|
||||
let player = gst_player::Player::new(None, None);
|
||||
player.set_uri(Some(uri));
|
||||
player.connect_end_of_stream(glib::clone!(@weak player => move |_| {
|
||||
player.seek(gstreamer::ClockTime::from_seconds(0));
|
||||
}));
|
||||
player.connect_error(|_, err| eprintln!("[ahfail] GStreamer error: {}", err));
|
||||
player
|
||||
}
|
||||
37
crates/ahfail-ui/src/config.rs
Normal file
37
crates/ahfail-ui/src/config.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use gtk::gdk;
|
||||
use std::ffi::CStr;
|
||||
use std::ptr;
|
||||
|
||||
// Storage for the command line argument string
|
||||
pub static mut DEADZONE_ARG: *mut std::os::raw::c_char = ptr::null_mut();
|
||||
|
||||
pub const DEADZONE_LONG: &[u8] = b"deadzone\0";
|
||||
pub const DEADZONE_DESC: &[u8] = b"Area to avoid spawning sprites (x,y,w,h)\0";
|
||||
pub const DEADZONE_ARG_DESC: &[u8] = b"x,y,w,h\0";
|
||||
|
||||
pub struct ModuleConfig {
|
||||
pub deadzone: Option<gdk::Rectangle>,
|
||||
}
|
||||
|
||||
impl ModuleConfig {
|
||||
pub unsafe fn from_args() -> Self {
|
||||
let mut deadzone = None;
|
||||
if !DEADZONE_ARG.is_null() {
|
||||
let c_str = CStr::from_ptr(DEADZONE_ARG);
|
||||
if let Ok(s) = c_str.to_str() {
|
||||
let parts: Vec<&str> = s.split(',').collect();
|
||||
if parts.len() == 4 {
|
||||
if let (Ok(x), Ok(y), Ok(w), Ok(h)) = (parts[0].parse(), parts[1].parse(), parts[2].parse(), parts[3].parse()) {
|
||||
deadzone = Some(gdk::Rectangle::new(x, y, w, h));
|
||||
println!("[ahfail] Configured deadzone: {:?}", deadzone);
|
||||
} else {
|
||||
eprintln!("[ahfail] Invalid numbers in deadzone argument: {}", s);
|
||||
}
|
||||
} else {
|
||||
eprintln!("[ahfail] Invalid format for deadzone argument (expected x,y,w,h): {}", s);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self { deadzone }
|
||||
}
|
||||
}
|
||||
43
crates/ahfail-ui/src/display.rs
Normal file
43
crates/ahfail-ui/src/display.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use gtk::{gdk_pixbuf, prelude::*};
|
||||
use rand::Rng;
|
||||
use crate::config::ModuleConfig;
|
||||
|
||||
const SPRITE_MARGIN: i32 = 100;
|
||||
const RETRY_ATTEMPTS: usize = 10;
|
||||
|
||||
pub fn place_sprite(
|
||||
fixed: >k::Fixed,
|
||||
animation: &gdk_pixbuf::PixbufAnimation,
|
||||
screen_w: i32,
|
||||
screen_h: i32,
|
||||
config: &ModuleConfig,
|
||||
) -> gtk::Image {
|
||||
let sprite_w = animation.width();
|
||||
let sprite_h = animation.height();
|
||||
|
||||
let safe_w = screen_w - SPRITE_MARGIN;
|
||||
let safe_h = screen_h - SPRITE_MARGIN;
|
||||
|
||||
let max_x = (safe_w - sprite_w).max(0);
|
||||
let max_y = (safe_h - sprite_h).max(0);
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut x = rng.gen_range(0..=max_x);
|
||||
let mut y = rng.gen_range(0..=max_y);
|
||||
|
||||
if let Some(dz) = config.deadzone {
|
||||
for _ in 0..RETRY_ATTEMPTS {
|
||||
let overlaps_x = x < dz.x() + dz.width() && x + sprite_w > dz.x();
|
||||
let overlaps_y = y < dz.y() + dz.height() && y + sprite_h > dz.y();
|
||||
if !(overlaps_x && overlaps_y) {
|
||||
break;
|
||||
}
|
||||
x = rng.gen_range(0..=max_x);
|
||||
y = rng.gen_range(0..=max_y);
|
||||
}
|
||||
}
|
||||
|
||||
let image = gtk::Image::from_animation(animation);
|
||||
fixed.put(&image, x, y);
|
||||
image
|
||||
}
|
||||
@@ -1 +1,8 @@
|
||||
pub fn placeholder() {}
|
||||
pub mod animation;
|
||||
pub mod audio;
|
||||
pub mod config;
|
||||
pub mod display;
|
||||
pub mod update;
|
||||
pub mod volume;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
1
crates/ahfail-ui/src/update.rs
Normal file
1
crates/ahfail-ui/src/update.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub fn check_for_update(_current_version: &str) {}
|
||||
7
crates/ahfail-ui/src/volume.rs
Normal file
7
crates/ahfail-ui/src/volume.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub struct VolumeState;
|
||||
|
||||
pub fn save_and_set_max() -> Option<VolumeState> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn restore(_state: VolumeState) {}
|
||||
Reference in New Issue
Block a user