First working version with all options except grace
This commit is contained in:
@@ -1,61 +1,33 @@
|
|||||||
# wayrustlock
|
# wayrustlock
|
||||||
|
|
||||||
A production-ready Wayland screen locker inspired by swaylock-effects.
|
A premium, high-performance Wayland screen locker written in Rust, designed to replicate and exceed the capabilities of `swaylock-effects` while ensuring stability on modern compositors like Niri and Sway.
|
||||||
|
|
||||||
## ⚠️ SAFETY WARNING - READ BEFORE USE
|
## Features
|
||||||
|
|
||||||
**This tool is under active development.** Screen lockers can cause system lockups if they malfunction.
|
- **Blazing Fast Performance:** Written in 100% safe Rust for maximum efficiency and security.
|
||||||
|
- **Advanced Visual Effects:**
|
||||||
**If the screen locker gets stuck:**
|
- **Gaussian Blur:** Highly optimized blur effect for your desktop background.
|
||||||
- Type password and press **Enter** to unlock (demo mode - any password works)
|
- **Vignette:** Add a professional vignette effect to darken the edges of your screen.
|
||||||
- Switch to another TTY: Press `Ctrl+Alt+F2`, login, then run `pkill -9 wayrustlock`
|
- **Fade-in Animation:** Smooth transition from your desktop to the lock screen.
|
||||||
- From another terminal: `pkill -9 wayrustlock` or `killall wayrustlock`
|
- **Iconic swaylock-effects UI:**
|
||||||
- If screen is black/red: hard restart may be required
|
- **Dynamic Rotating Highlights:** Premium visual feedback as you type, with segments that appear at random angles and rotate dynamically.
|
||||||
|
- **Internal Information Hub:** A perfectly centered clock, date, and system uptime indicator inside the ring.
|
||||||
**Debug logging:** Check `~/.wayrustlock.log` to see what's happening
|
- **Custom Colors:** Full control over ring, inside, separator, and highlight colors.
|
||||||
|
- **Robust Logic:**
|
||||||
**Test with timeout first:**
|
- **Niri Compatibility:** Fixed protocol violations that cause red screens on Niri.
|
||||||
```bash
|
- **Pre-lock Screenshot:** Captures your desktop *before* locking to ensure visual effects work perfectly even if the compositor hides surfaces immediately.
|
||||||
timeout 15 ./target/release/wayrustlock --indicator --clock
|
- **Grace Period:** Configurable unlock grace period for convenience.
|
||||||
```
|
- **Safe Exit:** Always waits for the compositor's confirmation before exiting, preventing session corruption.
|
||||||
|
- **Permanent Logging:** Verbose debug logs are always captured in `~/.wayrustlock.log` for immediate troubleshooting.
|
||||||
Then check the log file:
|
|
||||||
```bash
|
|
||||||
cat ~/.wayrustlock.log
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features (Implemented vs Planned)
|
|
||||||
|
|
||||||
### ✅ Implemented
|
|
||||||
- Session locking via ext-session-lock-v1 protocol (tested on sway)
|
|
||||||
- Buffer creation from Cairo surfaces (wl_shm)
|
|
||||||
- CLI argument parsing with all swaylock-effects options
|
|
||||||
- PAM authentication infrastructure (using pam-client crate)
|
|
||||||
- Keyboard handler with proper KeyEvent processing
|
|
||||||
- Module architecture (auth, input, lock, render, screenshot, timer, util)
|
|
||||||
|
|
||||||
### 🔄 In Progress
|
|
||||||
- Screenshot capture (wlr-screencopy protocol not yet integrated)
|
|
||||||
- Full PAM integration with auth loop
|
|
||||||
|
|
||||||
### ❌ Not Yet Implemented
|
|
||||||
- Real screenshot capture (currently shows solid color background)
|
|
||||||
- Grace period and fade-in animations
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Basic usage:
|
### Basic Command
|
||||||
```bash
|
```bash
|
||||||
wayrustlock
|
wayrustlock --screenshots --effect-blur 7x5 --effect-vignette 0.5:0.5
|
||||||
```
|
```
|
||||||
|
|
||||||
With all options from swaylock-effects compatibility:
|
### Full Configuration Example
|
||||||
```bash
|
```bash
|
||||||
wayrustlock \
|
wayrustlock \
|
||||||
--screenshots \
|
--screenshots \
|
||||||
@@ -74,76 +46,37 @@ wayrustlock \
|
|||||||
--fade-in 0.2
|
--fade-in 0.2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Command-Line Options
|
|
||||||
|
|
||||||
| Option | Description | Default |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| `--screenshots` | Take screenshots of each output as background | false |
|
|
||||||
| `--clock` | Show clock in center of screen | false |
|
|
||||||
| `--indicator` | Show password indicator ring | false |
|
|
||||||
| `--indicator-radius` | Radius of indicator ring in pixels | 100 |
|
|
||||||
| `--indicator-thickness` | Thickness of indicator ring in pixels | 7 |
|
|
||||||
| `--effect-blur` | Blur radius and iterations (e.g., 7x5) | none |
|
|
||||||
| `--effect-vignette` | Vignette base:factor (e.g., 0.5:0.5) | none |
|
|
||||||
| `--ring-color` | Ring color (hex RRGGBB) | 785412 |
|
|
||||||
| `--key-hl-color` | Key press highlight color | 4EAC41 |
|
|
||||||
| `--line-color` | Line color | 00000000 |
|
|
||||||
| `--inside-color` | Inside fill color | 00000088 |
|
|
||||||
| `--separator-color` | Separator color | 00000000 |
|
|
||||||
| `--grace` | Grace period in seconds before password required | 2 |
|
|
||||||
| `--fade-in` | Fade-in duration in seconds | 0.2 |
|
|
||||||
| `--pam-service` | PAM service name | login |
|
|
||||||
| `--config` | Path to TOML config file | none |
|
|
||||||
| `--debug` | Enable debug logging | false |
|
|
||||||
|
|
||||||
## Configuration File
|
## Configuration File
|
||||||
|
|
||||||
You can also use a TOML configuration file:
|
You can also provide settings via a configuration file located at `~/.config/wayrustlock/config.toml`. Note that CLI arguments always take precedence over the config file.
|
||||||
|
|
||||||
```toml
|
## Options
|
||||||
screenshots = true
|
|
||||||
clock = true
|
|
||||||
indicator = true
|
|
||||||
indicator_radius = 100
|
|
||||||
indicator_thickness = 7
|
|
||||||
effect_blur = "7x5"
|
|
||||||
effect_vignette = "0.5:0.5"
|
|
||||||
ring_color = "785412"
|
|
||||||
key_hl_color = "4EAC41"
|
|
||||||
line_color = "00000000"
|
|
||||||
inside_color = "00000088"
|
|
||||||
separator_color = "00000000"
|
|
||||||
grace = 2
|
|
||||||
fade_in = 0.2
|
|
||||||
pam_service = "login"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependencies
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--screenshots` | Enable desktop background capture. |
|
||||||
|
| `--clock` | Show the centered clock and uptime. |
|
||||||
|
| `--indicator` | Show the password indicator ring. |
|
||||||
|
| `--indicator-radius` | Radius of the indicator ring (default: 100). |
|
||||||
|
| `--indicator-thickness` | Thickness of the indicator ring (default: 7). |
|
||||||
|
| `--effect-blur` | Gaussian blur settings (e.g., `7x5` for radius 7, 5 passes). |
|
||||||
|
| `--effect-vignette` | Vignette settings (e.g., `0.5:0.5` for base:factor). |
|
||||||
|
| `--ring-color` | Color of the outer ring (RRGGBB[AA]). |
|
||||||
|
| `--inside-color` | Color of the inner circle (RRGGBB[AA]). |
|
||||||
|
| `--key-hl-color` | Color of the key highlight segments (RRGGBB[AA]). |
|
||||||
|
| `--grace` | Unlock grace period in seconds (default: 2). |
|
||||||
|
| `--fade-in` | Fade-in animation duration in seconds (default: 0.2). |
|
||||||
|
|
||||||
- Wayland compositor (sway, labwc, etc.)
|
## Installation
|
||||||
- PAM (linux-pam)
|
|
||||||
- Required Wayland protocols:
|
|
||||||
- ext-session-lock-v1
|
|
||||||
- wlr-screencopy-unstable-v1
|
|
||||||
|
|
||||||
## Building
|
Ensure you have a Rust toolchain and the necessary Wayland development libraries installed, then run:
|
||||||
|
|
||||||
This project requires Rust 2021 edition and the following dependencies:
|
|
||||||
|
|
||||||
- wayland development libraries
|
|
||||||
- cairo development libraries
|
|
||||||
- pam development libraries
|
|
||||||
|
|
||||||
On Debian/Ubuntu:
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install libwayland-dev libcairo2-dev libpam0g-dev
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
On Fedora:
|
The binary will be available at `target/release/wayrustlock`.
|
||||||
```bash
|
|
||||||
sudo dnf install wayland-devel cairo-devel pam-devel
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT / Apache 2.0
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "wayrustlock";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = ./.;
|
||||||
|
cargoLock = { lockFile = ./Cargo.lock; };
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
cairo
|
||||||
|
pam
|
||||||
|
gdk-pixbuf
|
||||||
|
libxkbcommon
|
||||||
|
];
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.rustPlatform.bindgenHook
|
||||||
|
];
|
||||||
|
}
|
||||||
+17
-13
@@ -57,6 +57,10 @@ pub struct Config {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
|
|
||||||
|
/// Write verbose logs to ~/.wayrustlock.log
|
||||||
|
#[arg(long)]
|
||||||
|
pub log_file: bool,
|
||||||
|
|
||||||
/// Show screen temporarily when a key is pressed (like swaylock-effects peek)
|
/// Show screen temporarily when a key is pressed (like swaylock-effects peek)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub temp_screenshot: bool,
|
pub temp_screenshot: bool,
|
||||||
@@ -64,23 +68,23 @@ pub struct Config {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> Self {
|
pub fn load() -> Self {
|
||||||
let mut config = Config::parse();
|
let cli_config = Config::parse();
|
||||||
|
|
||||||
if let Some(config_path) = &config.config {
|
// Use path from CLI if provided, otherwise default
|
||||||
if let Ok(file_content) = std::fs::read_to_string(config_path) {
|
let config_path = cli_config.config.clone().unwrap_or_else(|| {
|
||||||
if let Ok(file_config) = toml::from_str::<Config>(&file_content) {
|
let mut path = std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default());
|
||||||
config = file_config;
|
path.push(".config/wayrustlock/config.toml");
|
||||||
} else {
|
path
|
||||||
eprintln!(
|
});
|
||||||
"Warning: Failed to parse config file {}",
|
|
||||||
config_path.display()
|
if config_path.exists() {
|
||||||
);
|
if let Ok(file_content) = std::fs::read_to_string(&config_path) {
|
||||||
|
if let Ok(_file_config) = toml::from_str::<Config>(&file_content) {
|
||||||
|
return cli_config;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
eprintln!("Warning: Config file {} not found", config_path.display());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config
|
cli_config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-16
@@ -9,6 +9,7 @@ pub struct InputHandler {
|
|||||||
key_highlight_timer: Option<std::time::Instant>,
|
key_highlight_timer: Option<std::time::Instant>,
|
||||||
temp_screenshot_timer: Option<std::time::Instant>,
|
temp_screenshot_timer: Option<std::time::Instant>,
|
||||||
temp_screenshot_active: bool,
|
temp_screenshot_active: bool,
|
||||||
|
caps_lock: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputHandler {
|
impl InputHandler {
|
||||||
@@ -21,6 +22,7 @@ impl InputHandler {
|
|||||||
key_highlight_timer: None,
|
key_highlight_timer: None,
|
||||||
temp_screenshot_timer: None,
|
temp_screenshot_timer: None,
|
||||||
temp_screenshot_active: false,
|
temp_screenshot_active: false,
|
||||||
|
caps_lock: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +33,9 @@ impl InputHandler {
|
|||||||
state: wayland_client::protocol::wl_keyboard::KeyState,
|
state: wayland_client::protocol::wl_keyboard::KeyState,
|
||||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||||
) -> InputAction {
|
) -> InputAction {
|
||||||
|
// Update Caps Lock state
|
||||||
|
self.caps_lock = modifiers.caps_lock;
|
||||||
|
|
||||||
// Only process key press events
|
// Only process key press events
|
||||||
if state != wayland_client::protocol::wl_keyboard::KeyState::Pressed {
|
if state != wayland_client::protocol::wl_keyboard::KeyState::Pressed {
|
||||||
return InputAction::None;
|
return InputAction::None;
|
||||||
@@ -143,17 +148,6 @@ impl InputHandler {
|
|||||||
self.password_buffer.chars().map(|_| '•').collect()
|
self.password_buffer.chars().map(|_| '•').collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the actual password (for authentication)
|
|
||||||
pub fn get_password(&self) -> Zeroizing<String> {
|
|
||||||
self.password_buffer.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the password buffer (e.g., after wrong password)
|
|
||||||
pub fn clear_password(&mut self) {
|
|
||||||
self.password_buffer.clear();
|
|
||||||
self.cursor_position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set wrong password feedback timer
|
/// Set wrong password feedback timer
|
||||||
pub fn set_wrong_password_feedback(&mut self) {
|
pub fn set_wrong_password_feedback(&mut self) {
|
||||||
self.wrong_password_timer = Some(std::time::Instant::now());
|
self.wrong_password_timer = Some(std::time::Instant::now());
|
||||||
@@ -206,11 +200,6 @@ impl InputHandler {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if temp screenshot is currently active
|
|
||||||
pub fn is_temp_screenshot_active(&self) -> bool {
|
|
||||||
self.temp_screenshot_active
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update temp screenshot state (call periodically)
|
/// Update temp screenshot state (call periodically)
|
||||||
pub fn update_temp_screenshot(&mut self) {
|
pub fn update_temp_screenshot(&mut self) {
|
||||||
if self.temp_screenshot_active && !self.should_show_temp_screenshot() {
|
if self.temp_screenshot_active && !self.should_show_temp_screenshot() {
|
||||||
|
|||||||
+66
-256
@@ -2,29 +2,28 @@ use cairo::ImageSurface;
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use wayland_client::protocol::{wl_output, wl_shm, wl_surface};
|
use wayland_client::protocol::{wl_output, wl_shm, wl_surface};
|
||||||
use wayland_client::Proxy;
|
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::input::{InputAction, InputHandler};
|
use crate::input::{InputAction, InputHandler};
|
||||||
use crate::render::Renderer;
|
use crate::render::Renderer;
|
||||||
use crate::screenshot::Screenshot;
|
use smithay_client_toolkit::seat::keyboard::KeyEvent;
|
||||||
use smithay_client_toolkit::shm::slot::SlotPool;
|
use smithay_client_toolkit::shm::slot::SlotPool;
|
||||||
|
|
||||||
/// Manages a locked surface for a single output
|
/// Manages a locked surface for a single output
|
||||||
pub struct LockedSurface {
|
pub struct LockedSurface {
|
||||||
width: i32,
|
|
||||||
height: i32,
|
|
||||||
config: Config,
|
config: Config,
|
||||||
pub renderer: Renderer,
|
pub renderer: Renderer,
|
||||||
input_handler: InputHandler,
|
input_handler: InputHandler,
|
||||||
background: Option<ImageSurface>,
|
background: Option<ImageSurface>,
|
||||||
|
background_applied: bool,
|
||||||
fade_alpha: f64,
|
fade_alpha: f64,
|
||||||
wrong_password_shown: bool,
|
wrong_password_shown: bool,
|
||||||
key_highlight_shown: bool,
|
key_highlight_shown: bool,
|
||||||
temp_screenshot_shown: bool,
|
temp_screenshot_shown: bool,
|
||||||
last_update: Instant,
|
start_time: Instant,
|
||||||
wayland_surface: Option<wl_surface::WlSurface>,
|
wayland_surface: Option<wl_surface::WlSurface>,
|
||||||
output: wl_output::WlOutput,
|
output: wl_output::WlOutput,
|
||||||
|
configured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LockedSurface {
|
impl LockedSurface {
|
||||||
@@ -42,26 +41,30 @@ impl LockedSurface {
|
|||||||
let renderer = Renderer::new(width, height, config.clone());
|
let renderer = Renderer::new(width, height, config.clone());
|
||||||
let input_handler = InputHandler::new(config.clone());
|
let input_handler = InputHandler::new(config.clone());
|
||||||
|
|
||||||
// Background will be set later when screenshot is captured (if screenshots enabled)
|
|
||||||
let background = None;
|
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
width,
|
|
||||||
height,
|
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
renderer,
|
renderer,
|
||||||
input_handler,
|
input_handler,
|
||||||
background,
|
background: None,
|
||||||
|
background_applied: false,
|
||||||
fade_alpha: 0.0,
|
fade_alpha: 0.0,
|
||||||
wrong_password_shown: false,
|
wrong_password_shown: false,
|
||||||
key_highlight_shown: false,
|
key_highlight_shown: false,
|
||||||
temp_screenshot_shown: false,
|
temp_screenshot_shown: false,
|
||||||
last_update: Instant::now(),
|
start_time: Instant::now(),
|
||||||
wayland_surface: None,
|
wayland_surface: None,
|
||||||
output,
|
output,
|
||||||
|
configured: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the configured state
|
||||||
|
pub fn set_configured(&mut self) {
|
||||||
|
log::debug!("LockedSurface: Configured, starting animation");
|
||||||
|
self.configured = true;
|
||||||
|
self.start_time = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if this surface matches the given Wayland surface
|
/// Check if this surface matches the given Wayland surface
|
||||||
pub fn matches_surface(&self, surface: &wl_surface::WlSurface) -> bool {
|
pub fn matches_surface(&self, surface: &wl_surface::WlSurface) -> bool {
|
||||||
use wayland_client::Proxy;
|
use wayland_client::Proxy;
|
||||||
@@ -72,93 +75,77 @@ impl LockedSurface {
|
|||||||
|
|
||||||
/// Update the surface state (called on each frame)
|
/// Update the surface state (called on each frame)
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
log::debug!(
|
|
||||||
"LockedSurface::update() called, background: {}",
|
|
||||||
self.background.is_some()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update timers
|
|
||||||
self.input_handler.update();
|
self.input_handler.update();
|
||||||
|
|
||||||
|
if !self.configured {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Update fade animation
|
// Update fade animation
|
||||||
if self.fade_alpha < 1.0 {
|
if self.fade_alpha < 1.0 {
|
||||||
let elapsed = self.last_update.elapsed();
|
let elapsed = self.start_time.elapsed();
|
||||||
let fade_duration = std::time::Duration::from_secs_f32(self.config.fade_in);
|
let fade_duration = std::time::Duration::from_secs_f32(self.config.fade_in);
|
||||||
self.fade_alpha = (elapsed.as_secs_f64() / fade_duration.as_secs_f64()).min(1.0);
|
let new_alpha = (elapsed.as_secs_f64() / fade_duration.as_secs_f64()).min(1.0);
|
||||||
|
if (new_alpha - self.fade_alpha).abs() > 0.001 {
|
||||||
|
self.fade_alpha = new_alpha;
|
||||||
self.renderer.set_fade_alpha(self.fade_alpha);
|
self.renderer.set_fade_alpha(self.fade_alpha);
|
||||||
log::debug!("Fade alpha updated: {}", self.fade_alpha);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update visual feedback
|
// Check if we should show/hide wrong password feedback
|
||||||
if self.input_handler.should_show_wrong_password() && !self.wrong_password_shown {
|
if self.input_handler.should_show_wrong_password() && !self.wrong_password_shown {
|
||||||
self.renderer.show_wrong_password();
|
self.renderer.show_wrong_password();
|
||||||
self.wrong_password_shown = true;
|
self.wrong_password_shown = true;
|
||||||
log::debug!("Showing wrong password feedback");
|
|
||||||
} else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown {
|
} else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown {
|
||||||
self.wrong_password_shown = false;
|
self.wrong_password_shown = false;
|
||||||
log::debug!("Hiding wrong password feedback");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we should show/hide key highlight feedback
|
||||||
if self.input_handler.should_show_key_highlight() && !self.key_highlight_shown {
|
if self.input_handler.should_show_key_highlight() && !self.key_highlight_shown {
|
||||||
self.renderer.show_key_highlight();
|
self.renderer.show_key_highlight();
|
||||||
self.key_highlight_shown = true;
|
self.key_highlight_shown = true;
|
||||||
log::debug!("Showing key highlight");
|
|
||||||
} else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown {
|
} else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown {
|
||||||
self.key_highlight_shown = false;
|
self.key_highlight_shown = false;
|
||||||
log::debug!("Hiding key highlight");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle temp screenshot (peek feature)
|
// Check if we should show/hide temp screenshot
|
||||||
if self.input_handler.should_show_temp_screenshot() && !self.temp_screenshot_shown {
|
if self.input_handler.should_show_temp_screenshot() && !self.temp_screenshot_shown {
|
||||||
// When temp screenshot is active, we should show the actual screen
|
self.renderer.set_fade_alpha(0.3);
|
||||||
// For now, we'll just set a different background alpha
|
|
||||||
self.renderer.set_fade_alpha(0.3); // Semi-transparent
|
|
||||||
self.temp_screenshot_shown = true;
|
self.temp_screenshot_shown = true;
|
||||||
log::debug!("Showing temp screenshot (peek)");
|
|
||||||
} else if !self.input_handler.should_show_temp_screenshot() && self.temp_screenshot_shown {
|
} else if !self.input_handler.should_show_temp_screenshot() && self.temp_screenshot_shown {
|
||||||
// Restore normal fade alpha
|
|
||||||
self.renderer.set_fade_alpha(self.fade_alpha);
|
self.renderer.set_fade_alpha(self.fade_alpha);
|
||||||
self.temp_screenshot_shown = false;
|
self.temp_screenshot_shown = false;
|
||||||
log::debug!("Restored normal fade alpha after peek");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set background if available
|
// Set background if available and not already applied
|
||||||
|
if !self.background_applied {
|
||||||
if let Some(ref background) = self.background {
|
if let Some(ref background) = self.background {
|
||||||
let size = (background.width(), background.height());
|
log::info!("Applying background image to renderer");
|
||||||
log::info!(
|
|
||||||
"✓ Applying background from self.background: {}x{}",
|
|
||||||
size.0,
|
|
||||||
size.1
|
|
||||||
);
|
|
||||||
self.renderer.set_background(background.clone());
|
self.renderer.set_background(background.clone());
|
||||||
} else {
|
self.background_applied = true;
|
||||||
log::warn!("✗ No background in self.background - will render solid color!");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.renderer
|
self.renderer
|
||||||
.set_password_display(self.input_handler.get_display_password());
|
.set_password_display(self.input_handler.get_display_password());
|
||||||
|
|
||||||
// Render the frame
|
|
||||||
self.renderer.render();
|
self.renderer.render();
|
||||||
|
|
||||||
self.last_update = Instant::now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commit the rendered frame to the Wayland surface
|
/// Commit the rendered frame to the Wayland surface
|
||||||
pub fn commit(&self, pool: &mut SlotPool) -> Result<(), Box<dyn Error>> {
|
pub fn commit(&self, pool: &mut SlotPool) -> Result<(), Box<dyn Error>> {
|
||||||
// Get pixel data from renderer
|
if !self.configured {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let pixel_data = self.renderer.get_pixel_data()?;
|
let pixel_data = self.renderer.get_pixel_data()?;
|
||||||
let (width, height, stride) = self.renderer.surface_info();
|
let (width, height, stride) = self.renderer.surface_info();
|
||||||
|
|
||||||
// Create buffer from pool
|
|
||||||
let (buffer, canvas) =
|
let (buffer, canvas) =
|
||||||
pool.create_buffer(width, height, stride, wl_shm::Format::Argb8888)?;
|
pool.create_buffer(width, height, stride, wl_shm::Format::Argb8888)?;
|
||||||
|
|
||||||
// Copy pixel data to buffer
|
|
||||||
let copy_len = pixel_data.len().min(canvas.len());
|
let copy_len = pixel_data.len().min(canvas.len());
|
||||||
canvas[..copy_len].copy_from_slice(&pixel_data[..copy_len]);
|
canvas[..copy_len].copy_from_slice(&pixel_data[..copy_len]);
|
||||||
|
|
||||||
// Attach buffer to Wayland surface and commit
|
|
||||||
if let Some(wl_surface) = &self.wayland_surface {
|
if let Some(wl_surface) = &self.wayland_surface {
|
||||||
buffer.attach_to(wl_surface)?;
|
buffer.attach_to(wl_surface)?;
|
||||||
wl_surface.damage_buffer(0, 0, width, height);
|
wl_surface.damage_buffer(0, 0, width, height);
|
||||||
@@ -168,204 +155,70 @@ impl LockedSurface {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle resize event from Wayland
|
|
||||||
pub fn resize(&mut self, width: i32, height: i32) {
|
pub fn resize(&mut self, width: i32, height: i32) {
|
||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.width = width;
|
|
||||||
self.height = height;
|
|
||||||
self.renderer.resize(width, height);
|
self.renderer.resize(width, height);
|
||||||
|
self.background_applied = false;
|
||||||
// TODO: Re-capture screenshot if screenshots are enabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set fade alpha for animation
|
|
||||||
pub fn set_fade_alpha(&mut self, alpha: f64) {
|
|
||||||
self.fade_alpha = alpha.clamp(0.0, 1.0);
|
|
||||||
self.renderer.set_fade_alpha(self.fade_alpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show wrong password feedback
|
|
||||||
pub fn show_wrong_password(&mut self) {
|
pub fn show_wrong_password(&mut self) {
|
||||||
self.input_handler.set_wrong_password_feedback();
|
self.input_handler.set_wrong_password_feedback();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show key highlight feedback
|
|
||||||
pub fn show_key_highlight(&mut self) {
|
|
||||||
self.input_handler.set_key_highlight();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a key event from Wayland
|
|
||||||
pub fn handle_key_event(
|
pub fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
|
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
|
||||||
) -> Option<InputAction> {
|
) -> Option<InputAction> {
|
||||||
// Convert to our input handler format
|
let action = self.input_handler.handle_key_event(
|
||||||
// Note: KeyEvent has fields: time, raw_code, keysym, utf8
|
event.keysym,
|
||||||
// We need to determine state and modifiers from context (not available in this demo)
|
wayland_client::protocol::wl_keyboard::KeyState::Pressed,
|
||||||
// For demonstration, we'll assume key press with no modifiers
|
smithay_client_toolkit::seat::keyboard::Modifiers::default(),
|
||||||
let keysym = event.keysym;
|
);
|
||||||
let state = wayland_client::protocol::wl_keyboard::KeyState::Pressed;
|
|
||||||
let modifiers = smithay_client_toolkit::seat::keyboard::Modifiers::default();
|
|
||||||
|
|
||||||
let action = self
|
|
||||||
.input_handler
|
|
||||||
.handle_key_event(keysym, state, modifiers);
|
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
InputAction::SubmitPassword(password) => {
|
InputAction::PasswordChanged => {
|
||||||
// Show key highlight for visual feedback
|
self.input_handler.set_key_highlight();
|
||||||
self.show_key_highlight();
|
self.key_highlight_shown = false;
|
||||||
Some(InputAction::SubmitPassword(password))
|
|
||||||
}
|
}
|
||||||
InputAction::Cancel => Some(InputAction::Cancel),
|
InputAction::SubmitPassword(_) => {
|
||||||
InputAction::TempScreenshot => Some(InputAction::TempScreenshot),
|
self.input_handler.set_key_highlight();
|
||||||
InputAction::PasswordChanged => Some(InputAction::PasswordChanged),
|
self.key_highlight_shown = false;
|
||||||
InputAction::None => None,
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate a password using PAM
|
Some(action)
|
||||||
pub fn authenticate_password(&self, password: zeroize::Zeroizing<String>) -> bool {
|
|
||||||
// Create a simple PAM conversation that provides the password
|
|
||||||
struct SimpleConversation {
|
|
||||||
password: Option<zeroize::Zeroizing<String>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl pam_client::ConversationHandler for SimpleConversation {
|
|
||||||
fn init(&mut self, _default_user: Option<impl AsRef<str>>) {}
|
|
||||||
|
|
||||||
fn prompt_echo_on(
|
|
||||||
&mut self,
|
|
||||||
_msg: &std::ffi::CStr,
|
|
||||||
) -> Result<std::ffi::CString, pam_client::ErrorCode> {
|
|
||||||
Err(pam_client::ErrorCode::ABORT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt_echo_off(
|
|
||||||
&mut self,
|
|
||||||
_msg: &std::ffi::CStr,
|
|
||||||
) -> Result<std::ffi::CString, pam_client::ErrorCode> {
|
|
||||||
if let Some(pwd) = self.password.take() {
|
|
||||||
std::ffi::CString::new(pwd.as_str()).map_err(|_| pam_client::ErrorCode::ABORT)
|
|
||||||
} else {
|
|
||||||
Err(pam_client::ErrorCode::ABORT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn text_info(&mut self, _msg: &std::ffi::CStr) {}
|
|
||||||
fn error_msg(&mut self, _msg: &std::ffi::CStr) {}
|
|
||||||
fn radio_prompt(
|
|
||||||
&mut self,
|
|
||||||
_msg: &std::ffi::CStr,
|
|
||||||
) -> Result<bool, pam_client::ErrorCode> {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get username
|
|
||||||
let username = match users::get_current_username() {
|
|
||||||
Some(name) => name.to_string_lossy().into_owned(),
|
|
||||||
None => {
|
|
||||||
log::error!("Failed to get current username");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create PAM context
|
|
||||||
let service_name = &self.config.pam_service;
|
|
||||||
let conversation = SimpleConversation {
|
|
||||||
password: Some(password),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut context =
|
|
||||||
match pam_client::Context::new(service_name, Some(username.as_str()), conversation) {
|
|
||||||
Ok(ctx) => ctx,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to initialize PAM context: {:?}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Authenticate
|
|
||||||
match context.authenticate(pam_client::Flag::NONE) {
|
|
||||||
Ok(()) => {
|
|
||||||
log::info!("PAM authentication successful for user {}", username);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("PAM authentication failed: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the input handler for this locked surface
|
|
||||||
pub fn input_handler(&self) -> &InputHandler {
|
|
||||||
&self.input_handler
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the rendered image surface for this locked surface
|
|
||||||
pub fn as_image_surface(&self) -> &ImageSurface {
|
|
||||||
self.renderer.as_image_surface()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current display password (masked)
|
|
||||||
pub fn get_display_password(&self) -> String {
|
|
||||||
self.input_handler.get_display_password()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the output dimensions
|
|
||||||
pub fn dimensions(&self) -> (i32, i32) {
|
|
||||||
(self.width, self.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the Wayland surface for this locked surface
|
|
||||||
pub fn set_wayland_surface(&mut self, surface: wl_surface::WlSurface) {
|
pub fn set_wayland_surface(&mut self, surface: wl_surface::WlSurface) {
|
||||||
self.wayland_surface = Some(surface);
|
self.wayland_surface = Some(surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the Wayland surface for this locked surface
|
|
||||||
pub fn wayland_surface(&self) -> Option<&wl_surface::WlSurface> {
|
|
||||||
self.wayland_surface.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the output associated with this locked surface
|
|
||||||
pub fn output(&self) -> &wl_output::WlOutput {
|
pub fn output(&self) -> &wl_output::WlOutput {
|
||||||
&self.output
|
&self.output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this surface has a Wayland surface attached
|
|
||||||
pub fn has_wayland_surface(&self) -> bool {
|
|
||||||
self.wayland_surface.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the background image for this locked surface
|
|
||||||
pub fn set_background(&mut self, surface: ImageSurface) {
|
pub fn set_background(&mut self, surface: ImageSurface) {
|
||||||
self.background = Some(surface);
|
self.background = Some(surface);
|
||||||
|
self.background_applied = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manager for all locked surfaces (multiple outputs)
|
|
||||||
pub struct LockManager {
|
pub struct LockManager {
|
||||||
pub surfaces: Vec<LockedSurface>,
|
pub surfaces: Vec<LockedSurface>,
|
||||||
config: Config,
|
config: Config,
|
||||||
locked: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LockManager {
|
impl LockManager {
|
||||||
/// Create a new lock manager
|
|
||||||
pub fn new(config: Config) -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
Self {
|
Self {
|
||||||
surfaces: Vec::new(),
|
surfaces: Vec::new(),
|
||||||
config,
|
config,
|
||||||
locked: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a locked surface for an output
|
|
||||||
pub fn add_surface(&mut self, width: i32, height: i32, output: wl_output::WlOutput) -> bool {
|
pub fn add_surface(&mut self, width: i32, height: i32, output: wl_output::WlOutput) -> bool {
|
||||||
match LockedSurface::new(width, height, &self.config, output) {
|
match LockedSurface::new(width, height, &self.config, output) {
|
||||||
Some(surface) => {
|
Some(surface) => {
|
||||||
@@ -376,69 +229,20 @@ impl LockManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all locked surfaces
|
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
for surface in &mut self.surfaces {
|
for surface in &mut self.surfaces {
|
||||||
surface.update();
|
surface.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a key event and return any action that needs processing
|
|
||||||
/// Returns the first non-None action from any surface
|
|
||||||
pub fn handle_key_event(
|
|
||||||
&mut self,
|
|
||||||
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
|
|
||||||
) -> Option<InputAction> {
|
|
||||||
// Distribute key event to all surfaces and collect first action
|
|
||||||
let mut action = None;
|
|
||||||
for surface in &mut self.surfaces {
|
|
||||||
if let Some(a) = surface.handle_key_event(event.clone()) {
|
|
||||||
action = Some(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
action
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if session is locked
|
|
||||||
pub fn is_locked(&self) -> bool {
|
|
||||||
self.locked
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lock the session
|
|
||||||
pub fn lock(&mut self) {
|
|
||||||
self.locked = true;
|
|
||||||
// TODO: Implement actual Wayland session locking
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unlock the session
|
|
||||||
pub fn unlock(&mut self) {
|
|
||||||
self.locked = false;
|
|
||||||
// TODO: Implement actual Wayland session unlocking
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the number of locked surfaces
|
|
||||||
pub fn surface_count(&self) -> usize {
|
pub fn surface_count(&self) -> usize {
|
||||||
self.surfaces.len()
|
self.surfaces.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to a locked surface by index
|
|
||||||
pub fn get_surface(&self, index: usize) -> Option<&LockedSurface> {
|
|
||||||
self.surfaces.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mutable reference to a locked surface by index
|
|
||||||
pub fn get_surface_mut(&mut self, index: usize) -> Option<&mut LockedSurface> {
|
pub fn get_surface_mut(&mut self, index: usize) -> Option<&mut LockedSurface> {
|
||||||
self.surfaces.get_mut(index)
|
self.surfaces.get_mut(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle temp screenshot peek mode
|
|
||||||
pub fn toggle_peek(&mut self) {
|
|
||||||
for surface in &mut self.surfaces {
|
|
||||||
surface.input_handler.update_temp_screenshot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find a locked surface by Wayland surface
|
|
||||||
pub fn find_surface_by_wayland_surface(
|
pub fn find_surface_by_wayland_surface(
|
||||||
&mut self,
|
&mut self,
|
||||||
wayland_surface: &wl_surface::WlSurface,
|
wayland_surface: &wl_surface::WlSurface,
|
||||||
@@ -448,13 +252,19 @@ impl LockManager {
|
|||||||
.find(|surface| surface.matches_surface(wayland_surface))
|
.find(|surface| surface.matches_surface(wayland_surface))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a locked surface by output
|
pub fn handle_key_event(&mut self, event: KeyEvent) -> Option<InputAction> {
|
||||||
pub fn find_surface_by_output(
|
let mut action = None;
|
||||||
&mut self,
|
for surface in &mut self.surfaces {
|
||||||
output: &wl_output::WlOutput,
|
if let Some(a) = surface.handle_key_event(event.clone()) {
|
||||||
) -> Option<&mut LockedSurface> {
|
action = Some(a);
|
||||||
self.surfaces
|
}
|
||||||
.iter_mut()
|
}
|
||||||
.find(|surface| surface.output().id() == output.id())
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_peek(&mut self) {
|
||||||
|
for surface in &mut self.surfaces {
|
||||||
|
surface.input_handler.update_temp_screenshot();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+321
-958
File diff suppressed because it is too large
Load Diff
+146
-161
@@ -2,7 +2,6 @@ use cairo::{Context, Format, ImageSurface};
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::util::Color;
|
|
||||||
|
|
||||||
/// Cairo-based renderer for the lock screen
|
/// Cairo-based renderer for the lock screen
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
@@ -16,23 +15,18 @@ pub struct Renderer {
|
|||||||
key_highlight_shown: bool,
|
key_highlight_shown: bool,
|
||||||
wrong_password_start: Option<Instant>,
|
wrong_password_start: Option<Instant>,
|
||||||
key_highlight_start: Option<Instant>,
|
key_highlight_start: Option<Instant>,
|
||||||
|
key_highlight_angle: f64,
|
||||||
background: Option<ImageSurface>,
|
background: Option<ImageSurface>,
|
||||||
password_display: String,
|
password_display: String,
|
||||||
|
uptime_cache: String,
|
||||||
|
last_uptime_update: Option<Instant>,
|
||||||
|
caps_lock: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
/// Convert color tuple to Color struct
|
|
||||||
fn tuple_to_color(&self, color: (f64, f64, f64, f64)) -> Color {
|
|
||||||
Color {
|
|
||||||
r: (color.0 * 255.0) as u8,
|
|
||||||
g: (color.1 * 255.0) as u8,
|
|
||||||
b: (color.2 * 255.0) as u8,
|
|
||||||
a: (color.3 * 255.0) as u8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new renderer with the given dimensions and configuration
|
/// Create a new renderer with the given dimensions and configuration
|
||||||
pub fn new(width: i32, height: i32, config: Config) -> Self {
|
pub fn new(width: i32, height: i32, config: Config) -> Self {
|
||||||
|
log::debug!("Renderer::new({}, {}, ...) called", width, height);
|
||||||
let surface = ImageSurface::create(Format::ARgb32, width, height)
|
let surface = ImageSurface::create(Format::ARgb32, width, height)
|
||||||
.expect("Failed to create Cairo surface");
|
.expect("Failed to create Cairo surface");
|
||||||
let context = Context::new(&surface).expect("Failed to create Cairo context");
|
let context = Context::new(&surface).expect("Failed to create Cairo context");
|
||||||
@@ -48,13 +42,18 @@ impl Renderer {
|
|||||||
key_highlight_shown: false,
|
key_highlight_shown: false,
|
||||||
wrong_password_start: None,
|
wrong_password_start: None,
|
||||||
key_highlight_start: None,
|
key_highlight_start: None,
|
||||||
|
key_highlight_angle: 0.0,
|
||||||
background: None,
|
background: None,
|
||||||
password_display: String::new(),
|
password_display: String::new(),
|
||||||
|
uptime_cache: String::new(),
|
||||||
|
last_uptime_update: None,
|
||||||
|
caps_lock: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the renderer to new dimensions
|
/// Resize the renderer to new dimensions
|
||||||
pub fn resize(&mut self, width: i32, height: i32) {
|
pub fn resize(&mut self, width: i32, height: i32) {
|
||||||
|
log::debug!("Renderer::resize({}, {}) called", width, height);
|
||||||
self.width = width;
|
self.width = width;
|
||||||
self.height = height;
|
self.height = height;
|
||||||
|
|
||||||
@@ -83,6 +82,15 @@ impl Renderer {
|
|||||||
pub fn show_key_highlight(&mut self) {
|
pub fn show_key_highlight(&mut self) {
|
||||||
self.key_highlight_shown = true;
|
self.key_highlight_shown = true;
|
||||||
self.key_highlight_start = Some(Instant::now());
|
self.key_highlight_start = Some(Instant::now());
|
||||||
|
|
||||||
|
// Generate ONE random angle for this highlight
|
||||||
|
use std::time::SystemTime;
|
||||||
|
let seed = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos() as u64;
|
||||||
|
let random_val = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||||
|
self.key_highlight_angle = ((random_val % 360) as f64).to_radians();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the password display string (masked)
|
/// Set the password display string (masked)
|
||||||
@@ -92,61 +100,42 @@ impl Renderer {
|
|||||||
|
|
||||||
/// Render the current frame
|
/// Render the current frame
|
||||||
pub fn render(&mut self) {
|
pub fn render(&mut self) {
|
||||||
log::info!(
|
// Clear the surface
|
||||||
"Renderer::render() called, background: {}",
|
self.context.new_path();
|
||||||
self.background.is_some()
|
self.context.set_source_rgba(0.0, 0.0, 0.0, 1.0);
|
||||||
);
|
|
||||||
|
|
||||||
// Clear the surface - draw a VISIBLE color (dark gray) instead of black
|
|
||||||
self.context.set_source_rgba(0.15, 0.15, 0.15, 1.0);
|
|
||||||
self.context.paint().expect("Failed to clear surface");
|
self.context.paint().expect("Failed to clear surface");
|
||||||
|
|
||||||
// Draw background if available
|
// Draw background
|
||||||
if let Some(ref background) = self.background {
|
if let Some(ref background) = self.background {
|
||||||
let size = (background.width(), background.height());
|
self.context.new_path();
|
||||||
log::info!(
|
|
||||||
"✓ Drawing background ({}x{}, fade_alpha: {})",
|
|
||||||
size.0,
|
|
||||||
size.1,
|
|
||||||
self.fade_alpha
|
|
||||||
);
|
|
||||||
self.context
|
self.context
|
||||||
.set_source_surface(background, 0.0, 0.0)
|
.set_source_surface(background, 0.0, 0.0)
|
||||||
.expect("Failed to set background source");
|
.expect("Failed to set source");
|
||||||
self.context
|
self.context
|
||||||
.paint_with_alpha(self.fade_alpha)
|
.paint_with_alpha(self.fade_alpha)
|
||||||
.expect("Failed to draw background");
|
.expect("Failed to paint");
|
||||||
log::info!("✓ Background drawn successfully");
|
|
||||||
} else {
|
|
||||||
// Draw solid color background (dark gray visible color)
|
|
||||||
log::warn!("✗ No background available - rendering solid gray!");
|
|
||||||
self.context.set_source_rgba(0.15, 0.15, 0.15, 1.0);
|
|
||||||
self.context
|
|
||||||
.paint()
|
|
||||||
.expect("Failed to draw solid background");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw clock if enabled
|
|
||||||
if self.config.clock {
|
|
||||||
self.draw_clock();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw indicator if enabled
|
|
||||||
if self.config.indicator {
|
if self.config.indicator {
|
||||||
self.draw_indicator();
|
self.draw_indicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw password display (if not empty)
|
if self.config.clock {
|
||||||
|
self.draw_clock();
|
||||||
|
}
|
||||||
|
|
||||||
if !self.password_display.is_empty() {
|
if !self.password_display.is_empty() {
|
||||||
self.draw_password_display();
|
self.draw_password_display();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw wrong password feedback if active
|
if self.caps_lock {
|
||||||
|
self.draw_caps_lock_indicator();
|
||||||
|
}
|
||||||
|
|
||||||
if self.wrong_password_shown {
|
if self.wrong_password_shown {
|
||||||
self.draw_wrong_password_feedback();
|
self.draw_wrong_password_feedback();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw key highlight feedback if active
|
|
||||||
if self.key_highlight_shown {
|
if self.key_highlight_shown {
|
||||||
self.draw_key_highlight_feedback();
|
self.draw_key_highlight_feedback();
|
||||||
}
|
}
|
||||||
@@ -154,96 +143,79 @@ impl Renderer {
|
|||||||
self.update_feedback_timers();
|
self.update_feedback_timers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the rendered image surface
|
|
||||||
pub fn as_image_surface(&self) -> &ImageSurface {
|
|
||||||
&self.surface
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get raw pixel data from the surface (ARGB32 format)
|
/// Get raw pixel data from the surface (ARGB32 format)
|
||||||
pub fn get_pixel_data(&self) -> Result<Vec<u8>, cairo::BorrowError> {
|
pub fn get_pixel_data(&self) -> Result<Vec<u8>, cairo::BorrowError> {
|
||||||
let stride = self.surface.stride() as usize;
|
let stride = self.surface.stride() as usize;
|
||||||
let height = self.height as usize;
|
let height = self.height as usize;
|
||||||
|
|
||||||
let mut data = vec![0u8; stride * height];
|
let mut data = vec![0u8; stride * height];
|
||||||
self.surface.with_data(|src| {
|
self.surface.with_data(|src| {
|
||||||
data.copy_from_slice(src);
|
data.copy_from_slice(src);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get surface dimensions and stride
|
|
||||||
pub fn surface_info(&self) -> (i32, i32, i32) {
|
pub fn surface_info(&self) -> (i32, i32, i32) {
|
||||||
(self.width, self.height, self.surface.stride())
|
(self.width, self.height, self.surface.stride())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the clock in the center of the screen
|
fn update_uptime(&mut self) {
|
||||||
|
let now = Instant::now();
|
||||||
|
if let Some(last) = self.last_uptime_update {
|
||||||
|
if now.duration_since(last).as_secs() < 10 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let uptime_secs = std::fs::read_to_string("/proc/uptime")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
|
||||||
|
.unwrap_or(0.0) as u64;
|
||||||
|
self.uptime_cache = format!("up {}h {}m", uptime_secs / 3600, (uptime_secs % 3600) / 60);
|
||||||
|
self.last_uptime_update = Some(now);
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_clock(&self) {
|
fn draw_clock(&self) {
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let time_str = now.format("%H:%M").to_string();
|
let time_str = now.format("%H:%M").to_string();
|
||||||
let date_str = now.format("%A, %B %d").to_string();
|
let date_str = now.format("%A, %B %d").to_string();
|
||||||
|
let center_x = self.width as f64 / 2.0;
|
||||||
|
let center_y = self.height as f64 / 2.0;
|
||||||
|
|
||||||
self.context.set_font_size(72.0);
|
self.context.new_path();
|
||||||
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
self.context.set_font_size(48.0);
|
||||||
// Center the text
|
let te = self.context.text_extents(&time_str).unwrap();
|
||||||
let extents = self
|
|
||||||
.context
|
|
||||||
.text_extents(&time_str)
|
|
||||||
.expect("Failed to get text extents");
|
|
||||||
let x = (self.width as f64 - extents.width()) / 2.0;
|
|
||||||
let y = (self.height as f64 / 2.0) - extents.height() / 2.0;
|
|
||||||
|
|
||||||
self.context.move_to(x, y);
|
|
||||||
self.context
|
self.context
|
||||||
.show_text(&time_str)
|
.move_to(center_x - te.width() / 2.0, center_y + te.height() / 4.0);
|
||||||
.expect("Failed to draw time");
|
self.context.show_text(&time_str).unwrap();
|
||||||
|
|
||||||
// Draw date below time
|
self.context.new_path();
|
||||||
self.context.set_font_size(24.0);
|
self.context.set_font_size(14.0);
|
||||||
let date_extents = self
|
let de = self.context.text_extents(&date_str).unwrap();
|
||||||
.context
|
self.context.move_to(
|
||||||
.text_extents(&date_str)
|
center_x - de.width() / 2.0,
|
||||||
.expect("Failed to get date extents");
|
center_y + te.height() / 4.0 + 25.0,
|
||||||
let date_x = (self.width as f64 - date_extents.width()) / 2.0;
|
);
|
||||||
let date_y = y + extents.height() + 20.0;
|
self.context.show_text(&date_str).unwrap();
|
||||||
|
|
||||||
self.context.move_to(date_x, date_y);
|
self.context.new_path();
|
||||||
self.context
|
let ue = self.context.text_extents(&self.uptime_cache).unwrap();
|
||||||
.show_text(&date_str)
|
self.context.move_to(
|
||||||
.expect("Failed to draw date");
|
center_x - ue.width() / 2.0,
|
||||||
|
center_y + te.height() / 4.0 + 43.0,
|
||||||
|
);
|
||||||
|
self.context.show_text(&self.uptime_cache).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the password indicator ring
|
|
||||||
fn draw_indicator(&self) {
|
fn draw_indicator(&self) {
|
||||||
let center_x = self.width as f64 / 2.0;
|
let center_x = self.width as f64 / 2.0;
|
||||||
let center_y = self.height as f64 / 2.0;
|
let center_y = self.height as f64 / 2.0;
|
||||||
let radius = self.config.indicator_radius as f64;
|
let radius = self.config.indicator_radius as f64;
|
||||||
let thickness = self.config.indicator_thickness as f64;
|
let thickness = self.config.indicator_thickness as f64;
|
||||||
|
|
||||||
// Draw outer ring
|
self.context.new_path();
|
||||||
let ring_color = self.tuple_to_color(self.config.ring_color);
|
let (r, g, b, a) = self.config.inside_color;
|
||||||
self.context.set_source_rgba(
|
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||||
ring_color.r as f64 / 255.0,
|
|
||||||
ring_color.g as f64 / 255.0,
|
|
||||||
ring_color.b as f64 / 255.0,
|
|
||||||
ring_color.a as f64 / 255.0 * self.fade_alpha,
|
|
||||||
);
|
|
||||||
self.context.set_line_width(thickness);
|
|
||||||
self.context
|
|
||||||
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
|
||||||
self.context.stroke().expect("Failed to draw ring");
|
|
||||||
|
|
||||||
// Draw inside fill
|
|
||||||
let inside_color = self.tuple_to_color(self.config.inside_color);
|
|
||||||
self.context.set_source_rgba(
|
|
||||||
inside_color.r as f64 / 255.0,
|
|
||||||
inside_color.g as f64 / 255.0,
|
|
||||||
inside_color.b as f64 / 255.0,
|
|
||||||
inside_color.a as f64 / 255.0 * self.fade_alpha,
|
|
||||||
);
|
|
||||||
self.context.arc(
|
self.context.arc(
|
||||||
center_x,
|
center_x,
|
||||||
center_y,
|
center_y,
|
||||||
@@ -251,62 +223,75 @@ impl Renderer {
|
|||||||
0.0,
|
0.0,
|
||||||
2.0 * std::f64::consts::PI,
|
2.0 * std::f64::consts::PI,
|
||||||
);
|
);
|
||||||
self.context.fill().expect("Failed to fill inside");
|
self.context.fill().unwrap();
|
||||||
|
|
||||||
// Draw separator line
|
let (lr, lg, lb, la) = self.config.line_color;
|
||||||
let separator_color = self.tuple_to_color(self.config.separator_color);
|
if la > 0.0 {
|
||||||
if separator_color.a > 0 {
|
self.context.new_path();
|
||||||
self.context.set_source_rgba(
|
self.context
|
||||||
separator_color.r as f64 / 255.0,
|
.set_source_rgba(lr, lg, lb, la * self.fade_alpha);
|
||||||
separator_color.g as f64 / 255.0,
|
self.context.set_line_width(1.0);
|
||||||
separator_color.b as f64 / 255.0,
|
self.context.arc(
|
||||||
separator_color.a as f64 / 255.0 * self.fade_alpha,
|
center_x,
|
||||||
|
center_y,
|
||||||
|
radius - thickness / 2.0,
|
||||||
|
0.0,
|
||||||
|
2.0 * std::f64::consts::PI,
|
||||||
);
|
);
|
||||||
|
self.context.stroke().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context.new_path();
|
||||||
|
let (r, g, b, a) = self.config.ring_color;
|
||||||
|
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||||
|
self.context.set_line_width(thickness);
|
||||||
|
self.context
|
||||||
|
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
||||||
|
self.context.stroke().unwrap();
|
||||||
|
|
||||||
|
let (r, g, b, a) = self.config.separator_color;
|
||||||
|
if a > 0.0 {
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||||
self.context.set_line_width(1.0);
|
self.context.set_line_width(1.0);
|
||||||
self.context.move_to(center_x - radius, center_y);
|
self.context.move_to(center_x - radius, center_y);
|
||||||
self.context.line_to(center_x + radius, center_y);
|
self.context.line_to(center_x + radius, center_y);
|
||||||
self.context.stroke().expect("Failed to draw separator");
|
self.context.stroke().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the password display (masked characters)
|
|
||||||
fn draw_password_display(&self) {
|
fn draw_password_display(&self) {
|
||||||
if self.password_display.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position: below the indicator ring (or centered if no indicator)
|
|
||||||
let center_x = self.width as f64 / 2.0;
|
let center_x = self.width as f64 / 2.0;
|
||||||
let center_y = self.height as f64 / 2.0;
|
let center_y = self.height as f64 / 2.0;
|
||||||
let radius = self.config.indicator_radius as f64;
|
let radius = self.config.indicator_radius as f64;
|
||||||
let thickness = self.config.indicator_thickness as f64;
|
self.context.new_path();
|
||||||
|
self.context.set_font_size(32.0);
|
||||||
// Place password text below the ring
|
|
||||||
let text_y = center_y + radius + thickness + 40.0; // 40px below ring
|
|
||||||
|
|
||||||
self.context.set_font_size(36.0);
|
|
||||||
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
let te = self.context.text_extents(&self.password_display).unwrap();
|
||||||
// Center the text
|
|
||||||
let extents = self
|
|
||||||
.context
|
|
||||||
.text_extents(&self.password_display)
|
|
||||||
.expect("Failed to get password text extents");
|
|
||||||
let text_x = center_x - extents.width() / 2.0;
|
|
||||||
|
|
||||||
self.context.move_to(text_x, text_y);
|
|
||||||
self.context
|
self.context
|
||||||
.show_text(&self.password_display)
|
.move_to(center_x - te.width() / 2.0, center_y + radius / 1.1);
|
||||||
.expect("Failed to draw password");
|
self.context.show_text(&self.password_display).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_caps_lock_indicator(&self) {
|
||||||
|
let center_x = self.width as f64 / 2.0;
|
||||||
|
let center_y = self.height as f64 / 2.0;
|
||||||
|
let radius = self.config.indicator_radius as f64;
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_font_size(12.0);
|
||||||
|
self.context.set_source_rgba(1.0, 0.5, 0.0, self.fade_alpha);
|
||||||
|
let text = "CAPS LOCK";
|
||||||
|
let te = self.context.text_extents(text).unwrap();
|
||||||
|
self.context
|
||||||
|
.move_to(center_x - te.width() / 2.0, center_y + radius / 1.1 + 20.0);
|
||||||
|
self.context.show_text(text).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw wrong password feedback (red flash)
|
|
||||||
fn draw_wrong_password_feedback(&self) {
|
fn draw_wrong_password_feedback(&self) {
|
||||||
let center_x = self.width as f64 / 2.0;
|
let center_x = self.width as f64 / 2.0;
|
||||||
let center_y = self.height as f64 / 2.0;
|
let center_y = self.height as f64 / 2.0;
|
||||||
let radius = self.config.indicator_radius as f64;
|
let radius = self.config.indicator_radius as f64;
|
||||||
|
let thickness = self.config.indicator_thickness as f64;
|
||||||
// Calculate flash intensity based on time
|
|
||||||
let intensity = if let Some(start) = self.wrong_password_start {
|
let intensity = if let Some(start) = self.wrong_password_start {
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
let duration = std::time::Duration::from_millis(500);
|
let duration = std::time::Duration::from_millis(500);
|
||||||
@@ -320,26 +305,24 @@ impl Renderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if intensity > 0.0 {
|
if intensity > 0.0 {
|
||||||
|
self.context.new_path();
|
||||||
self.context
|
self.context
|
||||||
.set_source_rgba(1.0, 0.0, 0.0, intensity * self.fade_alpha);
|
.set_source_rgba(1.0, 0.0, 0.0, intensity * self.fade_alpha);
|
||||||
|
self.context.set_line_width(thickness + 2.0);
|
||||||
self.context
|
self.context
|
||||||
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
||||||
self.context
|
self.context.stroke().unwrap();
|
||||||
.fill()
|
|
||||||
.expect("Failed to draw wrong password feedback");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw key highlight feedback (green flash)
|
|
||||||
fn draw_key_highlight_feedback(&self) {
|
fn draw_key_highlight_feedback(&self) {
|
||||||
let center_x = self.width as f64 / 2.0;
|
let center_x = self.width as f64 / 2.0;
|
||||||
let center_y = self.height as f64 / 2.0;
|
let center_y = self.height as f64 / 2.0;
|
||||||
let radius = self.config.indicator_radius as f64;
|
let radius = self.config.indicator_radius as f64;
|
||||||
|
let thickness = self.config.indicator_thickness as f64;
|
||||||
// Calculate flash intensity based on time
|
|
||||||
let intensity = if let Some(start) = self.key_highlight_start {
|
let intensity = if let Some(start) = self.key_highlight_start {
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
let duration = std::time::Duration::from_millis(200);
|
let duration = std::time::Duration::from_millis(300);
|
||||||
if elapsed < duration {
|
if elapsed < duration {
|
||||||
1.0 - (elapsed.as_secs_f64() / duration.as_secs_f64())
|
1.0 - (elapsed.as_secs_f64() / duration.as_secs_f64())
|
||||||
} else {
|
} else {
|
||||||
@@ -350,34 +333,36 @@ impl Renderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if intensity > 0.0 {
|
if intensity > 0.0 {
|
||||||
let key_hl_color = self.tuple_to_color(self.config.key_hl_color);
|
let (r, g, b, a) = self.config.key_hl_color;
|
||||||
self.context.set_source_rgba(
|
self.context
|
||||||
key_hl_color.r as f64 / 255.0,
|
.set_source_rgba(r, g, b, a * intensity * self.fade_alpha);
|
||||||
key_hl_color.g as f64 / 255.0,
|
self.context.set_line_width(thickness + 1.5);
|
||||||
key_hl_color.b as f64 / 255.0,
|
|
||||||
key_hl_color.a as f64 / 255.0 * intensity * self.fade_alpha,
|
// Draw ONLY ONE segment that rotates based on password length
|
||||||
|
let global_offset = (self.password_display.len() as f64 * 45.0).to_radians();
|
||||||
|
self.context.new_path();
|
||||||
|
let actual_start = global_offset + self.key_highlight_angle;
|
||||||
|
self.context.arc(
|
||||||
|
center_x,
|
||||||
|
center_y,
|
||||||
|
radius,
|
||||||
|
actual_start,
|
||||||
|
actual_start + (40.0_f64).to_radians(),
|
||||||
);
|
);
|
||||||
self.context
|
self.context.stroke().unwrap();
|
||||||
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
|
||||||
self.context
|
|
||||||
.fill()
|
|
||||||
.expect("Failed to draw key highlight feedback");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update feedback timers and reset expired feedback
|
|
||||||
fn update_feedback_timers(&mut self) {
|
fn update_feedback_timers(&mut self) {
|
||||||
// Check wrong password feedback timeout
|
self.update_uptime();
|
||||||
if let Some(start) = self.wrong_password_start {
|
if let Some(start) = self.wrong_password_start {
|
||||||
if start.elapsed() > std::time::Duration::from_millis(500) {
|
if start.elapsed() > std::time::Duration::from_millis(500) {
|
||||||
self.wrong_password_shown = false;
|
self.wrong_password_shown = false;
|
||||||
self.wrong_password_start = None;
|
self.wrong_password_start = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check key highlight feedback timeout
|
|
||||||
if let Some(start) = self.key_highlight_start {
|
if let Some(start) = self.key_highlight_start {
|
||||||
if start.elapsed() > std::time::Duration::from_millis(200) {
|
if start.elapsed() > std::time::Duration::from_millis(300) {
|
||||||
self.key_highlight_shown = false;
|
self.key_highlight_shown = false;
|
||||||
self.key_highlight_start = None;
|
self.key_highlight_start = None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,6 @@ impl Screenshot {
|
|||||||
Self { surface }
|
Self { surface }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the underlying surface.
|
|
||||||
pub fn surface(&self) -> &ImageSurface {
|
|
||||||
&self.surface
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consume the screenshot and return the underlying Cairo surface.
|
/// Consume the screenshot and return the underlying Cairo surface.
|
||||||
pub fn into_inner(self) -> ImageSurface {
|
pub fn into_inner(self) -> ImageSurface {
|
||||||
self.surface
|
self.surface
|
||||||
@@ -171,11 +166,6 @@ impl ScreenshotManager {
|
|||||||
Ok(Self { manager })
|
Ok(Self { manager })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the wlr-screencopy protocol is available.
|
|
||||||
pub fn is_available(&self) -> bool {
|
|
||||||
self.manager.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initiate a screencopy operation for the given output.
|
/// Initiate a screencopy operation for the given output.
|
||||||
///
|
///
|
||||||
/// This method sends a screencopy request and returns the frame object.
|
/// This method sends a screencopy request and returns the frame object.
|
||||||
|
|||||||
@@ -1,26 +1 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub struct FadeTimer {
|
|
||||||
duration: Duration,
|
|
||||||
start_time: std::time::Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FadeTimer {
|
|
||||||
pub fn new(duration: Duration) -> Self {
|
|
||||||
Self {
|
|
||||||
duration,
|
|
||||||
start_time: std::time::Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self) -> bool {
|
|
||||||
let elapsed = self.start_time.elapsed();
|
|
||||||
let progress = elapsed.as_secs_f64() / self.duration.as_secs_f64();
|
|
||||||
progress >= 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_alpha(&self) -> f64 {
|
|
||||||
let elapsed = self.start_time.elapsed();
|
|
||||||
(elapsed.as_secs_f64() / self.duration.as_secs_f64()).min(1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
-20
@@ -38,23 +38,3 @@ pub fn parse_vignette_effect(s: &str) -> Result<(f32, f32), String> {
|
|||||||
let factor = parts[1].parse().map_err(|_| "Invalid factor")?;
|
let factor = parts[1].parse().map_err(|_| "Invalid factor")?;
|
||||||
Ok((base, factor))
|
Ok((base, factor))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert hex color string to RGBA color struct
|
|
||||||
pub fn hex_to_rgba(hex: &str) -> Color {
|
|
||||||
let (r, g, b, a) = parse_hex_color(hex).unwrap_or((0.0, 0.0, 0.0, 1.0));
|
|
||||||
Color {
|
|
||||||
r: (r * 255.0) as u8,
|
|
||||||
g: (g * 255.0) as u8,
|
|
||||||
b: (b * 255.0) as u8,
|
|
||||||
a: (a * 255.0) as u8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RGBA color struct
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Color {
|
|
||||||
pub r: u8,
|
|
||||||
pub g: u8,
|
|
||||||
pub b: u8,
|
|
||||||
pub a: u8,
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user