scythe/src/overlay.rs

812 lines
34 KiB
Rust

use gtk::prelude::*;
use gtk::{
Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider, Entry, Label,
Orientation, SpinButton, Stack, StackTransitionType, StyleContext, Switch,
};
#[cfg(target_os = "linux")]
use gtk_layer_shell::{Layer, LayerShell};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use crate::config::VrecConfig;
use crate::ipc::{self, Command};
pub fn show_notification_overlay() {
show_notification("Replay saved");
}
pub fn show_notification(message: &str) {
#[cfg(unix)]
if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() {
return;
}
if gtk::init().is_err() {
return;
}
let app = Application::builder()
.application_id("com.vrec.notification")
.build();
let msg_text = message.to_string();
app.connect_activate(move |app| {
let window = ApplicationWindow::builder()
.application(app)
.default_width(320)
.default_height(60)
.build();
#[cfg(target_os = "linux")]
{
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_namespace("vrec-notification");
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30);
window.set_anchor(gtk_layer_shell::Edge::Top, true);
}
#[cfg(not(target_os = "linux"))]
{
window.set_decorated(false);
window.set_keep_above(true);
window.set_skip_taskbar_hint(true);
window.set_position(gtk::WindowPosition::Center);
}
let css_provider = CssProvider::new();
let css = r#"
window {
background-color: #12141a;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8);
}
label {
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px 24px;
}
"#;
let _ = css_provider.load_from_data(css.as_bytes());
if let Some(screen) = gdk::Screen::default() {
StyleContext::add_provider_for_screen(
&screen,
&css_provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
let label = Label::new(Some(&msg_text));
window.add(&label);
window.show_all();
let window_clone = window.clone();
gtk::glib::timeout_add_local(Duration::from_millis(2200), move || {
window_clone.close();
gtk::glib::ControlFlow::Break
});
});
app.run_with_args(&[] as &[&str]);
}
pub fn show_menu_overlay() {
#[cfg(unix)]
if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() {
eprintln!("Error: No display server detected (WAYLAND_DISPLAY and DISPLAY are unset).");
return;
}
if gtk::init().is_err() {
eprintln!("Error: Failed to connect to display server.");
return;
}
let app = Application::builder()
.application_id("com.vrec.hud")
.build();
app.connect_activate(|app| {
let config = VrecConfig::load();
let window = ApplicationWindow::builder()
.application(app)
.default_width(820)
.default_height(480)
.build();
#[cfg(target_os = "linux")]
{
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_namespace("vrec-hud");
window.set_keyboard_interactivity(true);
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 50);
window.set_anchor(gtk_layer_shell::Edge::Top, true);
}
#[cfg(not(target_os = "linux"))]
{
window.set_decorated(false);
window.set_keep_above(true);
window.set_skip_taskbar_hint(true);
window.set_position(gtk::WindowPosition::Center);
}
let stack = Stack::new();
stack.set_transition_type(StackTransitionType::Crossfade);
stack.set_transition_duration(180);
// ==========================================
// PAGE 1: HUD Quick Actions (GPU Screen Recorder Style)
// ==========================================
let hud_page = Box::new(Orientation::Vertical, 16);
hud_page.set_margin_start(24);
hud_page.set_margin_end(24);
hud_page.set_margin_top(18);
hud_page.set_margin_bottom(18);
// Header Bar
let header_box = Box::new(Orientation::Horizontal, 10);
let title_label = Label::new(Some("VREC"));
title_label.style_context().add_class("hud-title");
let badge_label = Label::new(Some("GPU ACCELERATED"));
badge_label.style_context().add_class("badge-tag");
let spacer = Box::new(Orientation::Horizontal, 0);
let close_btn = Button::with_label("X");
close_btn.style_context().add_class("close-btn");
header_box.pack_start(&title_label, false, false, 0);
header_box.pack_start(&badge_label, false, false, 6);
header_box.pack_start(&spacer, true, true, 0);
header_box.pack_start(&close_btn, false, false, 0);
// 4 Action Cards Row
let cards_box = Box::new(Orientation::Horizontal, 14);
cards_box.set_halign(gtk::Align::Center);
// Card 1: Replay
let replay_card = Box::new(Orientation::Vertical, 8);
replay_card.style_context().add_class("card-box");
let replay_hdr = Box::new(Orientation::Horizontal, 6);
let replay_title = Label::new(Some("REPLAY"));
replay_title.style_context().add_class("card-header-label");
let replay_pill = Label::new(Some(if config.replay_enabled { "ACTIVE" } else { "OFF" }));
replay_pill.style_context().add_class("status-pill");
replay_pill.style_context().add_class(if config.replay_enabled { "pill-active" } else { "pill-idle" });
let replay_sp = Box::new(Orientation::Horizontal, 0);
replay_hdr.pack_start(&replay_title, false, false, 0);
replay_hdr.pack_start(&replay_sp, true, true, 0);
replay_hdr.pack_start(&replay_pill, false, false, 0);
let replay_toggle_btn = Button::with_label(if config.replay_enabled { "Disable Replay" } else { "Enable Replay" });
replay_toggle_btn.set_size_request(165, 45);
replay_toggle_btn.style_context().add_class("action-btn");
let replay_save_btn = Button::with_label("Save Replay");
replay_save_btn.style_context().add_class("sub-btn");
replay_card.pack_start(&replay_hdr, false, false, 0);
replay_card.pack_start(&replay_toggle_btn, true, true, 4);
replay_card.pack_start(&replay_save_btn, false, false, 0);
// Card 2: Record
let record_card = Box::new(Orientation::Vertical, 8);
record_card.style_context().add_class("card-box");
let record_hdr = Box::new(Orientation::Horizontal, 6);
let record_title = Label::new(Some("RECORD"));
record_title.style_context().add_class("card-header-label");
let record_pill = Label::new(Some("IDLE"));
record_pill.style_context().add_class("status-pill");
record_pill.style_context().add_class("pill-idle");
let record_sp = Box::new(Orientation::Horizontal, 0);
record_hdr.pack_start(&record_title, false, false, 0);
record_hdr.pack_start(&record_sp, true, true, 0);
record_hdr.pack_start(&record_pill, false, false, 0);
let record_toggle_btn = Button::with_label("Start Record");
record_toggle_btn.set_size_request(165, 45);
record_toggle_btn.style_context().add_class("action-btn");
let record_format_sub = Label::new(Some(&format!("{} • {} FPS", config.video_codec.to_uppercase(), config.fps)));
record_format_sub.style_context().add_class("sub-label");
record_card.pack_start(&record_hdr, false, false, 0);
record_card.pack_start(&record_toggle_btn, true, true, 4);
record_card.pack_start(&record_format_sub, false, false, 0);
// Card 3: Audio Mode
let audio_card = Box::new(Orientation::Vertical, 8);
audio_card.style_context().add_class("card-box");
let audio_hdr = Box::new(Orientation::Horizontal, 6);
let audio_title = Label::new(Some("AUDIO"));
audio_title.style_context().add_class("card-header-label");
let audio_pill = Label::new(Some(match config.audio_mode.as_str() {
"mic" => "MIC ONLY",
"both" => "SYSTEM + MIC",
"muted" => "MUTED",
_ => "SYSTEM ONLY",
}));
audio_pill.style_context().add_class("status-pill");
audio_pill.style_context().add_class(match config.audio_mode.as_str() {
"mic" => "pill-cyan",
"both" => "pill-purple",
"muted" => "pill-recording",
_ => "pill-active",
});
let audio_sp = Box::new(Orientation::Horizontal, 0);
audio_hdr.pack_start(&audio_title, false, false, 0);
audio_hdr.pack_start(&audio_sp, true, true, 0);
audio_hdr.pack_start(&audio_pill, false, false, 0);
let audio_cycle_btn = Button::with_label(match config.audio_mode.as_str() {
"mic" => "Microphone Only",
"both" => "System + Mic",
"muted" => "Muted",
_ => "System Audio Only",
});
audio_cycle_btn.set_size_request(165, 45);
audio_cycle_btn.style_context().add_class("action-btn");
let audio_hint_sub = Label::new(Some("Click to Cycle Mode"));
audio_hint_sub.style_context().add_class("sub-label");
audio_card.pack_start(&audio_hdr, false, false, 0);
audio_card.pack_start(&audio_cycle_btn, true, true, 4);
audio_card.pack_start(&audio_hint_sub, false, false, 0);
// Card 4: Settings
let settings_card = Box::new(Orientation::Vertical, 8);
settings_card.style_context().add_class("card-box");
let settings_hdr = Box::new(Orientation::Horizontal, 6);
let settings_title_hdr = Label::new(Some("PREFERENCES"));
settings_title_hdr.style_context().add_class("card-header-label");
let settings_pill = Label::new(Some("TUNING"));
settings_pill.style_context().add_class("status-pill");
settings_pill.style_context().add_class("pill-idle");
let settings_sp = Box::new(Orientation::Horizontal, 0);
settings_hdr.pack_start(&settings_title_hdr, false, false, 0);
settings_hdr.pack_start(&settings_sp, true, true, 0);
settings_hdr.pack_start(&settings_pill, false, false, 0);
let settings_open_btn = Button::with_label("Open Settings");
settings_open_btn.set_size_request(165, 45);
settings_open_btn.style_context().add_class("action-btn");
let settings_hint_sub = Label::new(Some("Esc to Close"));
settings_hint_sub.style_context().add_class("sub-label");
settings_card.pack_start(&settings_hdr, false, false, 0);
settings_card.pack_start(&settings_open_btn, true, true, 4);
settings_card.pack_start(&settings_hint_sub, false, false, 0);
cards_box.pack_start(&replay_card, true, true, 0);
cards_box.pack_start(&record_card, true, true, 0);
cards_box.pack_start(&audio_card, true, true, 0);
cards_box.pack_start(&settings_card, true, true, 0);
hud_page.pack_start(&header_box, false, false, 0);
hud_page.pack_start(&cards_box, true, true, 0);
// ==========================================
// PAGE 2: Fine-Tuned Settings Panel (Numeric inputs for Time, Quality, FPS)
// ==========================================
let settings_page = Box::new(Orientation::Vertical, 12);
settings_page.set_margin_start(24);
settings_page.set_margin_end(24);
settings_page.set_margin_top(16);
settings_page.set_margin_bottom(16);
let settings_header = Box::new(Orientation::Horizontal, 12);
let back_btn = Button::with_label("Back to HUD");
back_btn.style_context().add_class("sub-btn");
let settings_title = Label::new(Some("Recorder Settings & Quality Tuning"));
settings_title.style_context().add_class("hud-title");
settings_header.pack_start(&back_btn, false, false, 0);
settings_header.pack_start(&settings_title, false, false, 0);
let settings_grid = gtk::Grid::new();
settings_grid.set_column_spacing(20);
settings_grid.set_row_spacing(10);
// 1. Replay Duration (Seconds) - Direct numeric spin button + preset pills!
let dur_lbl = Label::new(Some("Replay Buffer (Seconds):"));
dur_lbl.set_halign(gtk::Align::Start);
let dur_box = Box::new(Orientation::Horizontal, 6);
let dur_spin = SpinButton::with_range(5.0, 1800.0, 5.0);
dur_spin.set_value(config.replay_duration_sec as f64);
dur_box.pack_start(&dur_spin, false, false, 0);
let p_30s = Button::with_label("30s"); p_30s.style_context().add_class("preset-btn");
let p_60s = Button::with_label("60s"); p_60s.style_context().add_class("preset-btn");
let p_2m = Button::with_label("2m"); p_2m.style_context().add_class("preset-btn");
let p_5m = Button::with_label("5m"); p_5m.style_context().add_class("preset-btn");
let s_clone = dur_spin.clone(); p_30s.connect_clicked(move |_| s_clone.set_value(30.0));
let s_clone = dur_spin.clone(); p_60s.connect_clicked(move |_| s_clone.set_value(60.0));
let s_clone = dur_spin.clone(); p_2m.connect_clicked(move |_| s_clone.set_value(120.0));
let s_clone = dur_spin.clone(); p_5m.connect_clicked(move |_| s_clone.set_value(300.0));
dur_box.pack_start(&p_30s, false, false, 0);
dur_box.pack_start(&p_60s, false, false, 0);
dur_box.pack_start(&p_2m, false, false, 0);
dur_box.pack_start(&p_5m, false, false, 0);
// 2. Video Quality / Bitrate (Mbps) - Direct numeric spin button + preset pills!
let bit_lbl = Label::new(Some("Video Bitrate (Mbps):"));
bit_lbl.set_halign(gtk::Align::Start);
let bit_box = Box::new(Orientation::Horizontal, 6);
let bit_spin = SpinButton::with_range(2.0, 100.0, 1.0);
bit_spin.set_value((config.record_bitrate_kbps / 1000) as f64);
bit_box.pack_start(&bit_spin, false, false, 0);
let b_10m = Button::with_label("10M"); b_10m.style_context().add_class("preset-btn");
let b_20m = Button::with_label("20M"); b_20m.style_context().add_class("preset-btn");
let b_30m = Button::with_label("30M"); b_30m.style_context().add_class("preset-btn");
let b_50m = Button::with_label("50M"); b_50m.style_context().add_class("preset-btn");
let b_clone = bit_spin.clone(); b_10m.connect_clicked(move |_| b_clone.set_value(10.0));
let b_clone = bit_spin.clone(); b_20m.connect_clicked(move |_| b_clone.set_value(20.0));
let b_clone = bit_spin.clone(); b_30m.connect_clicked(move |_| b_clone.set_value(30.0));
let b_clone = bit_spin.clone(); b_50m.connect_clicked(move |_| b_clone.set_value(50.0));
bit_box.pack_start(&b_10m, false, false, 0);
bit_box.pack_start(&b_20m, false, false, 0);
bit_box.pack_start(&b_30m, false, false, 0);
bit_box.pack_start(&b_50m, false, false, 0);
// 3. Framerate (FPS) - Direct numeric spin button + preset pills!
let fps_lbl = Label::new(Some("Target Framerate (FPS):"));
fps_lbl.set_halign(gtk::Align::Start);
let fps_box = Box::new(Orientation::Horizontal, 6);
let fps_spin = SpinButton::with_range(15.0, 240.0, 1.0);
fps_spin.set_value(config.fps as f64);
fps_box.pack_start(&fps_spin, false, false, 0);
let f_30 = Button::with_label("30"); f_30.style_context().add_class("preset-btn");
let f_60 = Button::with_label("60"); f_60.style_context().add_class("preset-btn");
let f_120 = Button::with_label("120"); f_120.style_context().add_class("preset-btn");
let f_144 = Button::with_label("144"); f_144.style_context().add_class("preset-btn");
let f_clone = fps_spin.clone(); f_30.connect_clicked(move |_| f_clone.set_value(30.0));
let f_clone = fps_spin.clone(); f_60.connect_clicked(move |_| f_clone.set_value(60.0));
let f_clone = fps_spin.clone(); f_120.connect_clicked(move |_| f_clone.set_value(120.0));
let f_clone = fps_spin.clone(); f_144.connect_clicked(move |_| f_clone.set_value(144.0));
fps_box.pack_start(&f_30, false, false, 0);
fps_box.pack_start(&f_60, false, false, 0);
fps_box.pack_start(&f_120, false, false, 0);
fps_box.pack_start(&f_144, false, false, 0);
// 4. Video Codec
let codec_lbl = Label::new(Some("Video Codec:"));
codec_lbl.set_halign(gtk::Align::Start);
let codec_combo = ComboBoxText::new();
codec_combo.append(Some("h264"), "H.264 / AVC (Most Compatible)");
codec_combo.append(Some("hevc"), "HEVC / H.265 (High Efficiency)");
codec_combo.append(Some("av1"), "AV1 (Next-Gen High Quality)");
codec_combo.set_active_id(Some(&config.video_codec));
// 5. Audio Source Mode (System Only vs Mic Only vs Both!)
let audio_mode_lbl = Label::new(Some("Audio Track Mode:"));
audio_mode_lbl.set_halign(gtk::Align::Start);
let audio_mode_combo = ComboBoxText::new();
audio_mode_combo.append(Some("system"), "System Sounds Only (Game / Desktop)");
audio_mode_combo.append(Some("mic"), "Microphone Only");
audio_mode_combo.append(Some("both"), "Both (System Sounds + Microphone Merged)");
audio_mode_combo.append(Some("muted"), "Muted (No Audio)");
audio_mode_combo.set_active_id(Some(&config.audio_mode));
// 6. Audio Device
let audio_dev_lbl = Label::new(Some("Audio Hardware Device:"));
audio_dev_lbl.set_halign(gtk::Align::Start);
let audio_dev_combo = ComboBoxText::new();
audio_dev_combo.append(Some("default"), "Default Device");
for dev in crate::capture::audio::list_input_devices() {
audio_dev_combo.append(Some(&dev), &dev);
}
audio_dev_combo.set_active_id(Some(&config.audio_device));
// 7. Save Folder
let dir_lbl = Label::new(Some("Save Folder:"));
dir_lbl.set_halign(gtk::Align::Start);
let dir_entry = Entry::new();
dir_entry.set_text(&config.output_directory);
// 8. Hotkeys
let save_hk_lbl = Label::new(Some("Save Replay Hotkey:"));
save_hk_lbl.set_halign(gtk::Align::Start);
let save_hk_entry = Entry::new();
save_hk_entry.set_text(&config.save_hotkey);
let record_hk_lbl = Label::new(Some("Toggle Record Hotkey:"));
record_hk_lbl.set_halign(gtk::Align::Start);
let record_hk_entry = Entry::new();
record_hk_entry.set_text(&config.record_hotkey);
let menu_hk_lbl = Label::new(Some("Menu Overlay Hotkey:"));
menu_hk_lbl.set_halign(gtk::Align::Start);
let menu_hk_entry = Entry::new();
menu_hk_entry.set_text(&config.menu_hotkey);
// 9. Autostart
let auto_lbl = Label::new(Some("Autostart on Login:"));
auto_lbl.set_halign(gtk::Align::Start);
let auto_switch = Switch::new();
auto_switch.set_active(config.autostart);
settings_grid.attach(&dur_lbl, 0, 0, 1, 1);
settings_grid.attach(&dur_box, 1, 0, 1, 1);
settings_grid.attach(&bit_lbl, 0, 1, 1, 1);
settings_grid.attach(&bit_box, 1, 1, 1, 1);
settings_grid.attach(&fps_lbl, 0, 2, 1, 1);
settings_grid.attach(&fps_box, 1, 2, 1, 1);
settings_grid.attach(&codec_lbl, 0, 3, 1, 1);
settings_grid.attach(&codec_combo, 1, 3, 1, 1);
settings_grid.attach(&audio_mode_lbl, 0, 4, 1, 1);
settings_grid.attach(&audio_mode_combo, 1, 4, 1, 1);
settings_grid.attach(&audio_dev_lbl, 0, 5, 1, 1);
settings_grid.attach(&audio_dev_combo, 1, 5, 1, 1);
settings_grid.attach(&dir_lbl, 0, 6, 1, 1);
settings_grid.attach(&dir_entry, 1, 6, 1, 1);
settings_grid.attach(&save_hk_lbl, 0, 7, 1, 1);
settings_grid.attach(&save_hk_entry, 1, 7, 1, 1);
settings_grid.attach(&record_hk_lbl, 0, 8, 1, 1);
settings_grid.attach(&record_hk_entry, 1, 8, 1, 1);
settings_grid.attach(&menu_hk_lbl, 0, 9, 1, 1);
settings_grid.attach(&menu_hk_entry, 1, 9, 1, 1);
settings_grid.attach(&auto_lbl, 0, 10, 1, 1);
settings_grid.attach(&auto_switch, 1, 10, 1, 1);
let save_settings_btn = Button::with_label("Apply & Save Settings");
save_settings_btn.style_context().add_class("apply-btn");
settings_page.pack_start(&settings_header, false, false, 0);
settings_page.pack_start(&settings_grid, true, true, 4);
settings_page.pack_start(&save_settings_btn, false, false, 4);
stack.add_named(&hud_page, "hud");
stack.add_named(&settings_page, "settings");
window.add(&stack);
// CSS Styling (Brushed Stealth HUD Theme)
let css_provider = CssProvider::new();
let css = r#"
window {
background-color: #12141a;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.85);
color: #f1f5f9;
}
.hud-title {
font-size: 18px;
font-weight: 800;
color: #ffffff;
letter-spacing: 1.5px;
}
.badge-tag {
background-color: rgba(56, 189, 248, 0.12);
color: #38bdf8;
border: 1px solid rgba(56, 189, 248, 0.3);
font-size: 10px;
font-weight: 700;
border-radius: 6px;
padding: 2px 8px;
}
.card-box {
background-color: #181b23;
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 12px;
padding: 12px;
min-width: 170px;
}
.card-box:hover {
background-color: #1e222d;
border-color: rgba(255, 255, 255, 0.15);
}
.card-header-label {
font-size: 11px;
font-weight: 700;
color: #94a3b8;
letter-spacing: 0.8px;
}
.status-pill {
font-size: 10px;
font-weight: 800;
padding: 2px 8px;
border-radius: 10px;
}
.pill-active {
background-color: rgba(16, 185, 129, 0.15);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.pill-idle {
background-color: rgba(148, 163, 184, 0.12);
color: #94a3b8;
border: 1px solid rgba(148, 163, 184, 0.2);
}
.pill-recording {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.4);
}
.pill-cyan {
background-color: rgba(6, 182, 212, 0.15);
color: #06b6d4;
border: 1px solid rgba(6, 182, 212, 0.3);
}
.pill-purple {
background-color: rgba(168, 85, 247, 0.15);
color: #a855f7;
border: 1px solid rgba(168, 85, 247, 0.3);
}
.action-btn {
background-color: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #ffffff;
font-size: 13px;
font-weight: 700;
padding: 8px 12px;
}
.action-btn:hover {
background-color: #2563eb;
border-color: #3b82f6;
color: #ffffff;
}
.recording-active {
background-color: rgba(239, 68, 68, 0.25) !important;
border-color: #ef4444 !important;
color: #ef4444 !important;
}
.sub-btn {
background-color: transparent;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
color: #94a3b8;
font-size: 11px;
font-weight: 600;
padding: 5px 10px;
}
.sub-btn:hover {
background-color: rgba(255, 255, 255, 0.08);
color: #ffffff;
}
.preset-btn {
background-color: #1e222d;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
color: #94a3b8;
font-size: 11px;
font-weight: 600;
padding: 4px 8px;
}
.preset-btn:hover {
background-color: rgba(37, 99, 235, 0.2);
border-color: #3b82f6;
color: #ffffff;
}
.apply-btn {
background-color: #2563eb;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 700;
padding: 10px 24px;
}
.apply-btn:hover {
background-color: #1d4ed8;
}
.close-btn {
background-color: transparent;
color: #94a3b8;
border: none;
font-size: 16px;
font-weight: bold;
padding: 4px 8px;
}
.close-btn:hover {
color: #f43f5e;
}
.sub-label {
color: #64748b;
font-size: 11px;
font-weight: 500;
}
entry, spinbutton, combobox button {
background-color: #171a22;
color: #f1f5f9;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 6px 10px;
font-size: 13px;
font-weight: 600;
}
entry:focus, spinbutton:focus, combobox button:focus {
border-color: #3b82f6;
background-color: #1c202a;
}
spinbutton button {
background-color: transparent;
color: #94a3b8;
border: none;
}
spinbutton button:hover {
color: #ffffff;
}
"#;
let _ = css_provider.load_from_data(css.as_bytes());
if let Some(screen) = gdk::Screen::default() {
StyleContext::add_provider_for_screen(
&screen,
&css_provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
// Window Close Triggers
let window_close = window.clone();
close_btn.connect_clicked(move |_| {
window_close.close();
});
let window_esc = window.clone();
window.connect_key_press_event(move |_, key| {
if key.keyval() == gdk::keys::constants::Escape {
window_esc.close();
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
});
// Stack Navigation
let stack_to_settings = stack.clone();
settings_open_btn.connect_clicked(move |_| {
stack_to_settings.set_visible_child_name("settings");
});
let stack_to_hud = stack.clone();
back_btn.connect_clicked(move |_| {
stack_to_hud.set_visible_child_name("hud");
});
// Save Settings Action
let stack_after_save = stack.clone();
save_settings_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.replay_duration_sec = dur_spin.value() as u32;
let bit_mbps = bit_spin.value() as u32;
cfg.record_bitrate_kbps = bit_mbps * 1000;
cfg.replay_bitrate_kbps = bit_mbps * 1000;
cfg.fps = fps_spin.value() as u32;
if let Some(c) = codec_combo.active_id() {
cfg.video_codec = c.to_string();
}
if let Some(m) = audio_mode_combo.active_id() {
cfg.audio_mode = m.to_string();
}
if let Some(dev) = audio_dev_combo.active_id() {
cfg.audio_device = dev.to_string();
}
let dir_val = dir_entry.text().to_string();
if !dir_val.trim().is_empty() {
cfg.output_directory = dir_val;
}
cfg.autostart = auto_switch.is_active();
cfg.save_hotkey = save_hk_entry.text().to_string();
cfg.menu_hotkey = menu_hk_entry.text().to_string();
cfg.record_hotkey = record_hk_entry.text().to_string();
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
stack_after_save.set_visible_child_name("hud");
});
// Action Handlers
let replay_lbl_btn = replay_toggle_btn.clone();
let replay_pill_clone = replay_pill.clone();
replay_toggle_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.replay_enabled = !cfg.replay_enabled;
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
if cfg.replay_enabled {
replay_lbl_btn.set_label("Disable Replay");
replay_pill_clone.set_label("ACTIVE");
replay_pill_clone.style_context().remove_class("pill-idle");
replay_pill_clone.style_context().add_class("pill-active");
} else {
replay_lbl_btn.set_label("Enable Replay");
replay_pill_clone.set_label("OFF");
replay_pill_clone.style_context().remove_class("pill-active");
replay_pill_clone.style_context().add_class("pill-idle");
}
});
replay_save_btn.connect_clicked(|_| {
let _ = ipc::send_command(Command::SaveReplay);
show_notification_overlay();
});
record_toggle_btn.connect_clicked(|_| {
let _ = ipc::send_command(Command::ToggleRecording);
});
// Audio Source Mode Quick-Cycle
let audio_pill_clone = audio_pill.clone();
let audio_btn_clone = audio_cycle_btn.clone();
audio_cycle_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
let next_mode = match cfg.audio_mode.as_str() {
"system" => "mic",
"mic" => "both",
"both" => "muted",
_ => "system",
};
cfg.audio_mode = next_mode.to_string();
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
audio_pill_clone.style_context().remove_class("pill-active");
audio_pill_clone.style_context().remove_class("pill-cyan");
audio_pill_clone.style_context().remove_class("pill-purple");
audio_pill_clone.style_context().remove_class("pill-recording");
match next_mode {
"mic" => {
audio_pill_clone.set_label("MIC ONLY");
audio_pill_clone.style_context().add_class("pill-cyan");
audio_btn_clone.set_label("Microphone Only");
}
"both" => {
audio_pill_clone.set_label("SYSTEM + MIC");
audio_pill_clone.style_context().add_class("pill-purple");
audio_btn_clone.set_label("System + Mic");
}
"muted" => {
audio_pill_clone.set_label("MUTED");
audio_pill_clone.style_context().add_class("pill-recording");
audio_btn_clone.set_label("Muted");
}
_ => {
audio_pill_clone.set_label("SYSTEM ONLY");
audio_pill_clone.style_context().add_class("pill-active");
audio_btn_clone.set_label("System Audio Only");
}
}
});
// Periodic Status Sync (Timer & Live States)
let rec_btn_sync = record_toggle_btn.clone();
let rec_pill_sync = record_pill.clone();
let is_running = Arc::new(AtomicBool::new(true));
let is_running_clone = Arc::clone(&is_running);
gtk::glib::timeout_add_local(Duration::from_millis(500), move || {
if !is_running_clone.load(Ordering::Relaxed) {
return gtk::glib::ControlFlow::Break;
}
if let Ok(status) = ipc::query_status() {
if status.is_recording {
let mins = status.recording_duration_sec / 60;
let secs = status.recording_duration_sec % 60;
rec_btn_sync.set_label("Stop Recording");
rec_btn_sync.style_context().add_class("recording-active");
rec_pill_sync.set_label(&format!("REC {:02}:{:02}", mins, secs));
rec_pill_sync.style_context().remove_class("pill-idle");
rec_pill_sync.style_context().add_class("pill-recording");
} else {
rec_btn_sync.set_label("Start Record");
rec_btn_sync.style_context().remove_class("recording-active");
rec_pill_sync.set_label("IDLE");
rec_pill_sync.style_context().remove_class("pill-recording");
rec_pill_sync.style_context().add_class("pill-idle");
}
}
gtk::glib::ControlFlow::Continue
});
window.connect_destroy(move |_| {
is_running.store(false, Ordering::Relaxed);
});
window.show_all();
});
app.run_with_args(&[] as &[&str]);
}