From 89a4c8a72fc9e751654456b4b71dbb891f85f09c Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sat, 5 Sep 2026 12:15:56 +0530 Subject: [PATCH] feat(overlay): implement transparent top-anchored 3-card NVIDIA ShadowPlay HUD with unified settings --- src/bin/vrec-ui.rs | 6 + src/hyprland_binds.rs | 9 +- src/overlay.rs | 1223 +++++++++++++++++++++++++---------------- 3 files changed, 772 insertions(+), 466 deletions(-) diff --git a/src/bin/vrec-ui.rs b/src/bin/vrec-ui.rs index 33d932e..e68a03e 100644 --- a/src/bin/vrec-ui.rs +++ b/src/bin/vrec-ui.rs @@ -208,6 +208,9 @@ fn main() -> Result<(), Box> { "--menu" => { ensure_daemon_running(); ensure_hotkeys_running(); + #[cfg(not(target_os = "windows"))] + vrec::overlay::show_menu_overlay(); + #[cfg(target_os = "windows")] vrec::overlay_egui::run_egui_overlay(); return Ok(()); } @@ -271,6 +274,9 @@ fn main() -> Result<(), Box> { } else { ensure_daemon_running(); ensure_hotkeys_running(); + #[cfg(not(target_os = "windows"))] + vrec::overlay::show_menu_overlay(); + #[cfg(target_os = "windows")] vrec::overlay_egui::run_egui_overlay(); return Ok(()); } diff --git a/src/hyprland_binds.rs b/src/hyprland_binds.rs index 653d16b..1ff8efd 100644 --- a/src/hyprland_binds.rs +++ b/src/hyprland_binds.rs @@ -69,13 +69,18 @@ pub fn register_hyprland_binds(config: &VrecConfig) { return; } - // Register overlay window rules so the UI floats and centers cleanly + // Register overlay window rules so the UI floats at the top cleanly let rules = [ "float, class:^(vrec-overlay)$", - "center, class:^(vrec-overlay)$", + "move 50% 45, class:^(vrec-overlay)$", "pin, class:^(vrec-overlay)$", "stayfocused, class:^(vrec-overlay)$", "noborder, class:^(vrec-overlay)$", + "float, class:^(vrec-hud)$", + "move 50% 45, class:^(vrec-hud)$", + "pin, class:^(vrec-hud)$", + "stayfocused, class:^(vrec-hud)$", + "noborder, class:^(vrec-hud)$", ]; for rule in rules { let _ = Command::new("hyprctl") diff --git a/src/overlay.rs b/src/overlay.rs index cfd290e..c6ea382 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -1,7 +1,8 @@ use gtk::prelude::*; use gtk::{ - Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider, Entry, Label, - Orientation, SpinButton, Stack, StackTransitionType, StyleContext, Switch, + Application, ApplicationWindow, Box, Button, CheckButton, ComboBoxText, CssProvider, + Entry, Label, Orientation, Scale, ScrolledWindow, SpinButton, Stack, + StackTransitionType, StyleContext, Switch, }; #[cfg(target_os = "linux")] use gtk_layer_shell::{Layer, LayerShell}; @@ -57,7 +58,7 @@ pub fn show_notification(message: &str) { let window = ApplicationWindow::builder() .application(app) .default_width(320) - .default_height(60) + .default_height(54) .build(); #[cfg(target_os = "linux")] @@ -65,23 +66,47 @@ pub fn show_notification(message: &str) { 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_layer_shell_margin(gtk_layer_shell::Edge::Top, 35); window.set_anchor(gtk_layer_shell::Edge::Top, true); } + if let Some(screen) = gdk::Screen::default() + && let Some(visual) = screen.rgba_visual() { + window.set_visual(Some(&visual)); + } + window.set_app_paintable(true); + window.connect_draw(|_, cr| { + cr.set_source_rgba(0.0, 0.0, 0.0, 0.0); + cr.set_operator(gtk::cairo::Operator::Source); + let _ = cr.paint(); + gtk::glib::Propagation::Proceed + }); + 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); + background-color: transparent !important; + background: transparent !important; } - label { + .notify-box { + background-color: rgba(18, 24, 34, 0.94); + border-radius: 12px; + border: 1px solid rgba(118, 185, 0, 0.4); + box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8); + padding: 8px 18px; + } + .notify-badge { + background-color: #76b900; + color: #0b1204; + font-size: 10px; + font-weight: 800; + border-radius: 4px; + padding: 2px 6px; + } + .notify-label { color: #ffffff; font-weight: 700; - font-size: 14px; - padding: 10px 24px; + font-size: 13px; } "#; let _ = css_provider.load_from_data(css.as_bytes()); @@ -93,8 +118,18 @@ pub fn show_notification(message: &str) { ); } + let hbox = Box::new(Orientation::Horizontal, 10); + hbox.style_context().add_class("notify-box"); + hbox.set_halign(gtk::Align::Center); + + let badge = Label::new(Some("VREC")); + badge.style_context().add_class("notify-badge"); let label = Label::new(Some(&msg_text)); - window.add(&label); + label.style_context().add_class("notify-label"); + + hbox.pack_start(&badge, false, false, 0); + hbox.pack_start(&label, false, false, 0); + window.add(&hbox); window.show_all(); let window_clone = window.clone(); @@ -128,17 +163,17 @@ pub fn show_menu_overlay() { let window = ApplicationWindow::builder() .application(app) - .default_width(820) - .default_height(480) + .default_width(780) + .default_height(230) .build(); #[cfg(target_os = "linux")] { window.init_layer_shell(); window.set_layer(Layer::Overlay); - window.set_namespace("vrec-hud"); + window.set_namespace("vrec-overlay"); window.set_keyboard_interactivity(true); - window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 50); + window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 45); window.set_anchor(gtk_layer_shell::Edge::Top, true); } @@ -150,43 +185,69 @@ pub fn show_menu_overlay() { window.set_position(gtk::WindowPosition::Center); } + // RGBA transparent visual setup + if let Some(screen) = gdk::Screen::default() + && let Some(visual) = screen.rgba_visual() { + window.set_visual(Some(&visual)); + } + window.set_app_paintable(true); + window.connect_draw(|_, cr| { + cr.set_source_rgba(0.0, 0.0, 0.0, 0.0); + cr.set_operator(gtk::cairo::Operator::Source); + let _ = cr.paint(); + gtk::glib::Propagation::Proceed + }); + let stack = Stack::new(); stack.set_transition_type(StackTransitionType::Crossfade); - stack.set_transition_duration(180); + stack.set_transition_duration(160); + stack.set_homogeneous(false); - // ========================================== - // 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); + // ========================================================================= + // PAGE 1: 3 TRANSLUCENT CARDS (NVIDIA ShadowPlay Overlay Style) + // ========================================================================= + let hud_page = Box::new(Orientation::Vertical, 10); + hud_page.style_context().add_class("hud-wrapper"); + hud_page.set_size_request(760, -1); - // Header Bar + // Header Bar (Floating Frosted Strip) 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"); + header_box.style_context().add_class("header-bar"); + + let brand_badge = Label::new(Some("VREC")); + brand_badge.style_context().add_class("vrec-badge"); + + let title_label = Label::new(Some("SHADOWPLAY OVERLAY")); + title_label.style_context().add_class("overlay-title"); + + let status_banner = Label::new(Some("")); + status_banner.style_context().add_class("status-banner"); + let spacer = Box::new(Orientation::Horizontal, 0); - let close_btn = Button::with_label("X"); + + let close_btn = Button::with_label("✕"); 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(&brand_badge, false, false, 0); + header_box.pack_start(&title_label, false, false, 4); + header_box.pack_start(&status_banner, false, false, 12); header_box.pack_start(&spacer, true, true, 0); header_box.pack_start(&close_btn, false, false, 0); - // 4 Action Cards Row + // 3 Convenient Floating Cards Row let cards_box = Box::new(Orientation::Horizontal, 14); + cards_box.style_context().add_class("cards-container"); cards_box.set_halign(gtk::Align::Center); - // Card 1: Replay + // ------------------------------------------------------------------------- + // CARD 1: INSTANT REPLAY + // ------------------------------------------------------------------------- let replay_card = Box::new(Orientation::Vertical, 8); replay_card.style_context().add_class("card-box"); + replay_card.set_size_request(235, 175); + let replay_hdr = Box::new(Orientation::Horizontal, 6); - let replay_title = Label::new(Some("REPLAY")); + let replay_title = Label::new(Some("INSTANT 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"); @@ -196,23 +257,36 @@ pub fn show_menu_overlay() { 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_dur_label = Label::new(Some(&format!("{}s Rolling Buffer", config.replay_duration_sec))); + replay_dur_label.style_context().add_class("card-subtitle"); 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); + replay_save_btn.style_context().add_class("action-btn"); + replay_save_btn.set_size_request(-1, 38); - // Card 2: Record + let replay_hotkey_hint = Label::new(Some("Ctrl + Shift + R")); + replay_hotkey_hint.style_context().add_class("hotkey-hint"); + + let replay_toggle_btn = Button::with_label(if config.replay_enabled { "Disable Replay" } else { "Enable Replay" }); + replay_toggle_btn.style_context().add_class("sub-toggle-btn"); + + replay_card.pack_start(&replay_hdr, false, false, 0); + replay_card.pack_start(&replay_dur_label, false, false, 2); + replay_card.pack_start(&replay_save_btn, false, false, 6); + replay_card.pack_start(&replay_hotkey_hint, false, false, 0); + replay_card.pack_start(&replay_toggle_btn, false, false, 4); + + // ------------------------------------------------------------------------- + // CARD 2: RECORD + // ------------------------------------------------------------------------- let record_card = Box::new(Orientation::Vertical, 8); record_card.style_context().add_class("card-box"); + record_card.set_size_request(235, 175); + 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")); + let record_pill = Label::new(Some("READY")); record_pill.style_context().add_class("status-pill"); record_pill.style_context().add_class("pill-idle"); let record_sp = Box::new(Orientation::Horizontal, 0); @@ -220,429 +294,608 @@ pub fn show_menu_overlay() { 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_info_label = Label::new(Some(&format!("{} FPS • {} Mbps", config.fps, config.record_bitrate_kbps / 1000))); + record_info_label.style_context().add_class("card-subtitle"); + + let record_action_btn = Button::with_label("Start Recording"); + record_action_btn.style_context().add_class("action-btn-secondary"); + record_action_btn.set_size_request(-1, 38); + + let record_hotkey_hint = Label::new(Some("Ctrl + Shift + F9")); + record_hotkey_hint.style_context().add_class("hotkey-hint"); + + let record_format_label = Label::new(Some(&format!("{} • {}", config.video_codec.to_uppercase(), config.audio_mode.to_uppercase()))); + record_format_label.style_context().add_class("sub-info-label"); - 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); + record_card.pack_start(&record_info_label, false, false, 2); + record_card.pack_start(&record_action_btn, false, false, 6); + record_card.pack_start(&record_hotkey_hint, false, false, 0); + record_card.pack_start(&record_format_label, false, false, 4); - // 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 + // ------------------------------------------------------------------------- + // CARD 3: SETTINGS + // ------------------------------------------------------------------------- let settings_card = Box::new(Orientation::Vertical, 8); settings_card.style_context().add_class("card-box"); + settings_card.set_size_request(235, 175); + let settings_hdr = Box::new(Orientation::Horizontal, 6); - let settings_title_hdr = Label::new(Some("PREFERENCES")); + let settings_title_hdr = Label::new(Some("SETTINGS")); settings_title_hdr.style_context().add_class("card-header-label"); - let settings_pill = Label::new(Some("TUNING")); + let settings_pill = Label::new(Some("CONFIG")); settings_pill.style_context().add_class("status-pill"); - settings_pill.style_context().add_class("pill-idle"); + settings_pill.style_context().add_class("pill-blue"); 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_sub = Label::new(Some("Cursor, Audio, Video & Storage")); + settings_sub.style_context().add_class("card-subtitle"); + 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"); + settings_open_btn.style_context().add_class("action-btn-secondary"); + settings_open_btn.set_size_request(-1, 38); + + let settings_hint = Label::new(Some("Alt + Z")); + settings_hint.style_context().add_class("hotkey-hint"); + + let settings_extra = Label::new(Some("All Preferences • Esc to Close")); + settings_extra.style_context().add_class("sub-info-label"); - 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); + settings_card.pack_start(&settings_sub, false, false, 2); + settings_card.pack_start(&settings_open_btn, false, false, 6); + settings_card.pack_start(&settings_hint, false, false, 0); + settings_card.pack_start(&settings_extra, false, false, 4); + // Pack the 3 cards 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); + // ========================================================================= + // PAGE 2: UNIFIED SETTINGS PANEL (Containing Mouse Cursor, Audio, Video, Storage) + // ========================================================================= + let settings_page = Box::new(Orientation::Vertical, 10); + settings_page.style_context().add_class("settings-panel"); + settings_page.set_size_request(760, -1); + // Settings Header Bar with Back Button 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"); + let back_btn = Button::with_label("< Back to Overlay"); + back_btn.style_context().add_class("back-btn"); + + let settings_page_title = Label::new(Some("RECORDER SETTINGS & HARDWARE TUNING")); + settings_page_title.style_context().add_class("settings-title"); + + let settings_spacer = Box::new(Orientation::Horizontal, 0); + + let settings_close_btn = Button::with_label("✕"); + settings_close_btn.style_context().add_class("close-btn"); + settings_header.pack_start(&back_btn, false, false, 0); - settings_header.pack_start(&settings_title, false, false, 0); + settings_header.pack_start(&settings_page_title, false, false, 8); + settings_header.pack_start(&settings_spacer, true, true, 0); + settings_header.pack_start(&settings_close_btn, false, false, 0); - let settings_grid = gtk::Grid::new(); - settings_grid.set_column_spacing(20); - settings_grid.set_row_spacing(10); + // Scrolled Settings Body + let scroll = ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); + scroll.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic); + scroll.set_min_content_height(420); + scroll.set_max_content_height(460); - // 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); + let settings_body = Box::new(Orientation::Vertical, 12); + settings_body.set_margin_start(6); + settings_body.set_margin_end(6); - // 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); + // ------------------------------------------------------------------------- + // SECTION 1: DISPLAY & CAPTURE (Mouse Cursor right at the top!) + // ------------------------------------------------------------------------- + let sec1_lbl = Label::new(Some("DISPLAY & CAPTURE")); + sec1_lbl.style_context().add_class("section-header"); + sec1_lbl.set_halign(gtk::Align::Start); + settings_body.pack_start(&sec1_lbl, 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); + let sec1_grid = gtk::Grid::new(); + sec1_grid.set_column_spacing(18); + sec1_grid.set_row_spacing(8); - // 3. Framerate (FPS) - Direct numeric spin button + preset pills! - let fps_lbl = Label::new(Some("Target Framerate (FPS):")); + // 1.1 Mouse Cursor Toggle + let cursor_lbl = Label::new(Some("Mouse Cursor:")); + cursor_lbl.set_halign(gtk::Align::Start); + let cursor_box = Box::new(Orientation::Horizontal, 10); + let cursor_check = CheckButton::with_label("Record Mouse Cursor"); + cursor_check.set_active(config.show_cursor); + let cursor_desc = Label::new(Some("(Include mouse pointer in video & replays)")); + cursor_desc.style_context().add_class("sub-info-label"); + cursor_box.pack_start(&cursor_check, false, false, 0); + cursor_box.pack_start(&cursor_desc, false, false, 0); + + // 1.2 Target Framerate (FPS) + let fps_lbl = Label::new(Some("Target Framerate:")); 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); + for &fps_val in &[30, 60, 120, 144] { + let p_btn = Button::with_label(&format!("{} FPS", fps_val)); + p_btn.style_context().add_class("preset-btn"); + let s_clone = fps_spin.clone(); + p_btn.connect_clicked(move |_| s_clone.set_value(fps_val as f64)); + fps_box.pack_start(&p_btn, 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); + // 1.3 Video Bitrate (Mbps) + let bit_lbl = Label::new(Some("Video Bitrate:")); + 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); + for &bit_val in &[10, 20, 30, 50] { + let p_btn = Button::with_label(&format!("{} Mbps", bit_val)); + p_btn.style_context().add_class("preset-btn"); + let s_clone = bit_spin.clone(); + p_btn.connect_clicked(move |_| s_clone.set_value(bit_val as f64)); + bit_box.pack_start(&p_btn, false, false, 0); + } - // 4. Video Codec + // 1.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("h264"), "H.264 / AVC (NVENC / VAAPI - Universal)"); codec_combo.append(Some("hevc"), "HEVC / H.265 (High Efficiency)"); - codec_combo.append(Some("av1"), "AV1 (Next-Gen High Quality)"); + codec_combo.append(Some("av1"), "AV1 (Next-Generation High Fidelity)"); 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:")); + sec1_grid.attach(&cursor_lbl, 0, 0, 1, 1); + sec1_grid.attach(&cursor_box, 1, 0, 1, 1); + sec1_grid.attach(&fps_lbl, 0, 1, 1, 1); + sec1_grid.attach(&fps_box, 1, 1, 1, 1); + sec1_grid.attach(&bit_lbl, 0, 2, 1, 1); + sec1_grid.attach(&bit_box, 1, 2, 1, 1); + sec1_grid.attach(&codec_lbl, 0, 3, 1, 1); + sec1_grid.attach(&codec_combo, 1, 3, 1, 1); + settings_body.pack_start(&sec1_grid, false, false, 0); + + // ------------------------------------------------------------------------- + // SECTION 2: INSTANT REPLAY BUFFER + // ------------------------------------------------------------------------- + let sec2_lbl = Label::new(Some("INSTANT REPLAY BUFFER")); + sec2_lbl.style_context().add_class("section-header"); + sec2_lbl.set_halign(gtk::Align::Start); + settings_body.pack_start(&sec2_lbl, false, false, 4); + + let sec2_grid = gtk::Grid::new(); + sec2_grid.set_column_spacing(18); + sec2_grid.set_row_spacing(8); + + let dur_lbl = Label::new(Some("Buffer Length:")); + 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); + for &(dur_val, dur_txt) in &[(15, "15s"), (30, "30s"), (60, "60s"), (120, "2m"), (300, "5m")] { + let p_btn = Button::with_label(dur_txt); + p_btn.style_context().add_class("preset-btn"); + let s_clone = dur_spin.clone(); + p_btn.connect_clicked(move |_| s_clone.set_value(dur_val as f64)); + dur_box.pack_start(&p_btn, false, false, 0); + } + + sec2_grid.attach(&dur_lbl, 0, 0, 1, 1); + sec2_grid.attach(&dur_box, 1, 0, 1, 1); + settings_body.pack_start(&sec2_grid, false, false, 0); + + // ------------------------------------------------------------------------- + // SECTION 3: AUDIO & SOUND ROUTING + // ------------------------------------------------------------------------- + let sec3_lbl = Label::new(Some("AUDIO & SOUND ROUTING")); + sec3_lbl.style_context().add_class("section-header"); + sec3_lbl.set_halign(gtk::Align::Start); + settings_body.pack_start(&sec3_lbl, false, false, 4); + + let sec3_grid = gtk::Grid::new(); + sec3_grid.set_column_spacing(18); + sec3_grid.set_row_spacing(8); + + let audio_mode_lbl = Label::new(Some("Audio 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.append(Some("both"), "Both Combined (System Sounds + Microphone)"); + audio_mode_combo.append(Some("muted"), "Muted (No Audio Recording)"); audio_mode_combo.set_active_id(Some(&config.audio_mode)); - // 6. Audio Device - let audio_dev_lbl = Label::new(Some("Audio Hardware Device:")); + let audio_dev_lbl = Label::new(Some("Input Device:")); audio_dev_lbl.set_halign(gtk::Align::Start); let audio_dev_combo = ComboBoxText::new(); - audio_dev_combo.append(Some("default"), "Default Device"); + audio_dev_combo.append(Some("default"), "Default Recording 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 sys_vol_lbl = Label::new(Some("System Volume:")); + sys_vol_lbl.set_halign(gtk::Align::Start); + let sys_vol_scale = Scale::with_range(Orientation::Horizontal, 0.0, 150.0, 5.0); + sys_vol_scale.set_value((config.system_volume * 100.0).round() as f64); + sys_vol_scale.set_size_request(240, -1); + + let mic_vol_lbl = Label::new(Some("Mic Volume:")); + mic_vol_lbl.set_halign(gtk::Align::Start); + let mic_vol_scale = Scale::with_range(Orientation::Horizontal, 0.0, 150.0, 5.0); + mic_vol_scale.set_value((config.mic_volume * 100.0).round() as f64); + mic_vol_scale.set_size_request(240, -1); + + sec3_grid.attach(&audio_mode_lbl, 0, 0, 1, 1); + sec3_grid.attach(&audio_mode_combo, 1, 0, 1, 1); + sec3_grid.attach(&audio_dev_lbl, 0, 1, 1, 1); + sec3_grid.attach(&audio_dev_combo, 1, 1, 1, 1); + sec3_grid.attach(&sys_vol_lbl, 0, 2, 1, 1); + sec3_grid.attach(&sys_vol_scale, 1, 2, 1, 1); + sec3_grid.attach(&mic_vol_lbl, 0, 3, 1, 1); + sec3_grid.attach(&mic_vol_scale, 1, 3, 1, 1); + settings_body.pack_start(&sec3_grid, false, false, 0); + + // ------------------------------------------------------------------------- + // SECTION 4: STORAGE & SHORTCUTS + // ------------------------------------------------------------------------- + let sec4_lbl = Label::new(Some("STORAGE & SHORTCUTS")); + sec4_lbl.style_context().add_class("section-header"); + sec4_lbl.set_halign(gtk::Align::Start); + settings_body.pack_start(&sec4_lbl, false, false, 4); + + let sec4_grid = gtk::Grid::new(); + sec4_grid.set_column_spacing(18); + sec4_grid.set_row_spacing(8); + let dir_lbl = Label::new(Some("Save Folder:")); dir_lbl.set_halign(gtk::Align::Start); + let dir_box = Box::new(Orientation::Horizontal, 8); let dir_entry = Entry::new(); dir_entry.set_text(&config.output_directory); + dir_entry.set_size_request(280, -1); + let dir_browse_btn = Button::with_label("Browse..."); + dir_browse_btn.style_context().add_class("preset-btn"); + + let dir_entry_clone = dir_entry.clone(); + dir_browse_btn.connect_clicked(move |_| { + let cur_path = dir_entry_clone.text().to_string(); + let (tx, rx) = std::sync::mpsc::channel::(); + let de = dir_entry_clone.clone(); + gtk::glib::timeout_add_local(Duration::from_millis(100), move || { + if let Ok(sel) = rx.try_recv() { + de.set_text(&sel); + gtk::glib::ControlFlow::Break + } else { + gtk::glib::ControlFlow::Continue + } + }); + std::thread::spawn(move || { + if let Ok(out) = std::process::Command::new("kdialog") + .args(["--getexistingdirectory", &cur_path]) + .output() + { + let sel = String::from_utf8_lossy(&out.stdout).trim().to_string(); + if !sel.is_empty() { + let _ = tx.send(sel); + } + } + }); + }); + + dir_box.pack_start(&dir_entry, true, true, 0); + dir_box.pack_start(&dir_browse_btn, false, false, 0); - // 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 rec_hk_lbl = Label::new(Some("Toggle Record Hotkey:")); + rec_hk_lbl.set_halign(gtk::Align::Start); + let rec_hk_entry = Entry::new(); + rec_hk_entry.set_text(&config.record_hotkey); - let menu_hk_lbl = Label::new(Some("Menu Overlay Hotkey:")); + let menu_hk_lbl = Label::new(Some("Overlay Menu 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 cur_hk_lbl = Label::new(Some("Toggle Cursor Hotkey:")); + cur_hk_lbl.set_halign(gtk::Align::Start); + let cur_hk_entry = Entry::new(); + cur_hk_entry.set_text(&config.cursor_hotkey); + 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); + sec4_grid.attach(&dir_lbl, 0, 0, 1, 1); + sec4_grid.attach(&dir_box, 1, 0, 1, 1); + sec4_grid.attach(&save_hk_lbl, 0, 1, 1, 1); + sec4_grid.attach(&save_hk_entry, 1, 1, 1, 1); + sec4_grid.attach(&rec_hk_lbl, 0, 2, 1, 1); + sec4_grid.attach(&rec_hk_entry, 1, 2, 1, 1); + sec4_grid.attach(&menu_hk_lbl, 0, 3, 1, 1); + sec4_grid.attach(&menu_hk_entry, 1, 3, 1, 1); + sec4_grid.attach(&cur_hk_lbl, 0, 4, 1, 1); + sec4_grid.attach(&cur_hk_entry, 1, 4, 1, 1); + sec4_grid.attach(&auto_lbl, 0, 5, 1, 1); + sec4_grid.attach(&auto_switch, 1, 5, 1, 1); + settings_body.pack_start(&sec4_grid, false, false, 0); - let save_settings_btn = Button::with_label("Apply & Save Settings"); - save_settings_btn.style_context().add_class("apply-btn"); + scroll.add(&settings_body); + + // Apply & Save Settings Button + let apply_btn = Button::with_label("Apply & Save Settings"); + apply_btn.style_context().add_class("action-btn"); + apply_btn.set_size_request(-1, 42); 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); + settings_page.pack_start(&scroll, true, true, 4); + settings_page.pack_start(&apply_btn, false, false, 4); + // Add Pages to Stack stack.add_named(&hud_page, "hud"); stack.add_named(&settings_page, "settings"); window.add(&stack); - // CSS Styling (Brushed Stealth HUD Theme) + // ========================================================================= + // CSS STYLING (Translucent NVIDIA Frosted Glass 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; + background-color: transparent !important; + background: transparent !important; } - .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 { + .hud-wrapper { background-color: transparent; - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 6px; - color: #94a3b8; + padding: 0px; + } + .header-bar { + background-color: rgba(14, 18, 26, 0.88); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 10px; + padding: 6px 14px; + box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.6); + } + .vrec-badge { + background-color: #76b900; + color: #0b1204; font-size: 11px; - font-weight: 600; - padding: 5px 10px; + font-weight: 900; + border-radius: 4px; + padding: 2px 7px; + letter-spacing: 0.5px; } - .sub-btn:hover { - background-color: rgba(255, 255, 255, 0.08); - color: #ffffff; + .overlay-title { + color: #f1f5f9; + font-size: 12px; + font-weight: 800; + letter-spacing: 1px; } - .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; + .status-banner { + color: #76b900; + font-size: 11.5px; 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; + font-size: 14px; + font-weight: 700; + padding: 2px 8px; } .close-btn:hover { - color: #f43f5e; + color: #ef4444; } - .sub-label { - color: #64748b; + .card-box { + background-color: rgba(18, 24, 34, 0.88); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 14px; + padding: 14px 16px; + box-shadow: 0px 12px 36px rgba(0, 0, 0, 0.7); + } + .card-box:hover { + background-color: rgba(24, 32, 45, 0.94); + border-color: rgba(118, 185, 0, 0.45); + } + .card-header-label { + color: #ffffff; + font-size: 11.5px; + font-weight: 800; + letter-spacing: 0.8px; + } + .card-subtitle { + color: #94a3b8; font-size: 11px; + font-weight: 600; + } + .status-pill { + font-size: 9.5px; + font-weight: 800; + padding: 2px 8px; + border-radius: 6px; + } + .pill-active { + background-color: rgba(118, 185, 0, 0.2); + color: #76b900; + border: 1px solid rgba(118, 185, 0, 0.5); + } + .pill-idle { + background-color: rgba(148, 163, 184, 0.14); + color: #94a3b8; + border: 1px solid rgba(148, 163, 184, 0.25); + } + .pill-rec { + background-color: rgba(239, 68, 68, 0.25); + color: #ef4444; + border: 1px solid rgba(239, 68, 68, 0.6); + } + .pill-blue { + background-color: rgba(56, 189, 248, 0.15); + color: #38bdf8; + border: 1px solid rgba(56, 189, 248, 0.4); + } + .action-btn { + background-color: #76b900; + color: #0b1204; + font-size: 13px; + font-weight: 800; + border-radius: 8px; + border: none; + padding: 8px 14px; + } + .action-btn:hover { + background-color: #8ce000; + } + .action-btn-secondary { + background-color: rgba(255, 255, 255, 0.08); + color: #ffffff; + font-size: 13px; + font-weight: 700; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.15); + padding: 8px 14px; + } + .action-btn-secondary:hover { + background-color: rgba(255, 255, 255, 0.14); + border-color: rgba(255, 255, 255, 0.3); + } + .action-btn-rec-stop { + background-color: #dc2626; + color: #ffffff; + font-size: 13px; + font-weight: 800; + border-radius: 8px; + border: none; + padding: 8px 14px; + } + .action-btn-rec-stop:hover { + background-color: #ef4444; + } + .sub-toggle-btn { + background-color: transparent; + color: #94a3b8; + font-size: 11px; + font-weight: 600; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + padding: 4px 8px; + } + .sub-toggle-btn:hover { + background-color: rgba(255, 255, 255, 0.08); + color: #ffffff; + } + .hotkey-hint { + color: #64748b; + font-size: 10px; + font-weight: 600; + } + .sub-info-label { + color: #64748b; + font-size: 10.5px; 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; + .settings-panel { + background-color: rgba(16, 22, 32, 0.94); + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 14px; + padding: 16px 22px; + box-shadow: 0px 16px 48px rgba(0, 0, 0, 0.85); + } + .settings-title { + color: #ffffff; font-size: 13px; + font-weight: 800; + letter-spacing: 1px; + } + .back-btn { + background-color: transparent; + color: #76b900; + font-size: 12px; + font-weight: 700; + border: 1px solid rgba(118, 185, 0, 0.4); + border-radius: 6px; + padding: 4px 10px; + } + .back-btn:hover { + background-color: rgba(118, 185, 0, 0.15); + color: #8ce000; + } + .section-header { + color: #76b900; + font-size: 11px; + font-weight: 800; + letter-spacing: 1px; + } + .preset-btn { + background-color: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + color: #cbd5e1; + font-size: 11px; + font-weight: 600; + padding: 3px 8px; + } + .preset-btn:hover { + background-color: rgba(118, 185, 0, 0.2); + border-color: #76b900; + color: #ffffff; + } + checkbutton check { + background-color: #1a2230; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + } + checkbutton check:checked { + background-color: #76b900; + border-color: #8ce000; + } + checkbutton label { + color: #ffffff; + font-size: 12px; + font-weight: 700; + } + entry, spinbutton, combobox button { + background-color: #161c28; + color: #f1f5f9; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 6px; + padding: 5px 8px; + font-size: 12px; font-weight: 600; } entry:focus, spinbutton:focus, combobox button:focus { - border-color: #3b82f6; - background-color: #1c202a; + border-color: #76b900; } - spinbutton button { - background-color: transparent; - color: #94a3b8; - border: none; + scale highlight { + background-color: #76b900; + border-radius: 3px; } - spinbutton button:hover { - color: #ffffff; + scale trough { + background-color: #1c2432; + border-radius: 3px; + } + scale slider { + background-color: #ffffff; + border-radius: 50%; + min-width: 14px; + min-height: 14px; } "#; let _ = css_provider.load_from_data(css.as_bytes()); @@ -654,166 +907,208 @@ pub fn show_menu_overlay() { ); } - // Window Close Triggers - let window_close = window.clone(); - close_btn.connect_clicked(move |_| { - window_close.close(); - }); + // ========================================================================= + // EVENT HANDLERS & NAVIGATION + // ========================================================================= + // Close Buttons + let w_close1 = window.clone(); + close_btn.connect_clicked(move |_| w_close1.close()); + let w_close2 = window.clone(); + settings_close_btn.connect_clicked(move |_| w_close2.close()); - let window_esc = window.clone(); + // Escape Key Closes + let w_esc = window.clone(); window.connect_key_press_event(move |_, key| { if key.keyval() == gdk::keys::constants::Escape { - window_esc.close(); + w_esc.close(); gtk::glib::Propagation::Stop } else { gtk::glib::Propagation::Proceed } }); - // Stack Navigation + // Switch to Settings Page let stack_to_settings = stack.clone(); + let w_to_settings = window.clone(); settings_open_btn.connect_clicked(move |_| { stack_to_settings.set_visible_child_name("settings"); + w_to_settings.resize(780, 560); }); + // Switch back to HUD Page let stack_to_hud = stack.clone(); + let w_to_hud = window.clone(); back_btn.connect_clicked(move |_| { stack_to_hud.set_visible_child_name("hud"); + w_to_hud.resize(780, 230); }); - // 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: Save Replay + let banner_save = status_banner.clone(); + replay_save_btn.connect_clicked(move |_| { + let _ = ipc::send_command(Command::SaveReplay); + banner_save.set_text("Replay saved successfully!"); + let b_clear = banner_save.clone(); + gtk::glib::timeout_add_local(Duration::from_millis(3000), move || { + b_clear.set_text(""); + gtk::glib::ControlFlow::Break + }); }); - // Action Handlers - let replay_lbl_btn = replay_toggle_btn.clone(); - let replay_pill_clone = replay_pill.clone(); + // Action: Toggle Replay Buffer + let r_btn_clone = replay_toggle_btn.clone(); + let r_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"); + r_btn_clone.set_label("Disable Replay"); + r_pill_clone.set_label("ACTIVE"); + r_pill_clone.style_context().remove_class("pill-idle"); + r_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"); + r_btn_clone.set_label("Enable Replay"); + r_pill_clone.set_label("OFF"); + r_pill_clone.style_context().remove_class("pill-active"); + r_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(|_| { + // Action: Toggle Record + record_action_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 |_| { + // Action: Direct Mouse Cursor Checkbox Toggle + let cursor_check_sync = cursor_check.clone(); + let banner_cur = status_banner.clone(); + cursor_check.connect_toggled(move |c| { + let active = c.is_active(); 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"); - } + if cfg.show_cursor != active { + cfg.show_cursor = active; + let _ = cfg.save(); + let _ = ipc::send_command(Command::ToggleCursor); + banner_cur.set_text(if active { "Cursor: Visible in recording" } else { "Cursor: Hidden from recording" }); + let b_clear = banner_cur.clone(); + gtk::glib::timeout_add_local(Duration::from_millis(3000), move || { + b_clear.set_text(""); + gtk::glib::ControlFlow::Break + }); } }); - // Periodic Status Sync (Timer & Live States) - let rec_btn_sync = record_toggle_btn.clone(); + // Action: Apply & Save Settings + let stack_after_save = stack.clone(); + let w_after_save = window.clone(); + let banner_after_save = status_banner.clone(); + let r_dur_lbl_sync = replay_dur_label.clone(); + let rec_info_lbl_sync = record_info_label.clone(); + let rec_fmt_lbl_sync = record_format_label.clone(); + + apply_btn.connect_clicked(move |_| { + let mut cfg = VrecConfig::load(); + cfg.show_cursor = cursor_check_sync.is_active(); + cfg.fps = fps_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; + if let Some(c) = codec_combo.active_id() { + cfg.video_codec = c.to_string(); + } + cfg.replay_duration_sec = dur_spin.value() as u32; + 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(); + } + cfg.system_volume = (sys_vol_scale.value() / 100.0) as f32; + cfg.mic_volume = (mic_vol_scale.value() / 100.0) as f32; + + let dir_val = dir_entry.text().to_string(); + if !dir_val.trim().is_empty() { + cfg.output_directory = dir_val; + } + cfg.save_hotkey = save_hk_entry.text().to_string(); + cfg.record_hotkey = rec_hk_entry.text().to_string(); + cfg.menu_hotkey = menu_hk_entry.text().to_string(); + cfg.cursor_hotkey = cur_hk_entry.text().to_string(); + cfg.autostart = auto_switch.is_active(); + + let _ = cfg.save(); + VrecConfig::notify_daemon_reload(); + + // Update HUD card labels + r_dur_lbl_sync.set_text(&format!("{}s Rolling Buffer", cfg.replay_duration_sec)); + rec_info_lbl_sync.set_text(&format!("{} FPS • {} Mbps", cfg.fps, cfg.record_bitrate_kbps / 1000)); + rec_fmt_lbl_sync.set_text(&format!("{} • {}", cfg.video_codec.to_uppercase(), cfg.audio_mode.to_uppercase())); + + banner_after_save.set_text("Settings saved!"); + let b_clear = banner_after_save.clone(); + gtk::glib::timeout_add_local(Duration::from_millis(3000), move || { + b_clear.set_text(""); + gtk::glib::ControlFlow::Break + }); + + stack_after_save.set_visible_child_name("hud"); + w_after_save.resize(780, 230); + }); + + // ========================================================================= + // LIVE STATUS POLLER (Syncs Daemon States & Timers Every 300ms) + // ========================================================================= + let rec_btn_sync = record_action_btn.clone(); let rec_pill_sync = record_pill.clone(); + let rec_info_sync = record_info_label.clone(); + let r_pill_sync = replay_pill.clone(); + let r_btn_sync = replay_toggle_btn.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 || { + gtk::glib::timeout_add_local(Duration::from_millis(300), 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; + if let Ok(st) = ipc::query_status() { + // Recording status sync + if st.is_recording { + let mins = st.recording_duration_sec / 60; + let secs = st.recording_duration_sec % 60; rec_btn_sync.set_label("Stop Recording"); - rec_btn_sync.style_context().add_class("recording-active"); + rec_btn_sync.style_context().remove_class("action-btn-secondary"); + rec_btn_sync.style_context().add_class("action-btn-rec-stop"); + 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"); + rec_pill_sync.style_context().add_class("pill-rec"); + + rec_info_sync.set_label(&format!("Recording {:02}:{:02}", mins, secs)); } 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_btn_sync.set_label("Start Recording"); + rec_btn_sync.style_context().remove_class("action-btn-rec-stop"); + rec_btn_sync.style_context().add_class("action-btn-secondary"); + + rec_pill_sync.set_label("READY"); + rec_pill_sync.style_context().remove_class("pill-rec"); rec_pill_sync.style_context().add_class("pill-idle"); } + + // Replay status sync + if st.is_replay_active { + r_pill_sync.set_label("ACTIVE"); + r_pill_sync.style_context().remove_class("pill-idle"); + r_pill_sync.style_context().add_class("pill-active"); + r_btn_sync.set_label("Disable Replay"); + } else { + r_pill_sync.set_label("OFF"); + r_pill_sync.style_context().remove_class("pill-active"); + r_pill_sync.style_context().add_class("pill-idle"); + r_btn_sync.set_label("Enable Replay"); + } } gtk::glib::ControlFlow::Continue });