Code
58
.gitignore
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug
|
||||||
|
target
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# Generated by cargo mutants
|
||||||
|
# Contains mutation testing data
|
||||||
|
**/mutants.out*/
|
||||||
|
|
||||||
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
.mypy_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
/.project
|
||||||
|
/.pydevproject
|
||||||
|
/.settings
|
||||||
|
/.cproject
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
|
||||||
|
__pycache__
|
||||||
|
/.coverage/
|
||||||
|
/.coveragerc
|
||||||
|
/install dir
|
||||||
|
/work area
|
||||||
|
|
||||||
|
/meson-test-run.txt
|
||||||
|
/meson-test-run.xml
|
||||||
|
/meson-cross-test-run.txt
|
||||||
|
/meson-cross-test-run.xml
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
packagecache
|
||||||
|
.wraplock
|
||||||
|
/MANIFEST
|
||||||
|
/build
|
||||||
|
/dist
|
||||||
|
/meson.egg-info
|
||||||
|
|
||||||
|
/docs/built_docs
|
||||||
|
/docs/hotdoc-private*
|
||||||
|
|
||||||
|
*.pyc
|
||||||
|
/*venv*
|
||||||
|
|
||||||
1002
Cargo.lock
generated
Normal file
23
Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "ahfail"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "ahfail_module"
|
||||||
|
crate-type = ["staticlib", "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"
|
||||||
|
libc = "0.2"
|
||||||
|
once_cell = "1.10"
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
pkg-config = "0.3"
|
||||||
31
assets/ahfail.gresource.xml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gresources>
|
||||||
|
<gresource prefix="/ahfail">
|
||||||
|
<file alias="audio/magic-word.mp3">magic-word.mp3</file>
|
||||||
|
<file alias="sprites/frame__0000.png">frame__0000.png</file>
|
||||||
|
<file alias="sprites/frame__0001.png">frame__0001.png</file>
|
||||||
|
<file alias="sprites/frame__0002.png">frame__0002.png</file>
|
||||||
|
<file alias="sprites/frame__0003.png">frame__0003.png</file>
|
||||||
|
<file alias="sprites/frame__0004.png">frame__0004.png</file>
|
||||||
|
<file alias="sprites/frame__0005.png">frame__0005.png</file>
|
||||||
|
<file alias="sprites/frame__0006.png">frame__0006.png</file>
|
||||||
|
<file alias="sprites/frame__0007.png">frame__0007.png</file>
|
||||||
|
<file alias="sprites/frame__0008.png">frame__0008.png</file>
|
||||||
|
<file alias="sprites/frame__0009.png">frame__0009.png</file>
|
||||||
|
<file alias="sprites/frame__0010.png">frame__0010.png</file>
|
||||||
|
<file alias="sprites/frame__0011.png">frame__0011.png</file>
|
||||||
|
<file alias="sprites/frame__0012.png">frame__0012.png</file>
|
||||||
|
<file alias="sprites/frame__0013.png">frame__0013.png</file>
|
||||||
|
<file alias="sprites/frame__0014.png">frame__0014.png</file>
|
||||||
|
<file alias="sprites/frame__0015.png">frame__0015.png</file>
|
||||||
|
<file alias="sprites/frame__0016.png">frame__0016.png</file>
|
||||||
|
<file alias="sprites/frame__0017.png">frame__0017.png</file>
|
||||||
|
<file alias="sprites/frame__0018.png">frame__0018.png</file>
|
||||||
|
<file alias="sprites/frame__0019.png">frame__0019.png</file>
|
||||||
|
<file alias="sprites/frame__0020.png">frame__0020.png</file>
|
||||||
|
<file alias="sprites/frame__0021.png">frame__0021.png</file>
|
||||||
|
<file alias="sprites/frame__0022.png">frame__0022.png</file>
|
||||||
|
<file alias="sprites/frame__0023.png">frame__0023.png</file>
|
||||||
|
<file alias="sprites/frame__0024.png">frame__0024.png</file>
|
||||||
|
</gresource>
|
||||||
|
</gresources>
|
||||||
BIN
assets/frame__0000.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/frame__0001.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0002.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0003.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0004.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0005.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0006.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/frame__0007.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
assets/frame__0008.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
assets/frame__0009.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
assets/frame__0010.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/frame__0011.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/frame__0012.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0013.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0014.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/frame__0015.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0016.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0017.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0018.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0019.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0020.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0021.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/frame__0022.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0023.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/frame__0024.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/magic-word.mp3
Normal file
8
include/ahfail/module.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "gtklock-module.h"
|
||||||
|
|
||||||
|
extern const char module_name[];
|
||||||
|
extern const guint module_major_version;
|
||||||
|
extern const guint module_minor_version;
|
||||||
|
extern GOptionEntry module_entries[];
|
||||||
100
include/gtklock-module.h
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// gtklock-userinfo-module
|
||||||
|
// Copyright (c) 2024 Jovan Lanik
|
||||||
|
|
||||||
|
// Module header
|
||||||
|
|
||||||
|
#include "gtk/gtk.h"
|
||||||
|
|
||||||
|
struct Window {
|
||||||
|
GdkMonitor *monitor;
|
||||||
|
|
||||||
|
GtkWidget *window;
|
||||||
|
GtkWidget *overlay;
|
||||||
|
GtkWidget *window_box;
|
||||||
|
GtkWidget *body_revealer;
|
||||||
|
GtkWidget *body_grid;
|
||||||
|
GtkWidget *input_label;
|
||||||
|
GtkWidget *input_field;
|
||||||
|
GtkWidget *message_revealer;
|
||||||
|
GtkWidget *message_scrolled_window;
|
||||||
|
GtkWidget *message_box;
|
||||||
|
GtkWidget *unlock_button;
|
||||||
|
GtkWidget *error_label;
|
||||||
|
GtkWidget *warning_label;
|
||||||
|
GtkWidget *info_box;
|
||||||
|
GtkWidget *time_box;
|
||||||
|
GtkWidget *clock_label;
|
||||||
|
GtkWidget *date_label;
|
||||||
|
|
||||||
|
void *module_data[];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GtkLock {
|
||||||
|
GtkApplication *app;
|
||||||
|
void *lock;
|
||||||
|
pid_t parent;
|
||||||
|
|
||||||
|
GArray *windows;
|
||||||
|
GArray *messages;
|
||||||
|
GArray *errors;
|
||||||
|
|
||||||
|
struct Window *focused_window;
|
||||||
|
gboolean hidden;
|
||||||
|
guint idle_timeout;
|
||||||
|
|
||||||
|
guint draw_time_source;
|
||||||
|
guint idle_hide_source;
|
||||||
|
|
||||||
|
gboolean follow_focus;
|
||||||
|
gboolean use_idle_hide;
|
||||||
|
|
||||||
|
char *time;
|
||||||
|
char *date;
|
||||||
|
char *time_format;
|
||||||
|
char *date_format;
|
||||||
|
char *config_path;
|
||||||
|
char *layout_path;
|
||||||
|
char *lock_command;
|
||||||
|
char *unlock_command;
|
||||||
|
|
||||||
|
GArray *modules;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gchar *g_module_check_init(GModule *m);
|
||||||
|
void g_module_unload(GModule *m);
|
||||||
|
void on_activation(struct GtkLock *gtklock, int id);
|
||||||
|
void on_locked(struct GtkLock *gtklock);
|
||||||
|
void on_output_change(struct GtkLock *gtklock);
|
||||||
|
void on_focus_change(struct GtkLock *gtklock, struct Window *win, struct Window *old);
|
||||||
|
void on_idle_hide(struct GtkLock *gtklock);
|
||||||
|
void on_idle_show(struct GtkLock *gtklock);
|
||||||
|
void on_window_create(struct GtkLock *gtklock, struct Window *win);
|
||||||
|
void on_window_destroy(struct GtkLock *gtklock, struct Window *win);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
MIT Licence
|
||||||
|
|
||||||
|
Copyright (c) 2024 Jovan Lanik <jox969@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
MIT Licence
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
53
meson.build
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
project(
|
||||||
|
'ahfail',
|
||||||
|
['c', 'rust'],
|
||||||
|
version: '0.1.0',
|
||||||
|
meson_version: '>=1.3.0',
|
||||||
|
default_options: ['warning_level=3', 'c_std=c11']
|
||||||
|
)
|
||||||
|
|
||||||
|
ahfail_inc = include_directories('include')
|
||||||
|
cc = meson.get_compiler('c')
|
||||||
|
m_dep = cc.find_library('m', required : false)
|
||||||
|
gtk_dep = dependency('gtk+-3.0')
|
||||||
|
gstplayer_dep = dependency('gstreamer-player-1.0')
|
||||||
|
gnome = import('gnome')
|
||||||
|
|
||||||
|
resources = gnome.compile_resources(
|
||||||
|
'ahfail-resources',
|
||||||
|
'assets/ahfail.gresource.xml',
|
||||||
|
source_dir: 'assets',
|
||||||
|
c_name: 'ahfail'
|
||||||
|
)
|
||||||
|
|
||||||
|
cargo_target = custom_target(
|
||||||
|
'ahfail-cargo-build',
|
||||||
|
input: ['src/lib.rs', 'Cargo.toml'],
|
||||||
|
output: ['libahfail_module.a'],
|
||||||
|
command: [
|
||||||
|
'sh', '-c', 'cargo build --release --target-dir "@OUTDIR@/target" && cp "@OUTDIR@/target/release/libahfail_module.a" "@OUTPUT@"'
|
||||||
|
],
|
||||||
|
build_by_default: true
|
||||||
|
)
|
||||||
|
|
||||||
|
libahfail = shared_library(
|
||||||
|
'ahfail-module',
|
||||||
|
resources,
|
||||||
|
link_args: ['-Wl,--whole-archive', meson.current_build_dir() / 'libahfail_module.a', '-Wl,--no-whole-archive'],
|
||||||
|
link_depends: cargo_target,
|
||||||
|
include_directories: ahfail_inc,
|
||||||
|
dependencies: [gtk_dep, gstplayer_dep, m_dep],
|
||||||
|
install: true,
|
||||||
|
install_dir: get_option('libdir') / 'gtklock',
|
||||||
|
name_prefix: ''
|
||||||
|
)
|
||||||
|
|
||||||
|
smoke = executable(
|
||||||
|
'module_smoke_test',
|
||||||
|
'tests/module_test.c',
|
||||||
|
include_directories: ahfail_inc,
|
||||||
|
link_with: libahfail,
|
||||||
|
dependencies: [gtk_dep, gstplayer_dep]
|
||||||
|
)
|
||||||
|
|
||||||
|
test('module symbols', smoke)
|
||||||
37
src/config.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use gtk::{glib, 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/context.rs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
use gtk::prelude::*; // Keep this if extension traits are needed, otherwise remove. Window uses it.
|
||||||
|
use gtk::{glib, gdk}; // Window uses gdk, glib.
|
||||||
|
use gtk::gdk::Monitor;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::ptr; // Import ptr
|
||||||
|
use glib::translate::from_glib_none;
|
||||||
|
use crate::state::WindowData;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct __IncompleteArrayField<T>(PhantomData<T>);
|
||||||
|
|
||||||
|
impl<T> __IncompleteArrayField<T> {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self(PhantomData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Returns a raw pointer to the field. The caller must ensure the struct was allocated
|
||||||
|
/// with enough space for this flexible array member.
|
||||||
|
pub unsafe fn as_ptr(&self) -> *mut T {
|
||||||
|
self as *const _ as *mut T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for __IncompleteArrayField<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Window {
|
||||||
|
pub monitor: *mut gdk::ffi::GdkMonitor,
|
||||||
|
pub window: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub overlay: *mut gtk::ffi::GtkOverlay,
|
||||||
|
pub window_box: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub body_revealer: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub body_grid: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub input_label: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub input_field: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub message_revealer: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub message_scrolled_window: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub message_box: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub unlock_button: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub error_label: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub warning_label: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub info_box: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub time_box: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub clock_label: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub date_label: *mut gtk::ffi::GtkWidget,
|
||||||
|
pub module_data: __IncompleteArrayField<*mut c_void>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct GtkLock {
|
||||||
|
pub windows: *mut glib::ffi::GArray,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe wrapper for Window pointer operations
|
||||||
|
pub struct WindowContext(*mut Window);
|
||||||
|
|
||||||
|
impl WindowContext {
|
||||||
|
/// # Safety
|
||||||
|
/// `ctx` must be a valid pointer to a `Window` struct.
|
||||||
|
pub unsafe fn new(ctx: *mut Window) -> Option<Self> {
|
||||||
|
if ctx.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Self(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_error_label(&self) -> gtk::Label {
|
||||||
|
from_glib_none((*self.0).error_label as *mut gtk::ffi::GtkLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_overlay(&self) -> gtk::Overlay {
|
||||||
|
from_glib_none((*self.0).overlay as *mut gtk::ffi::GtkOverlay)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_monitor(&self) -> Monitor {
|
||||||
|
from_glib_none((*self.0).monitor as *mut gdk::ffi::GdkMonitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// The caller must ensure `module_data` has been initialized and points to a valid `WindowData`.
|
||||||
|
pub unsafe fn set_data(&self, data: Box<WindowData>) {
|
||||||
|
let raw_ptr = Box::into_raw(data);
|
||||||
|
(*(*self.0).module_data.as_ptr()) = raw_ptr as *mut c_void;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_data_ptr(&self) -> *mut WindowData {
|
||||||
|
*(*self.0).module_data.as_ptr() as *mut WindowData
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// The caller takes ownership of the data and must drop it.
|
||||||
|
pub unsafe fn take_data(&self) -> Option<Box<WindowData>> {
|
||||||
|
let ptr_ref = (*self.0).module_data.as_ptr();
|
||||||
|
let ptr = *ptr_ref;
|
||||||
|
if ptr.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
*ptr_ref = ptr::null_mut(); // Clear it
|
||||||
|
Some(Box::from_raw(ptr as *mut WindowData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
165
src/handler.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{glib, gdk, gdk_pixbuf, gio};
|
||||||
|
use gstreamer as gst;
|
||||||
|
use gstreamer_player as gst_player;
|
||||||
|
use rand::Rng;
|
||||||
|
use crate::state::{MODULE_STATE, WindowData};
|
||||||
|
use crate::context::WindowContext;
|
||||||
|
|
||||||
|
const SPRITE_MARGIN: i32 = 100;
|
||||||
|
const SPRITE_SCALE: f64 = 0.6;
|
||||||
|
const PLAYER_POOL_SIZE: usize = 3;
|
||||||
|
const RETRY_ATTEMPTS: usize = 10;
|
||||||
|
|
||||||
|
pub struct WindowHandler;
|
||||||
|
|
||||||
|
impl WindowHandler {
|
||||||
|
pub unsafe fn create(ctx: &WindowContext) {
|
||||||
|
println!("[ahfail] on_window_create called");
|
||||||
|
|
||||||
|
let error_label = ctx.get_error_label();
|
||||||
|
let overlay = ctx.get_overlay();
|
||||||
|
let monitor = ctx.get_monitor();
|
||||||
|
|
||||||
|
let geom = monitor.geometry();
|
||||||
|
let screen_w = geom.width();
|
||||||
|
let screen_h = geom.height();
|
||||||
|
|
||||||
|
let fixed = gtk::Fixed::new();
|
||||||
|
fixed.set_size_request(screen_w, screen_h);
|
||||||
|
fixed.set_halign(gtk::Align::Fill);
|
||||||
|
fixed.set_valign(gtk::Align::Fill);
|
||||||
|
fixed.set_hexpand(true);
|
||||||
|
fixed.set_vexpand(true);
|
||||||
|
|
||||||
|
overlay.add_overlay(&fixed);
|
||||||
|
overlay.set_overlay_pass_through(&fixed, true);
|
||||||
|
|
||||||
|
let mut ready_players = Vec::new();
|
||||||
|
MODULE_STATE.with(|state| {
|
||||||
|
if let Some(audio_uri) = &state.borrow().audio_uri {
|
||||||
|
for _ in 0..PLAYER_POOL_SIZE {
|
||||||
|
ready_players.push(Self::create_player(audio_uri));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = Box::new(WindowData {
|
||||||
|
sprites: Vec::new(),
|
||||||
|
active_players: Vec::new(),
|
||||||
|
ready_players,
|
||||||
|
fixed: fixed.clone(),
|
||||||
|
signal_id: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.set_data(data);
|
||||||
|
let ptr_addr = ctx.get_data_ptr() as usize;
|
||||||
|
|
||||||
|
let signal_id = error_label.connect_notify(Some("label"), move |label, _| {
|
||||||
|
let text = label.text();
|
||||||
|
let text_str = text.as_str();
|
||||||
|
if text_str.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("[ahfail] Error label changed to: '{}'", text_str);
|
||||||
|
|
||||||
|
MODULE_STATE.with(|state| {
|
||||||
|
let state = state.borrow();
|
||||||
|
if let (Some(animation), Some(audio_uri)) = (&state.animation, &state.audio_uri) {
|
||||||
|
let data = unsafe { &mut *(ptr_addr as *mut WindowData) };
|
||||||
|
|
||||||
|
let image = gtk::Image::from_animation(animation);
|
||||||
|
image.show();
|
||||||
|
|
||||||
|
let sprite_w = animation.width();
|
||||||
|
let sprite_h = animation.height();
|
||||||
|
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
let safe_w = screen_w - SPRITE_MARGIN;
|
||||||
|
let safe_h = screen_h - SPRITE_MARGIN;
|
||||||
|
|
||||||
|
let max_x = if safe_w > sprite_w { safe_w - sprite_w } else { 0 };
|
||||||
|
let max_y = if safe_h > sprite_h { safe_h - sprite_h } else { 0 };
|
||||||
|
|
||||||
|
let mut x = 0;
|
||||||
|
let mut y = 0;
|
||||||
|
let mut found_safe_spot = false;
|
||||||
|
|
||||||
|
for _ in 0..RETRY_ATTEMPTS {
|
||||||
|
x = rng.gen_range(0..=max_x);
|
||||||
|
y = rng.gen_range(0..=max_y);
|
||||||
|
|
||||||
|
if let Some(deadzone) = &state.config.deadzone {
|
||||||
|
let sprite_rect = gdk::Rectangle::new(x, y, sprite_w, sprite_h);
|
||||||
|
if deadzone.intersect(&sprite_rect).is_none() {
|
||||||
|
found_safe_spot = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
found_safe_spot = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found_safe_spot {
|
||||||
|
println!("[ahfail] Could not find safe spot after retries, placing anyway");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[ahfail] Placing sprite at ({}, {})", x, y);
|
||||||
|
data.fixed.put(&image, x, y);
|
||||||
|
data.sprites.push(image);
|
||||||
|
|
||||||
|
let player = if let Some(p) = data.ready_players.pop() {
|
||||||
|
p
|
||||||
|
} else {
|
||||||
|
Self::create_player(audio_uri)
|
||||||
|
};
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
data.active_players.push(player);
|
||||||
|
|
||||||
|
let new_player = Self::create_player(audio_uri);
|
||||||
|
data.ready_players.push(new_player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
(*ctx.get_data_ptr()).signal_id = Some(signal_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn destroy(ctx: &WindowContext) {
|
||||||
|
if let Some(data) = ctx.take_data() {
|
||||||
|
if let Some(signal_id) = data.signal_id {
|
||||||
|
let error_label = ctx.get_error_label();
|
||||||
|
error_label.disconnect(signal_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for sprite in data.sprites {
|
||||||
|
sprite.destroy();
|
||||||
|
}
|
||||||
|
for player in data.active_players {
|
||||||
|
player.stop();
|
||||||
|
}
|
||||||
|
for player in data.ready_players {
|
||||||
|
player.stop();
|
||||||
|
}
|
||||||
|
data.fixed.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(gst::ClockTime::from_seconds(0));
|
||||||
|
}));
|
||||||
|
player.connect_error(|_, err| {
|
||||||
|
eprintln!("[ahfail] GStreamer Player Error: {}", err);
|
||||||
|
});
|
||||||
|
player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
193
src/lib.rs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod context;
|
||||||
|
pub mod state;
|
||||||
|
pub mod handler;
|
||||||
|
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{glib, gdk_pixbuf, gio};
|
||||||
|
use gtk::gdk_pixbuf::InterpType;
|
||||||
|
use gstreamer as gst;
|
||||||
|
use std::ffi::{c_void, CStr};
|
||||||
|
use glib::translate::from_glib_none;
|
||||||
|
use std::os::raw::{c_char, c_int, c_uint};
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
// Re-export types for external use (e.g. integration tests)
|
||||||
|
pub use config::{ModuleConfig, DEADZONE_ARG, DEADZONE_LONG, DEADZONE_DESC, DEADZONE_ARG_DESC};
|
||||||
|
pub use context::{Window, GtkLock, WindowContext, __IncompleteArrayField};
|
||||||
|
pub use state::{MODULE_STATE, WindowData, ModuleState};
|
||||||
|
pub use handler::WindowHandler;
|
||||||
|
|
||||||
|
// Scale factor to reduce the image size and avoid cropping/overlap
|
||||||
|
const SPRITE_SCALE: f64 = 0.6;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub static module_name: [c_char; 7] = [
|
||||||
|
b'a' as c_char,
|
||||||
|
b'h' as c_char,
|
||||||
|
b'f' as c_char,
|
||||||
|
b'a' as c_char,
|
||||||
|
b'i' as c_char,
|
||||||
|
b'l' as c_char,
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub static module_major_version: c_uint = 4;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub static module_minor_version: c_uint = 0;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub static mut module_entries: [glib::ffi::GOptionEntry; 3] = [
|
||||||
|
glib::ffi::GOptionEntry {
|
||||||
|
long_name: DEADZONE_LONG.as_ptr() as *const c_char,
|
||||||
|
short_name: 0,
|
||||||
|
flags: 0,
|
||||||
|
arg: glib::ffi::G_OPTION_ARG_STRING,
|
||||||
|
arg_data: unsafe { &raw mut DEADZONE_ARG as *mut _ },
|
||||||
|
description: DEADZONE_DESC.as_ptr() as *const c_char,
|
||||||
|
arg_description: DEADZONE_ARG_DESC.as_ptr() as *const c_char
|
||||||
|
},
|
||||||
|
glib::ffi::GOptionEntry { long_name: ptr::null(), short_name: 0, flags: 0, arg: 0, arg_data: ptr::null_mut(), description: ptr::null(), arg_description: ptr::null() },
|
||||||
|
glib::ffi::GOptionEntry { long_name: ptr::null(), short_name: 0, flags: 0, arg: 0, arg_data: ptr::null_mut(), description: ptr::null(), arg_description: ptr::null() },
|
||||||
|
];
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn ahfail_get_resource() -> *mut gio::ffi::GResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for tests to inspect window data
|
||||||
|
/// # Safety
|
||||||
|
/// `ctx` must be a valid Window pointer.
|
||||||
|
pub unsafe fn get_window_data_ref(ctx: *mut Window) -> Option<&'static state::WindowData> {
|
||||||
|
let ptr = *(*ctx).module_data.as_ptr();
|
||||||
|
if ptr.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&*(ptr as *mut state::WindowData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// This function is called by the C host.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn on_activation(_gtklock: *mut GtkLock, _id: c_int) {
|
||||||
|
println!("[ahfail] on_activation called");
|
||||||
|
if let Err(e) = gtk::init() {
|
||||||
|
eprintln!("Failed to initialize GTK bindings: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Err(e) = gst::init() {
|
||||||
|
eprintln!("Failed to initialize GStreamer: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resource_ptr = ahfail_get_resource();
|
||||||
|
if !resource_ptr.is_null() {
|
||||||
|
let resource = from_glib_none::<_, gio::Resource>(resource_ptr);
|
||||||
|
gio::resources_register(&resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load frames
|
||||||
|
let mut loaded_frames: Vec<gdk_pixbuf::Pixbuf> = Vec::new();
|
||||||
|
match gio::resources_enumerate_children("/ahfail/sprites", gio::ResourceLookupFlags::NONE) {
|
||||||
|
Ok(mut frames) => {
|
||||||
|
frames.sort();
|
||||||
|
for frame_path in frames {
|
||||||
|
let full_path = format!("/ahfail/sprites/{}", frame_path);
|
||||||
|
match gdk_pixbuf::Pixbuf::from_resource(&full_path) {
|
||||||
|
Ok(pixbuf) => {
|
||||||
|
let w = (pixbuf.width() as f64 * SPRITE_SCALE) as i32;
|
||||||
|
let h = (pixbuf.height() as f64 * SPRITE_SCALE) as i32;
|
||||||
|
if let Some(scaled) = pixbuf.scale_simple(w, h, InterpType::Bilinear) {
|
||||||
|
loaded_frames.push(scaled);
|
||||||
|
} else {
|
||||||
|
loaded_frames.push(pixbuf);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Failed to load sprite frame {}: {}", full_path, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Failed to enumerate sprites: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
let anim_opt = if !loaded_frames.is_empty() {
|
||||||
|
let first = &loaded_frames[0];
|
||||||
|
let anim = gdk_pixbuf::PixbufSimpleAnim::new(first.width(), first.height(), 12.0);
|
||||||
|
anim.set_loop(true);
|
||||||
|
for frame in loaded_frames {
|
||||||
|
anim.add_frame(&frame);
|
||||||
|
}
|
||||||
|
Some(anim.upcast())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = ModuleConfig::from_args();
|
||||||
|
|
||||||
|
MODULE_STATE.with(|state| {
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.animation = anim_opt;
|
||||||
|
state.audio_uri = Some("resource:///ahfail/audio/magic-word.mp3".to_string());
|
||||||
|
state.config = config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// This function is called by the C host.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn on_window_create(_gtklock: *mut GtkLock, ctx: *mut Window) {
|
||||||
|
if let Some(win_ctx) = WindowContext::new(ctx) {
|
||||||
|
WindowHandler::create(&win_ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// This function is called by the C host.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn on_window_destroy(_gtklock: *mut GtkLock, ctx: *mut Window) {
|
||||||
|
if let Some(win_ctx) = WindowContext::new(ctx) {
|
||||||
|
WindowHandler::destroy(&win_ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// This function is called by the C host.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn on_idle_hide(gtklock: *mut GtkLock) {
|
||||||
|
if gtklock.is_null() { return; }
|
||||||
|
let garray = (*gtklock).windows;
|
||||||
|
if !garray.is_null() {
|
||||||
|
let len = (*garray).len;
|
||||||
|
let data_ptr = (*garray).data as *mut *mut Window;
|
||||||
|
let windows = std::slice::from_raw_parts(data_ptr, len as usize);
|
||||||
|
|
||||||
|
for &window_ptr in windows {
|
||||||
|
on_window_destroy(ptr::null_mut(), window_ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// This function is called by the C host.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn on_focus_change(_gtklock: *mut GtkLock, _win: *mut Window, _old: *mut Window) {}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// This function is called by the C host.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn on_idle_show(_gtklock: *mut GtkLock) {}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// This function is called by the C host.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn g_module_unload(_module: *mut c_void) {
|
||||||
|
MODULE_STATE.with(|state| {
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.animation = None;
|
||||||
|
state.audio_uri = None;
|
||||||
|
state.config.deadzone = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
26
src/state.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use gtk::{glib, gdk_pixbuf};
|
||||||
|
use gstreamer_player as gst_player;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use crate::config::ModuleConfig;
|
||||||
|
|
||||||
|
pub struct ModuleState {
|
||||||
|
pub animation: Option<gdk_pixbuf::PixbufAnimation>,
|
||||||
|
pub audio_uri: Option<String>,
|
||||||
|
pub config: ModuleConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WindowData {
|
||||||
|
pub sprites: Vec<gtk::Image>,
|
||||||
|
pub active_players: Vec<gst_player::Player>,
|
||||||
|
pub ready_players: Vec<gst_player::Player>,
|
||||||
|
pub fixed: gtk::Fixed,
|
||||||
|
pub signal_id: Option<glib::SignalHandlerId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
pub static MODULE_STATE: RefCell<ModuleState> = const { RefCell::new(ModuleState {
|
||||||
|
animation: None,
|
||||||
|
audio_uri: None,
|
||||||
|
config: ModuleConfig { deadzone: None },
|
||||||
|
}) };
|
||||||
|
}
|
||||||
12
src/utils/bench.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
pub fn time_execution<F, T>(name: &str, f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce() -> T,
|
||||||
|
{
|
||||||
|
let start = Instant::now();
|
||||||
|
let result = f();
|
||||||
|
let duration = start.elapsed();
|
||||||
|
println!("[benchmark] {}: {:?}", name, duration);
|
||||||
|
result
|
||||||
|
}
|
||||||
299
tests/ahfail_tests.rs
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
use ahfail_module::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{glib, gdk_pixbuf, gdk};
|
||||||
|
use gstreamer as gst;
|
||||||
|
use std::ptr;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use gtk::glib::translate::{ToGlibPtr, Stash};
|
||||||
|
|
||||||
|
// Mock implementation of the resource getter for tests
|
||||||
|
#[no_mangle]
|
||||||
|
extern "C" fn ahfail_get_resource() -> *mut gtk::gio::ffi::GResource {
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper struct to simulate the C flexible array member allocation
|
||||||
|
#[repr(C)]
|
||||||
|
struct WindowWithStorage {
|
||||||
|
window: Window,
|
||||||
|
// Reserve space for the 'module_data' flexible array (1 pointer)
|
||||||
|
storage: *mut c_void,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_test_environment() {
|
||||||
|
static ONCE: std::sync::Once = std::sync::Once::new();
|
||||||
|
ONCE.call_once(|| {
|
||||||
|
let _ = gtk::init();
|
||||||
|
let _ = gst::init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_events() {
|
||||||
|
while gtk::events_pending() {
|
||||||
|
gtk::main_iteration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_mock_context() -> (WindowWithStorage, gtk::Window, gtk::Label, gtk::Overlay, gdk::Display) {
|
||||||
|
setup_test_environment();
|
||||||
|
|
||||||
|
let error_label = gtk::Label::new(Some(""));
|
||||||
|
let overlay = gtk::Overlay::new();
|
||||||
|
let window_widget = gtk::Window::new(gtk::WindowType::Toplevel);
|
||||||
|
let display = gdk::Display::default().expect("No display available for testing");
|
||||||
|
let monitor = display.monitor(0).expect("No monitor available");
|
||||||
|
|
||||||
|
// Explicitly type the Stash to avoid ambiguity
|
||||||
|
let monitor_stash: Stash<*mut gdk::ffi::GdkMonitor, _> = monitor.to_glib_none();
|
||||||
|
let window_stash: Stash<*mut gtk::ffi::GtkWindow, _> = window_widget.to_glib_none();
|
||||||
|
let overlay_stash: Stash<*mut gtk::ffi::GtkOverlay, _> = overlay.to_glib_none();
|
||||||
|
let label_stash: Stash<*mut gtk::ffi::GtkLabel, _> = error_label.to_glib_none();
|
||||||
|
|
||||||
|
let win = Window {
|
||||||
|
monitor: monitor_stash.0,
|
||||||
|
window: window_stash.0 as *mut gtk::ffi::GtkWidget,
|
||||||
|
overlay: overlay_stash.0,
|
||||||
|
window_box: ptr::null_mut(),
|
||||||
|
body_revealer: ptr::null_mut(),
|
||||||
|
body_grid: ptr::null_mut(),
|
||||||
|
input_label: ptr::null_mut(),
|
||||||
|
input_field: ptr::null_mut(),
|
||||||
|
message_revealer: ptr::null_mut(),
|
||||||
|
message_scrolled_window: ptr::null_mut(),
|
||||||
|
message_box: ptr::null_mut(),
|
||||||
|
unlock_button: ptr::null_mut(),
|
||||||
|
error_label: label_stash.0 as *mut gtk::ffi::GtkWidget,
|
||||||
|
warning_label: ptr::null_mut(),
|
||||||
|
info_box: ptr::null_mut(),
|
||||||
|
time_box: ptr::null_mut(),
|
||||||
|
clock_label: ptr::null_mut(),
|
||||||
|
date_label: ptr::null_mut(),
|
||||||
|
module_data: __IncompleteArrayField::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let storage = WindowWithStorage {
|
||||||
|
window: win,
|
||||||
|
storage: ptr::null_mut(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(storage, window_widget, error_label, overlay, display)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_test_state() {
|
||||||
|
let pixbuf = gdk_pixbuf::Pixbuf::new(gdk_pixbuf::Colorspace::Rgb, false, 8, 1, 1).unwrap();
|
||||||
|
let anim = gdk_pixbuf::PixbufSimpleAnim::new(1, 1, 1.0);
|
||||||
|
anim.add_frame(&pixbuf);
|
||||||
|
|
||||||
|
// Use a minimal valid WAV (silent) to avoid GStreamer errors
|
||||||
|
let wav_base64 = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQEAAAAA";
|
||||||
|
let data_uri = format!("data:audio/wav;base64,{}", wav_base64);
|
||||||
|
|
||||||
|
MODULE_STATE.with(|state| {
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
state.animation = Some(anim.upcast());
|
||||||
|
state.audio_uri = Some(data_uri);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_01_initialization() {
|
||||||
|
unsafe {
|
||||||
|
on_activation(ptr::null_mut(), 0);
|
||||||
|
}
|
||||||
|
MODULE_STATE.with(|state| {
|
||||||
|
let state = state.borrow();
|
||||||
|
// It should have set the audio URI to the default
|
||||||
|
assert_eq!(state.audio_uri.as_deref(), Some("resource:///ahfail/audio/magic-word.mp3"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_02_window_create_null() {
|
||||||
|
unsafe {
|
||||||
|
on_window_create(ptr::null_mut(), ptr::null_mut());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_03_window_create_valid() {
|
||||||
|
let (mut win_storage, _w, _l, _o, _) = create_mock_context();
|
||||||
|
let ctx_ptr = &mut win_storage.window as *mut Window;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
on_window_create(ptr::null_mut(), ctx_ptr);
|
||||||
|
let data = get_window_data_ref(ctx_ptr);
|
||||||
|
assert!(data.is_some(), "WindowData should be initialized");
|
||||||
|
|
||||||
|
on_window_destroy(ptr::null_mut(), ctx_ptr);
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_04_trigger_effect_sprite() {
|
||||||
|
let (mut win_storage, _w, label, _o, _) = create_mock_context();
|
||||||
|
let ctx_ptr = &mut win_storage.window as *mut Window;
|
||||||
|
inject_test_state();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
on_window_create(ptr::null_mut(), ctx_ptr);
|
||||||
|
label.set_text("Error");
|
||||||
|
flush_events();
|
||||||
|
|
||||||
|
let data = get_window_data_ref(ctx_ptr).unwrap();
|
||||||
|
assert_eq!(data.sprites.len(), 1);
|
||||||
|
|
||||||
|
on_window_destroy(ptr::null_mut(), ctx_ptr);
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_05_trigger_effect_audio() {
|
||||||
|
let (mut win_storage, _w, label, _o, _) = create_mock_context();
|
||||||
|
let ctx_ptr = &mut win_storage.window as *mut Window;
|
||||||
|
inject_test_state();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
on_window_create(ptr::null_mut(), ctx_ptr);
|
||||||
|
label.set_text("Error");
|
||||||
|
flush_events();
|
||||||
|
|
||||||
|
let data = get_window_data_ref(ctx_ptr).unwrap();
|
||||||
|
assert_eq!(data.active_players.len(), 1);
|
||||||
|
|
||||||
|
on_window_destroy(ptr::null_mut(), ctx_ptr);
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_06_multiple_triggers() {
|
||||||
|
let (mut win_storage, _w, label, _o, _) = create_mock_context();
|
||||||
|
let ctx_ptr = &mut win_storage.window as *mut Window;
|
||||||
|
inject_test_state();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
on_window_create(ptr::null_mut(), ctx_ptr);
|
||||||
|
for _ in 0..5 {
|
||||||
|
label.set_text("Error");
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = get_window_data_ref(ctx_ptr).unwrap();
|
||||||
|
assert_eq!(data.sprites.len(), 5);
|
||||||
|
assert_eq!(data.active_players.len(), 5);
|
||||||
|
|
||||||
|
on_window_destroy(ptr::null_mut(), ctx_ptr);
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_07_window_cleanup() {
|
||||||
|
let (mut win_storage, _w, label, _o, _) = create_mock_context();
|
||||||
|
let ctx_ptr = &mut win_storage.window as *mut Window;
|
||||||
|
inject_test_state();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
on_window_create(ptr::null_mut(), ctx_ptr);
|
||||||
|
label.set_text("Error");
|
||||||
|
flush_events();
|
||||||
|
|
||||||
|
assert!(get_window_data_ref(ctx_ptr).is_some());
|
||||||
|
on_window_destroy(ptr::null_mut(), ctx_ptr);
|
||||||
|
assert!(get_window_data_ref(ctx_ptr).is_none());
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_08_multiple_windows() {
|
||||||
|
let (mut win1, _w1, label1, _o1, _) = create_mock_context();
|
||||||
|
let (mut win2, _w2, label2, _o2, _) = create_mock_context();
|
||||||
|
let ctx1 = &mut win1.window as *mut Window;
|
||||||
|
let ctx2 = &mut win2.window as *mut Window;
|
||||||
|
inject_test_state();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
on_window_create(ptr::null_mut(), ctx1);
|
||||||
|
on_window_create(ptr::null_mut(), ctx2);
|
||||||
|
|
||||||
|
label1.set_text("E1");
|
||||||
|
flush_events();
|
||||||
|
|
||||||
|
label2.set_text("E2");
|
||||||
|
label2.set_text("E2 again");
|
||||||
|
flush_events();
|
||||||
|
|
||||||
|
let data1 = get_window_data_ref(ctx1).unwrap();
|
||||||
|
let data2 = get_window_data_ref(ctx2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(data1.sprites.len(), 1);
|
||||||
|
assert_eq!(data2.sprites.len(), 2);
|
||||||
|
|
||||||
|
on_window_destroy(ptr::null_mut(), ctx1);
|
||||||
|
on_window_destroy(ptr::null_mut(), ctx2);
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_09_idle_hide_cleanup() {
|
||||||
|
let (mut win_storage, _w, label, _o, _) = create_mock_context();
|
||||||
|
let ctx_ptr = &mut win_storage.window as *mut Window;
|
||||||
|
inject_test_state();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
on_window_create(ptr::null_mut(), ctx_ptr);
|
||||||
|
label.set_text("Error");
|
||||||
|
flush_events();
|
||||||
|
|
||||||
|
// Mock GtkLock struct
|
||||||
|
let mut windows_array = glib::ffi::g_array_new(0, 0, std::mem::size_of::<*mut Window>() as u32);
|
||||||
|
glib::ffi::g_array_append_vals(windows_array, &ctx_ptr as *const _ as *const c_void, 1);
|
||||||
|
|
||||||
|
let mut lock = GtkLock {
|
||||||
|
windows: windows_array,
|
||||||
|
};
|
||||||
|
|
||||||
|
on_idle_hide(&mut lock);
|
||||||
|
|
||||||
|
assert!(get_window_data_ref(ctx_ptr).is_none());
|
||||||
|
glib::ffi::g_array_free(windows_array, 1);
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_test_10_module_unload() {
|
||||||
|
inject_test_state();
|
||||||
|
MODULE_STATE.with(|state| {
|
||||||
|
assert!(state.borrow().audio_uri.is_some());
|
||||||
|
});
|
||||||
|
unsafe {
|
||||||
|
g_module_unload(ptr::null_mut());
|
||||||
|
}
|
||||||
|
MODULE_STATE.with(|state| {
|
||||||
|
let state = state.borrow();
|
||||||
|
assert!(state.animation.is_none());
|
||||||
|
assert!(state.audio_uri.is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_all_tests_sequentially() {
|
||||||
|
println!("Running test 01...");
|
||||||
|
run_test_01_initialization();
|
||||||
|
println!("Running test 02...");
|
||||||
|
run_test_02_window_create_null();
|
||||||
|
println!("Running test 03...");
|
||||||
|
run_test_03_window_create_valid();
|
||||||
|
println!("Running test 04...");
|
||||||
|
run_test_04_trigger_effect_sprite();
|
||||||
|
println!("Running test 05...");
|
||||||
|
run_test_05_trigger_effect_audio();
|
||||||
|
println!("Running test 06...");
|
||||||
|
run_test_06_multiple_triggers();
|
||||||
|
println!("Running test 07...");
|
||||||
|
run_test_07_window_cleanup();
|
||||||
|
println!("Running test 08...");
|
||||||
|
run_test_08_multiple_windows();
|
||||||
|
println!("Running test 09...");
|
||||||
|
run_test_09_idle_hide_cleanup();
|
||||||
|
println!("Running test 10...");
|
||||||
|
run_test_10_module_unload();
|
||||||
|
|
||||||
|
// Final flush
|
||||||
|
flush_events();
|
||||||
|
}
|
||||||
57
tests/benchmarks.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{gdk, gdk_pixbuf};
|
||||||
|
use gstreamer as gst;
|
||||||
|
use gstreamer_player as gst_player;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod benchmarks {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
|
||||||
|
fn init() {
|
||||||
|
INIT.call_once(|| {
|
||||||
|
gtk::init().unwrap();
|
||||||
|
gst::init().unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bench_pixbuf_loading() {
|
||||||
|
init();
|
||||||
|
let start = Instant::now();
|
||||||
|
// Create a 1x1 pixbuf to simulate loading
|
||||||
|
let _pixbuf = gdk_pixbuf::Pixbuf::new(gdk_pixbuf::Colorspace::Rgb, false, 8, 220, 220).unwrap();
|
||||||
|
println!("Pixbuf creation: {:?}", start.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bench_animation_creation() {
|
||||||
|
init();
|
||||||
|
let start = Instant::now();
|
||||||
|
let _anim = gdk_pixbuf::PixbufSimpleAnim::new(220, 220, 12.0);
|
||||||
|
println!("Animation creation: {:?}", start.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bench_player_creation() {
|
||||||
|
init();
|
||||||
|
let start = Instant::now();
|
||||||
|
let _player = gst_player::Player::new(None, None);
|
||||||
|
println!("GstPlayer creation: {:?}", start.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bench_coord_calculation() {
|
||||||
|
use rand::Rng;
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
for _ in 0..1000 {
|
||||||
|
let _x = rng.gen_range(0..1920);
|
||||||
|
let _y = rng.gen_range(0..1080);
|
||||||
|
}
|
||||||
|
println!("1000 RNG calculations: {:?}", start.elapsed());
|
||||||
|
}
|
||||||
|
}
|
||||||
18
tests/module_test.c
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "ahfail/module.h"
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
if (module_name[0] == '\0') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_activation(NULL, 42);
|
||||||
|
on_window_create(NULL, NULL);
|
||||||
|
on_focus_change(NULL, NULL, NULL);
|
||||||
|
on_idle_show(NULL);
|
||||||
|
on_idle_hide(NULL);
|
||||||
|
on_window_destroy(NULL, NULL);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||