use gtk::prelude::*; use gtk::{ Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider, DrawingArea, Entry, Fixed, Label, LevelBar, Orientation, Revealer, RevealerTransitionType, Scale, ScrolledWindow, SpinButton, Stack, StackTransitionType, StyleContext, Switch, }; #[cfg(target_os = "linux")] use gtk_layer_shell::{Layer, LayerShell}; use std::f64::consts::PI; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use crate::config::ScytheConfig; use crate::ipc::{self, Command}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ToastIcon { Replay, Record, Save, Cursor, Info, Error, } impl ToastIcon { pub fn from_name(name: &str) -> Self { match name.to_lowercase().as_str() { "replay" | "save_replay" => ToastIcon::Replay, "record" | "recording" | "start" => ToastIcon::Record, "save" | "saved" | "stop" => ToastIcon::Save, "cursor" => ToastIcon::Cursor, "error" => ToastIcon::Error, _ => ToastIcon::Info, } } } pub fn spawn_toast(title: &str, subtitle: &str, icon: ToastIcon) { let icon_str = match icon { ToastIcon::Replay => "replay", ToastIcon::Record => "record", ToastIcon::Save => "save", ToastIcon::Cursor => "cursor", ToastIcon::Error => "error", ToastIcon::Info => "info", }; let title_owned = title.to_string(); let subtitle_owned = subtitle.to_string(); let icon_owned = icon_str.to_string(); std::thread::spawn(move || { let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("scythe-ui")); let _ = std::process::Command::new(exe) .args(["--toast", &title_owned, &subtitle_owned, &icon_owned]) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn(); }); } pub fn show_notification_overlay() { show_shadowplay_toast("INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay); } pub fn show_notification(message: &str) { let lower = message.to_lowercase(); let (title, subtitle, icon) = if lower.contains("replay saved") || lower.contains("replay") { ("INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay) } else if lower.contains("recording started") { ("RECORDING", "Recording started", ToastIcon::Record) } else if lower.contains("recording saved") || lower.contains("stopped") { ("RECORDING", "Recording saved", ToastIcon::Save) } else if lower.contains("cursor") { ("MOUSE CURSOR", message, ToastIcon::Cursor) } else if lower.contains("error") { ("SCYTHE", message, ToastIcon::Error) } else { ("SCYTHE", message, ToastIcon::Info) }; show_shadowplay_toast(title, subtitle, icon); } pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { #[cfg(target_os = "windows")] { crate::overlay_egui::run_egui_toast(title, subtitle, icon); return; } #[cfg(not(target_os = "windows"))] { if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() { crate::overlay_egui::run_egui_toast(title, subtitle, icon); return; } if gtk::init().is_err() { crate::overlay_egui::run_egui_toast(title, subtitle, icon); return; } let app_id = format!("com.scythe.toast.p{}", std::process::id()); let app = Application::builder() .application_id(&app_id) .flags(gtk::gio::ApplicationFlags::NON_UNIQUE) .build(); let title_text = title.to_string(); let sub_text = subtitle.to_string(); let cfg = ScytheConfig::load(); let accent_name = cfg.accent_color.to_lowercase(); let (accent_hex, accent_rgb): (&str, (f64, f64, f64)) = match accent_name.as_str() { "green" | "emerald" => ("#22c55e", (0.133, 0.773, 0.369)), "cyan" | "ice" => ("#06b6d4", (0.024, 0.714, 0.831)), "purple" | "violet" => ("#a855f7", (0.659, 0.333, 0.969)), "amber" | "orange" => ("#f59e0b", (0.961, 0.620, 0.043)), "red" | "crimson" => ("#ef4444", (0.937, 0.267, 0.267)), "blue" | "sapphire" | _ => ("#38bdf8", (0.220, 0.741, 0.973)), }; let active_accent_hex = if icon == ToastIcon::Record { "#ef4444" } else { accent_hex }; app.connect_activate(move |app| { let window = ApplicationWindow::builder() .application(app) .default_width(340) .default_height(64) .build(); #[cfg(target_os = "linux")] let layer_shell_ok = gtk_layer_shell::is_supported(); #[cfg(not(target_os = "linux"))] let layer_shell_ok = false; if layer_shell_ok { window.init_layer_shell(); window.set_layer(Layer::Overlay); window.set_namespace("scythe-notification"); window.set_anchor(gtk_layer_shell::Edge::Top, true); window.set_anchor(gtk_layer_shell::Edge::Right, true); window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 24); window.set_layer_shell_margin(gtk_layer_shell::Edge::Right, 24); window.set_keyboard_interactivity(false); } else { window.set_decorated(false); window.set_keep_above(true); window.set_skip_taskbar_hint(true); window.set_accept_focus(false); if let Some(display) = gdk::Display::default() && let Some(mon) = display.primary_monitor().or_else(|| display.monitor(0)) { let geom = mon.geometry(); let x = geom.x() + geom.width() - 340 - 24; let y = geom.y() + 24; window.move_(x, y); } } 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 = format!( r#" window, window.background, .background {{ background-color: transparent !important; background: transparent !important; border: none !important; box-shadow: none !important; }} .shadowplay-toast {{ background-color: rgba(12, 13, 17, 0.82); border: 1px solid rgba(255, 255, 255, 0.12); border-left: 3.5px solid {accent}; border-radius: 0px !important; box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.85); padding: 8px 14px; }} .toast-title {{ color: #ffffff; font-size: 12px; font-weight: 800; letter-spacing: 0.6px; }} .toast-subtitle {{ color: #a1a1aa; font-size: 11px; font-weight: 500; }} "#, accent = active_accent_hex ); 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_USER, ); } window.style_context().add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_USER); let hbox = Box::new(Orientation::Horizontal, 12); hbox.style_context().add_class("shadowplay-toast"); hbox.set_size_request(320, 54); // Left DrawingArea for vector icon let icon_area = DrawingArea::new(); icon_area.set_size_request(32, 32); icon_area.set_valign(gtk::Align::Center); let icon_type = icon; icon_area.connect_draw(move |_, cr| { let cx = 16.0; let cy = 16.0; match icon_type { ToastIcon::Replay => { let r = 10.0; cr.set_source_rgb(accent_rgb.0, accent_rgb.1, accent_rgb.2); cr.set_line_width(2.2); cr.arc(cx, cy, r, 0.25 * PI, 1.80 * PI); let _ = cr.stroke(); let a_x = cx + r * (0.25 * PI).cos(); let a_y = cy + r * (0.25 * PI).sin(); cr.move_to(a_x, a_y); cr.line_to(a_x - 4.5, a_y + 0.5); cr.line_to(a_x - 0.5, a_y - 4.5); cr.close_path(); let _ = cr.fill(); let tri_r = 4.0; cr.move_to(cx + tri_r + 0.5, cy); cr.line_to(cx - tri_r * 0.6 + 0.5, cy - tri_r * 0.86); cr.line_to(cx - tri_r * 0.6 + 0.5, cy + tri_r * 0.86); cr.close_path(); let _ = cr.fill(); } ToastIcon::Record => { cr.set_source_rgba(0.937, 0.267, 0.267, 0.25); cr.arc(cx, cy, 13.0, 0.0, PI * 2.0); let _ = cr.fill(); cr.set_source_rgb(0.937, 0.267, 0.267); cr.set_line_width(1.8); cr.arc(cx, cy, 10.0, 0.0, PI * 2.0); let _ = cr.stroke(); cr.arc(cx, cy, 5.0, 0.0, PI * 2.0); let _ = cr.fill(); } ToastIcon::Save => { cr.set_source_rgb(accent_rgb.0, accent_rgb.1, accent_rgb.2); cr.set_line_width(2.5); cr.set_line_cap(gtk::cairo::LineCap::Round); cr.set_line_join(gtk::cairo::LineJoin::Round); cr.move_to(cx - 7.0, cy); cr.line_to(cx - 2.0, cy + 5.0); cr.line_to(cx + 7.0, cy - 5.0); let _ = cr.stroke(); } ToastIcon::Cursor => { cr.set_source_rgb(accent_rgb.0, accent_rgb.1, accent_rgb.2); cr.set_line_width(1.8); cr.move_to(cx - 6.0, cy - 8.0); cr.line_to(cx + 6.0, cy - 1.0); cr.line_to(cx, cy + 1.0); cr.line_to(cx + 2.0, cy + 7.0); cr.line_to(cx - 1.0, cy + 8.0); cr.line_to(cx - 3.0, cy + 2.0); cr.line_to(cx - 6.0, cy + 4.0); cr.close_path(); let _ = cr.fill(); } ToastIcon::Error => { cr.set_source_rgb(0.937, 0.267, 0.267); cr.set_line_width(2.2); cr.move_to(cx - 6.0, cy - 6.0); cr.line_to(cx + 6.0, cy + 6.0); cr.move_to(cx + 6.0, cy - 6.0); cr.line_to(cx - 6.0, cy + 6.0); let _ = cr.stroke(); } ToastIcon::Info => { cr.set_source_rgb(accent_rgb.0, accent_rgb.1, accent_rgb.2); cr.arc(cx, cy, 6.0, 0.0, PI * 2.0); let _ = cr.fill(); } } gtk::glib::Propagation::Proceed }); // Right text column let vbox = Box::new(Orientation::Vertical, 2); vbox.set_valign(gtk::Align::Center); let title_lbl = Label::new(Some(&title_text)); title_lbl.style_context().add_class("toast-title"); title_lbl.set_halign(gtk::Align::Start); let sub_lbl = Label::new(Some(&sub_text)); sub_lbl.style_context().add_class("toast-subtitle"); sub_lbl.set_halign(gtk::Align::Start); vbox.pack_start(&title_lbl, false, false, 0); vbox.pack_start(&sub_lbl, false, false, 0); hbox.pack_start(&icon_area, false, false, 0); hbox.pack_start(&vbox, true, true, 0); let fixed = Fixed::new(); fixed.set_size_request(340, 64); fixed.put(&hbox, 340, 5); window.add(&fixed); window.show_all(); let window_clone = window.clone(); let hbox_clone = hbox.clone(); let fixed_clone = fixed.clone(); let start_time = Instant::now(); gtk::glib::timeout_add_local(Duration::from_millis(16), move || { let elapsed = start_time.elapsed().as_secs_f32(); if elapsed >= 2.80 { window_clone.close(); return gtk::glib::ControlFlow::Break; } let slide_x = if elapsed < 0.35 { let t = (elapsed / 0.35).min(1.0); let ease = 1.0 - (1.0 - t).powi(3); (1.0 - ease) * 340.0 } else if elapsed < 2.40 { 0.0 } else { let t = ((elapsed - 2.40) / 0.40).min(1.0); let ease = t.powi(3); ease * 340.0 }; let target_x = (slide_x + 10.0).round() as i32; fixed_clone.move_(&hbox_clone, target_x, 5); gtk::glib::ControlFlow::Continue }); }); app.run_with_args(&[] as &[&str]); } } // Vector Icon Drawing Helpers (Centered & Antialiased) fn draw_replay_icon(cr: >k::cairo::Context, width: f64, height: f64, is_active: bool) { let cx = width / 2.0; let cy = height / 2.0; let r = 24.0; if is_active { cr.set_source_rgb(0.463, 0.725, 0.0); // #76b900 NVIDIA green } else { cr.set_source_rgb(0.60, 0.68, 0.78); // #94a3b8 } cr.set_line_width(3.8); cr.arc(cx, cy, r, 0.25 * PI, 1.80 * PI); let _ = cr.stroke(); // Arrowhead at start of arc let a_x = cx + r * (0.25 * PI).cos(); let a_y = cy + r * (0.25 * PI).sin(); cr.move_to(a_x, a_y); cr.line_to(a_x - 9.0, a_y + 1.0); cr.line_to(a_x - 1.0, a_y - 9.0); cr.close_path(); let _ = cr.fill(); // Centered play triangle let tri_r = 8.5; let tri_cx = cx + 1.0; let tri_cy = cy; cr.move_to(tri_cx + tri_r, tri_cy); cr.line_to(tri_cx - tri_r * 0.6, tri_cy - tri_r * 0.86); cr.line_to(tri_cx - tri_r * 0.6, tri_cy + tri_r * 0.86); cr.close_path(); let _ = cr.fill(); } fn draw_record_icon(cr: >k::cairo::Context, width: f64, height: f64, is_recording: bool) { let cx = width / 2.0; let cy = height / 2.0; if is_recording { // Glowing red recording indicator with halo cr.set_source_rgba(0.937, 0.267, 0.267, 0.25); cr.arc(cx, cy, 32.0, 0.0, PI * 2.0); let _ = cr.fill(); cr.set_source_rgb(0.937, 0.267, 0.267); // #ef4444 cr.set_line_width(3.5); cr.arc(cx, cy, 26.0, 0.0, PI * 2.0); let _ = cr.stroke(); cr.set_source_rgb(0.937, 0.267, 0.267); cr.arc(cx, cy, 13.0, 0.0, PI * 2.0); let _ = cr.fill(); } else { cr.set_source_rgb(0.60, 0.68, 0.78); // #94a3b8 cr.set_line_width(3.2); cr.arc(cx, cy, 25.0, 0.0, PI * 2.0); let _ = cr.stroke(); cr.set_source_rgb(0.88, 0.91, 0.94); cr.arc(cx, cy, 12.0, 0.0, PI * 2.0); let _ = cr.fill(); } } fn draw_gear_icon(cr: >k::cairo::Context, width: f64, height: f64) { let cx = width / 2.0; let cy = height / 2.0; let r = 24.0; cr.set_source_rgb(0.60, 0.68, 0.78); // #94a3b8 cr.set_line_width(3.0); cr.arc(cx, cy, r * 0.75, 0.0, PI * 2.0); let _ = cr.stroke(); cr.arc(cx, cy, r * 0.32, 0.0, PI * 2.0); let _ = cr.stroke(); for i in 0..8 { let angle = i as f64 * (PI / 4.0); let p_in_x = cx + angle.cos() * (r * 0.68); let p_in_y = cy + angle.sin() * (r * 0.68); let p_out_x = cx + angle.cos() * (r * 1.08); let p_out_y = cy + angle.sin() * (r * 1.08); cr.set_line_width(4.2); cr.move_to(p_in_x, p_in_y); cr.line_to(p_out_x, p_out_y); let _ = cr.stroke(); } } pub fn show_menu_overlay() { #[cfg(target_os = "windows")] { crate::overlay_egui::run_egui_overlay(); return; } #[cfg(not(target_os = "windows"))] { if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() { eprintln!("Warning: No display found for GTK menu overlay. Falling back to cross-platform egui overlay..."); crate::overlay_egui::run_egui_overlay(); return; } if gtk::init().is_err() { eprintln!("Note: GTK display init failed. Falling back to cross-platform egui overlay..."); crate::overlay_egui::run_egui_overlay(); return; } let app = Application::builder() .application_id("com.scythe.hud") .flags(gtk::gio::ApplicationFlags::NON_UNIQUE) .build(); app.connect_activate(|app| { let config = ScytheConfig::load(); let window = ApplicationWindow::builder() .application(app) .default_width(740) .default_height(280) .build(); #[cfg(target_os = "linux")] let layer_shell_ok = gtk_layer_shell::is_supported(); #[cfg(not(target_os = "linux"))] let layer_shell_ok = false; if layer_shell_ok { window.init_layer_shell(); window.set_layer(Layer::Overlay); window.set_namespace("scythe-overlay"); window.set_keyboard_interactivity(true); // Shifted higher towards the top (50px margin) per user feedback window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 50); window.set_anchor(gtk_layer_shell::Edge::Top, true); } else { window.set_decorated(false); window.set_keep_above(true); window.set_skip_taskbar_hint(true); window.set_position(gtk::WindowPosition::Center); if let Some(display) = gdk::Display::default() && let Some(mon) = display.primary_monitor().or_else(|| display.monitor(0)) { let geom = mon.geometry(); let x = geom.x() + (geom.width() - 760) / 2; let y = geom.y() + 50; window.move_(x, y); } } // 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(160); stack.set_homogeneous(false); // ========================================================================= // PAGE 1: 3 FROSTED GLASS CARDS WITH CONTEXTUAL DROPDOWNS // ========================================================================= let hud_page = Box::new(Orientation::Vertical, 10); hud_page.style_context().add_class("hud-wrapper"); hud_page.set_size_request(740, -1); // Top Bar (Frosted glass header strip, no "X" button) let header_box = Box::new(Orientation::Horizontal, 10); header_box.style_context().add_class("header-bar"); let brand_badge = Label::new(Some("SCYTHE")); brand_badge.style_context().add_class("scythe-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 esc_hint = Label::new(Some("Esc to Close")); esc_hint.style_context().add_class("esc-pill"); 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(&esc_hint, false, false, 0); // Row of 3 Cards let cards_box = Box::new(Orientation::Horizontal, 16); cards_box.style_context().add_class("cards-container"); cards_box.set_halign(gtk::Align::Center); // State Trackers for Vector Redraws let replay_active_state = Arc::new(AtomicBool::new(config.replay_enabled)); let record_active_state = Arc::new(AtomicBool::new(false)); // ------------------------------------------------------------------------- // CARD 1: INSTANT REPLAY // ------------------------------------------------------------------------- let replay_col = Box::new(Orientation::Vertical, 0); replay_col.set_size_request(225, -1); let replay_card_btn = Button::new(); replay_card_btn.style_context().add_class("card-btn"); replay_card_btn.set_size_request(225, 195); let replay_card_inner = Box::new(Orientation::Vertical, 6); replay_card_inner.set_valign(gtk::Align::Center); replay_card_inner.set_halign(gtk::Align::Center); let replay_title_lbl = Label::new(Some("INSTANT REPLAY")); replay_title_lbl.style_context().add_class("card-title"); let replay_icon_area = DrawingArea::new(); replay_icon_area.set_size_request(76, 76); replay_icon_area.set_valign(gtk::Align::Center); replay_icon_area.set_halign(gtk::Align::Center); let r_state_clone = Arc::clone(&replay_active_state); replay_icon_area.connect_draw(move |w, cr| { let width = w.allocated_width() as f64; let height = w.allocated_height() as f64; draw_replay_icon(cr, width, height, r_state_clone.load(Ordering::Relaxed)); gtk::glib::Propagation::Proceed }); let replay_status_lbl = Label::new(Some(if config.replay_enabled { "On" } else { "Off" })); replay_status_lbl.style_context().add_class("card-status"); if config.replay_enabled { replay_status_lbl.style_context().add_class("status-green"); } let replay_sub_lbl = Label::new(Some(&format!("{}s Buffer", config.replay_duration_sec))); replay_sub_lbl.style_context().add_class("card-sub"); replay_card_inner.pack_start(&replay_title_lbl, false, false, 0); replay_card_inner.pack_start(&replay_icon_area, false, false, 6); replay_card_inner.pack_start(&replay_status_lbl, false, false, 0); replay_card_inner.pack_start(&replay_sub_lbl, false, false, 2); replay_card_btn.add(&replay_card_inner); // Instant Replay Attached Dropdown Menu let replay_revealer = Revealer::new(); replay_revealer.set_transition_type(RevealerTransitionType::SlideDown); replay_revealer.set_transition_duration(150); let replay_menu_box = Box::new(Orientation::Vertical, 3); replay_menu_box.style_context().add_class("dropdown-menu"); let replay_toggle_item = Button::with_label(if config.replay_enabled { "Turn off" } else { "Turn on" }); replay_toggle_item.style_context().add_class("dropdown-item"); let replay_save_item = Button::with_label("Save (Ctrl+Shift+R)"); replay_save_item.style_context().add_class("dropdown-item"); let replay_settings_item = Button::with_label("Settings"); replay_settings_item.style_context().add_class("dropdown-item"); replay_menu_box.pack_start(&replay_toggle_item, false, false, 0); replay_menu_box.pack_start(&replay_save_item, false, false, 0); replay_menu_box.pack_start(&replay_settings_item, false, false, 0); replay_revealer.add(&replay_menu_box); replay_col.pack_start(&replay_card_btn, false, false, 0); replay_col.pack_start(&replay_revealer, false, false, 0); // ------------------------------------------------------------------------- // CARD 2: RECORD // ------------------------------------------------------------------------- let record_col = Box::new(Orientation::Vertical, 0); record_col.set_size_request(225, -1); let record_card_btn = Button::new(); record_card_btn.style_context().add_class("card-btn"); record_card_btn.set_size_request(225, 195); let record_card_inner = Box::new(Orientation::Vertical, 6); record_card_inner.set_valign(gtk::Align::Center); record_card_inner.set_halign(gtk::Align::Center); let record_title_lbl = Label::new(Some("RECORD")); record_title_lbl.style_context().add_class("card-title"); let record_icon_area = DrawingArea::new(); record_icon_area.set_size_request(76, 76); record_icon_area.set_valign(gtk::Align::Center); record_icon_area.set_halign(gtk::Align::Center); let rec_state_clone = Arc::clone(&record_active_state); record_icon_area.connect_draw(move |w, cr| { let width = w.allocated_width() as f64; let height = w.allocated_height() as f64; draw_record_icon(cr, width, height, rec_state_clone.load(Ordering::Relaxed)); gtk::glib::Propagation::Proceed }); let record_status_lbl = Label::new(Some("Not recording")); record_status_lbl.style_context().add_class("card-status"); let record_sub_lbl = Label::new(Some(&format!("{} FPS • {} Mbps", config.fps, config.record_bitrate_kbps / 1000))); record_sub_lbl.style_context().add_class("card-sub"); record_card_inner.pack_start(&record_title_lbl, false, false, 0); record_card_inner.pack_start(&record_icon_area, false, false, 6); record_card_inner.pack_start(&record_status_lbl, false, false, 0); record_card_inner.pack_start(&record_sub_lbl, false, false, 2); record_card_btn.add(&record_card_inner); // Record Attached Dropdown Menu let record_revealer = Revealer::new(); record_revealer.set_transition_type(RevealerTransitionType::SlideDown); record_revealer.set_transition_duration(150); let record_menu_box = Box::new(Orientation::Vertical, 3); record_menu_box.style_context().add_class("dropdown-menu"); let record_toggle_item = Button::with_label("Start (Ctrl+Shift+F9)"); record_toggle_item.style_context().add_class("dropdown-item"); let record_settings_item = Button::with_label("Settings"); record_settings_item.style_context().add_class("dropdown-item"); record_menu_box.pack_start(&record_toggle_item, false, false, 0); record_menu_box.pack_start(&record_settings_item, false, false, 0); record_revealer.add(&record_menu_box); record_col.pack_start(&record_card_btn, false, false, 0); record_col.pack_start(&record_revealer, false, false, 0); // ------------------------------------------------------------------------- // CARD 3: SETTINGS // ------------------------------------------------------------------------- let settings_col = Box::new(Orientation::Vertical, 0); settings_col.set_size_request(225, -1); let settings_card_btn = Button::new(); settings_card_btn.style_context().add_class("card-btn"); settings_card_btn.set_size_request(225, 195); let settings_card_inner = Box::new(Orientation::Vertical, 6); settings_card_inner.set_valign(gtk::Align::Center); settings_card_inner.set_halign(gtk::Align::Center); let settings_title_lbl = Label::new(Some("SETTINGS")); settings_title_lbl.style_context().add_class("card-title"); let settings_icon_area = DrawingArea::new(); settings_icon_area.set_size_request(76, 76); settings_icon_area.set_valign(gtk::Align::Center); settings_icon_area.set_halign(gtk::Align::Center); settings_icon_area.connect_draw(|w, cr| { let width = w.allocated_width() as f64; let height = w.allocated_height() as f64; draw_gear_icon(cr, width, height); gtk::glib::Propagation::Proceed }); let settings_status_lbl = Label::new(Some("Preferences")); settings_status_lbl.style_context().add_class("card-status"); let settings_sub_lbl = Label::new(Some("Audio, Quality & Hotkeys")); settings_sub_lbl.style_context().add_class("card-sub"); settings_card_inner.pack_start(&settings_title_lbl, false, false, 0); settings_card_inner.pack_start(&settings_icon_area, false, false, 6); settings_card_inner.pack_start(&settings_status_lbl, false, false, 0); settings_card_inner.pack_start(&settings_sub_lbl, false, false, 2); settings_card_btn.add(&settings_card_inner); settings_col.pack_start(&settings_card_btn, false, false, 0); // Pack 3 columns cards_box.pack_start(&replay_col, false, false, 0); cards_box.pack_start(&record_col, false, false, 0); cards_box.pack_start(&settings_col, false, false, 0); hud_page.pack_start(&header_box, false, false, 0); hud_page.pack_start(&cards_box, true, true, 0); // ========================================================================= // PAGE 2: REVAMPED CLEAN FROSTED GLASS SETTINGS PANEL // ========================================================================= let settings_page = Box::new(Orientation::Vertical, 12); settings_page.style_context().add_class("settings-panel"); settings_page.set_size_request(760, -1); // Header with Back Button (No "X" button) let settings_header = Box::new(Orientation::Horizontal, 12); 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")); settings_page_title.style_context().add_class("settings-title"); let settings_spacer = Box::new(Orientation::Horizontal, 0); let settings_esc_hint = Label::new(Some("Esc to Close")); settings_esc_hint.style_context().add_class("esc-pill"); settings_header.pack_start(&back_btn, false, false, 0); settings_header.pack_start(&settings_page_title, false, false, 10); settings_header.pack_start(&settings_spacer, true, true, 0); settings_header.pack_start(&settings_esc_hint, false, false, 0); // Scrollable 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(440); scroll.set_max_content_height(480); let settings_body = Box::new(Orientation::Vertical, 10); settings_body.set_margin_start(4); settings_body.set_margin_end(4); // ------------------------------------------------------------------------- // SECTION 1: DISPLAY & CAPTURE (With Mouse Cursor Switch!) // ------------------------------------------------------------------------- let sec1_card = Box::new(Orientation::Vertical, 8); sec1_card.style_context().add_class("settings-section-card"); let sec1_hdr = Label::new(Some("DISPLAY & CAPTURE")); sec1_hdr.style_context().add_class("section-header"); sec1_hdr.set_halign(gtk::Align::Start); sec1_card.pack_start(&sec1_hdr, false, false, 0); // 1.1 Mouse Cursor Toggle Row let cursor_row = Box::new(Orientation::Horizontal, 12); let cursor_text_box = Box::new(Orientation::Vertical, 2); let cursor_title = Label::new(Some("Record Mouse Cursor")); cursor_title.style_context().add_class("setting-row-title"); cursor_title.set_halign(gtk::Align::Start); let cursor_desc = Label::new(Some("Capture the mouse pointer in gameplay recordings and instant replays")); cursor_desc.style_context().add_class("sub-info-label"); cursor_desc.set_halign(gtk::Align::Start); cursor_text_box.pack_start(&cursor_title, false, false, 0); cursor_text_box.pack_start(&cursor_desc, false, false, 0); let cursor_switch = Switch::new(); cursor_switch.set_active(config.show_cursor); cursor_switch.set_valign(gtk::Align::Center); let cursor_sp = Box::new(Orientation::Horizontal, 0); cursor_row.pack_start(&cursor_text_box, false, false, 0); cursor_row.pack_start(&cursor_sp, true, true, 0); cursor_row.pack_start(&cursor_switch, false, false, 0); sec1_card.pack_start(&cursor_row, false, false, 4); let sec1_grid = gtk::Grid::new(); sec1_grid.set_column_spacing(18); sec1_grid.set_row_spacing(8); // 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); } // 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); } // 1.4 Video Codec let codec_lbl = Label::new(Some("Encoder Codec:")); codec_lbl.set_halign(gtk::Align::Start); let codec_combo = ComboBoxText::new(); codec_combo.append(Some("h264"), "H.264 / AVC (Fast & Universal)"); codec_combo.append(Some("hevc"), "HEVC / H.265 (High Efficiency)"); codec_combo.append(Some("av1"), "AV1 (Next-Generation Quality)"); codec_combo.set_active_id(Some(&config.video_codec)); sec1_grid.attach(&fps_lbl, 0, 0, 1, 1); sec1_grid.attach(&fps_box, 1, 0, 1, 1); sec1_grid.attach(&bit_lbl, 0, 1, 1, 1); sec1_grid.attach(&bit_box, 1, 1, 1, 1); sec1_grid.attach(&codec_lbl, 0, 2, 1, 1); sec1_grid.attach(&codec_combo, 1, 2, 1, 1); sec1_card.pack_start(&sec1_grid, false, false, 2); settings_body.pack_start(&sec1_card, false, false, 0); // ------------------------------------------------------------------------- // SECTION 2: INSTANT REPLAY BUFFER // ------------------------------------------------------------------------- let sec2_card = Box::new(Orientation::Vertical, 8); sec2_card.style_context().add_class("settings-section-card"); let sec2_hdr = Label::new(Some("INSTANT REPLAY BUFFER")); sec2_hdr.style_context().add_class("section-header"); sec2_hdr.set_halign(gtk::Align::Start); sec2_card.pack_start(&sec2_hdr, false, false, 0); 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); sec2_card.pack_start(&sec2_grid, false, false, 2); settings_body.pack_start(&sec2_card, false, false, 0); // ------------------------------------------------------------------------- // SECTION 3: AUDIO ROUTING & LEVELS // ------------------------------------------------------------------------- let sec3_card = Box::new(Orientation::Vertical, 8); sec3_card.style_context().add_class("settings-section-card"); let sec3_hdr = Label::new(Some("AUDIO ROUTING & SOUND")); sec3_hdr.style_context().add_class("section-header"); sec3_hdr.set_halign(gtk::Align::Start); sec3_card.pack_start(&sec3_hdr, false, false, 0); 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 Combined (System Sounds + Microphone)"); audio_mode_combo.append(Some("muted"), "Muted (No Audio)"); audio_mode_combo.set_active_id(Some(&config.audio_mode)); let audio_dev_lbl = Label::new(Some("Recording Device:")); audio_dev_lbl.set_halign(gtk::Align::Start); let audio_dev_combo = ComboBoxText::new(); 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); } for app in crate::capture::audio::list_application_audio() { audio_dev_combo.append(Some(&format!("app:{}", app)), &format!("App: {}", app)); } audio_dev_combo.set_active_id(Some(&config.audio_device)); 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 sys_level_bar = LevelBar::new(); sys_level_bar.set_min_value(0.0); sys_level_bar.set_max_value(1.0); sys_level_bar.set_size_request(240, 6); 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); let mic_level_bar = LevelBar::new(); mic_level_bar.set_min_value(0.0); mic_level_bar.set_max_value(1.0); mic_level_bar.set_size_request(240, 6); 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(&sys_level_bar, 1, 3, 1, 1); sec3_grid.attach(&mic_vol_lbl, 0, 4, 1, 1); sec3_grid.attach(&mic_vol_scale, 1, 4, 1, 1); sec3_grid.attach(&mic_level_bar, 1, 5, 1, 1); sec3_card.pack_start(&sec3_grid, false, false, 2); settings_body.pack_start(&sec3_card, false, false, 0); // ------------------------------------------------------------------------- // SECTION 4: STORAGE & SHORTCUTS // ------------------------------------------------------------------------- let sec4_card = Box::new(Orientation::Vertical, 8); sec4_card.style_context().add_class("settings-section-card"); let sec4_hdr = Label::new(Some("STORAGE & SHORTCUTS")); sec4_hdr.style_context().add_class("section-header"); sec4_hdr.set_halign(gtk::Align::Start); sec4_card.pack_start(&sec4_hdr, false, false, 0); 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); 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 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("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); 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); 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); sec4_card.pack_start(&sec4_grid, false, false, 2); settings_body.pack_start(&sec4_card, false, false, 0); scroll.add(&settings_body); // Apply & Save Settings Button let apply_btn = Button::with_label("Apply & Save Settings"); apply_btn.style_context().add_class("apply-save-btn"); apply_btn.set_size_request(-1, 44); settings_page.pack_start(&settings_header, false, false, 0); 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 (Translucent Frosted Glass Theme) // ========================================================================= let css_provider = CssProvider::new(); let css = r#" window, window.background, .background { background-color: transparent !important; background: transparent !important; border: none !important; box-shadow: none !important; } .hud-wrapper { background-color: transparent; padding: 0px; } /* Floating Frosted Glass Top Bar */ .header-bar { background-color: rgba(18, 26, 38, 0.72); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 12px; padding: 8px 18px; box-shadow: 0px 12px 32px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.16); } .scythe-badge, .vrec-badge { background-color: #76b900; color: #0b1204; font-size: 11px; font-weight: 900; border-radius: 4px; padding: 2px 7px; letter-spacing: 0.5px; } .overlay-title { color: #f1f5f9; font-size: 12.5px; font-weight: 800; letter-spacing: 1.2px; } .status-banner { color: #76b900; font-size: 11.5px; font-weight: 700; } .esc-pill { background-color: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.14); color: #94a3b8; font-size: 10.5px; font-weight: 700; border-radius: 6px; padding: 3px 8px; } /* Frosted Glass Action Cards */ .card-btn { background-color: rgba(20, 28, 42, 0.68); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 16px; padding: 18px; box-shadow: 0px 16px 40px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.18); } .card-btn:hover { background-color: rgba(26, 38, 56, 0.82); border-color: #76b900; box-shadow: 0 20px 48px rgba(0, 0, 0, 0.65), 0 0 20px rgba(118, 185, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.25); } .card-btn-active { background-color: rgba(26, 38, 56, 0.90); border-color: #76b900; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; box-shadow: 0 0 20px rgba(118, 185, 0, 0.35); } .card-title { color: #ffffff; font-size: 11.5px; font-weight: 800; letter-spacing: 0.8px; } .card-status { color: #94a3b8; font-size: 11.5px; font-weight: 700; } .status-green { color: #76b900 !important; } .status-red { color: #ef4444 !important; } .card-sub { color: #64748b; font-size: 10.5px; font-weight: 500; } /* Attached Frosted Dropdown Menus */ .dropdown-menu { background-color: rgba(16, 22, 34, 0.88); border: 1px solid rgba(255, 255, 255, 0.15); border-top: none; border-bottom-left-radius: 14px; border-bottom-right-radius: 14px; padding: 8px; box-shadow: 0px 20px 48px rgba(0, 0, 0, 0.75); } .dropdown-item { background-color: transparent; color: #e2e8f0; font-size: 12.5px; font-weight: 700; border-radius: 8px; border: 1px solid transparent; padding: 9px 14px; } .dropdown-item:hover { background-color: rgba(118, 185, 0, 0.15); border-color: rgba(118, 185, 0, 0.5); color: #76b900; } /* Frosted Settings Panel */ .settings-panel { background-color: #0f1620; border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 18px; padding: 20px 26px; box-shadow: 0px 24px 64px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.2); } .settings-section-card { background-color: #18202e; border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 14px 18px; } .settings-title { color: #ffffff; font-size: 13.5px; font-weight: 800; letter-spacing: 1px; } .setting-row-title { color: #ffffff; font-size: 13px; font-weight: 700; } .back-btn { background-color: rgba(118, 185, 0, 0.12); color: #76b900; font-size: 12px; font-weight: 800; border: 1px solid rgba(118, 185, 0, 0.45); border-radius: 8px; padding: 6px 14px; } .back-btn:hover { background-color: rgba(118, 185, 0, 0.22); 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.12); border-radius: 6px; color: #cbd5e1; font-size: 11px; font-weight: 600; padding: 4px 10px; } .preset-btn:hover { background-color: rgba(118, 185, 0, 0.22); border-color: #76b900; color: #ffffff; } .apply-save-btn { background-color: #76b900; color: #0b1204; font-size: 13px; font-weight: 800; border-radius: 8px; border: none; padding: 10px 16px; box-shadow: 0 4px 16px rgba(118, 185, 0, 0.35); } .apply-save-btn:hover { background-color: #8ce000; } switch { border-radius: 14px; background-color: #1e2838; border: 1px solid rgba(255, 255, 255, 0.2); } switch:checked { background-color: #76b900; border-color: #8ce000; } switch slider { background-color: #ffffff; border-radius: 50%; min-width: 18px; min-height: 18px; } entry, spinbutton, combobox button { background-color: rgba(18, 24, 36, 0.7); color: #f1f5f9; border: 1px solid rgba(255, 255, 255, 0.14); border-radius: 6px; padding: 5px 8px; font-size: 12px; font-weight: 600; } entry:focus, spinbutton:focus, combobox button:focus { border-color: #76b900; } scale highlight { background-color: #76b900; border-radius: 3px; } scale trough { background-color: #1c2432; border-radius: 3px; } scale slider { background-color: #ffffff; border-radius: 50%; min-width: 14px; min-height: 14px; } .sub-info-label { color: #94a3b8; font-size: 10.5px; font-weight: 500; } "#; 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_USER, ); } window.style_context().add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_USER); // ========================================================================= // EVENT HANDLERS & NAVIGATION (Esc key closes cleanly) // ========================================================================= let w_esc = window.clone(); window.connect_key_press_event(move |_, key| { if key.keyval() == gdk::keys::constants::Escape { w_esc.close(); std::process::exit(0); } else { gtk::glib::Propagation::Proceed } }); // ------------------------------------------------------------------------- // CARD INTERACTION: TOGGLE ATTACHED DROPDOWNS // ------------------------------------------------------------------------- let rev_r_click = replay_revealer.clone(); let rev_rec_close = record_revealer.clone(); let card_r_btn = replay_card_btn.clone(); let card_rec_btn = record_card_btn.clone(); replay_card_btn.connect_clicked(move |_| { let next_rev = !rev_r_click.reveals_child(); rev_r_click.set_reveal_child(next_rev); rev_rec_close.set_reveal_child(false); card_rec_btn.style_context().remove_class("card-btn-active"); if next_rev { card_r_btn.style_context().add_class("card-btn-active"); } else { card_r_btn.style_context().remove_class("card-btn-active"); } }); let rev_rec_click = record_revealer.clone(); let rev_r_close = replay_revealer.clone(); let card_rec_btn2 = record_card_btn.clone(); let card_r_btn2 = replay_card_btn.clone(); record_card_btn.connect_clicked(move |_| { let next_rev = !rev_rec_click.reveals_child(); rev_rec_click.set_reveal_child(next_rev); rev_r_close.set_reveal_child(false); card_r_btn2.style_context().remove_class("card-btn-active"); if next_rev { card_rec_btn2.style_context().add_class("card-btn-active"); } else { card_rec_btn2.style_context().remove_class("card-btn-active"); } }); let stack_to_settings = stack.clone(); let w_to_settings = window.clone(); settings_card_btn.connect_clicked(move |_| { stack_to_settings.set_visible_child_name("settings"); w_to_settings.resize(760, 560); }); // Back to HUD from Settings 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(740, 280); }); // Replay Menu Items let r_toggle_lbl = replay_toggle_item.clone(); let r_status_lbl_sync = replay_status_lbl.clone(); let r_state_sync = Arc::clone(&replay_active_state); let r_icon_sync = replay_icon_area.clone(); let rev_r_hide = replay_revealer.clone(); let card_r_deactivate = replay_card_btn.clone(); replay_toggle_item.connect_clicked(move |_| { let mut cfg = ScytheConfig::load(); cfg.replay_enabled = !cfg.replay_enabled; let _ = cfg.save(); ScytheConfig::notify_daemon_reload(); r_state_sync.store(cfg.replay_enabled, Ordering::Relaxed); r_icon_sync.queue_draw(); if cfg.replay_enabled { r_toggle_lbl.set_label("Turn off"); r_status_lbl_sync.set_label("On"); r_status_lbl_sync.style_context().add_class("status-green"); } else { r_toggle_lbl.set_label("Turn on"); r_status_lbl_sync.set_label("Off"); r_status_lbl_sync.style_context().remove_class("status-green"); } rev_r_hide.set_reveal_child(false); card_r_deactivate.style_context().remove_class("card-btn-active"); }); let banner_save = status_banner.clone(); let rev_r_hide2 = replay_revealer.clone(); let card_r_deactivate2 = replay_card_btn.clone(); replay_save_item.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 }); rev_r_hide2.set_reveal_child(false); card_r_deactivate2.style_context().remove_class("card-btn-active"); }); let stack_r_settings = stack.clone(); let w_r_settings = window.clone(); let rev_r_hide3 = replay_revealer.clone(); let card_r_deactivate3 = replay_card_btn.clone(); replay_settings_item.connect_clicked(move |_| { rev_r_hide3.set_reveal_child(false); card_r_deactivate3.style_context().remove_class("card-btn-active"); stack_r_settings.set_visible_child_name("settings"); w_r_settings.resize(760, 560); }); // Record Menu Items let rev_rec_hide = record_revealer.clone(); let card_rec_deactivate = record_card_btn.clone(); record_toggle_item.connect_clicked(move |_| { let _ = ipc::send_command(Command::ToggleRecording); rev_rec_hide.set_reveal_child(false); card_rec_deactivate.style_context().remove_class("card-btn-active"); }); let stack_rec_settings = stack.clone(); let w_rec_settings = window.clone(); let rev_rec_hide2 = record_revealer.clone(); let card_rec_deactivate2 = record_card_btn.clone(); record_settings_item.connect_clicked(move |_| { rev_rec_hide2.set_reveal_child(false); card_rec_deactivate2.style_context().remove_class("card-btn-active"); stack_rec_settings.set_visible_child_name("settings"); w_rec_settings.resize(760, 560); }); // Direct Mouse Cursor Switch Toggle let banner_cur = status_banner.clone(); cursor_switch.connect_state_set(move |_, active| { let mut cfg = ScytheConfig::load(); 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 }); } gtk::glib::Propagation::Proceed }); // Apply & Save Settings let stack_after_save = stack.clone(); let w_after_save = window.clone(); let banner_after_save = status_banner.clone(); let r_sub_lbl_sync = replay_sub_lbl.clone(); let rec_sub_lbl_sync = record_sub_lbl.clone(); let cur_sw_sync = cursor_switch.clone(); apply_btn.connect_clicked(move |_| { let mut cfg = ScytheConfig::load(); cfg.show_cursor = cur_sw_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(); ScytheConfig::notify_daemon_reload(); r_sub_lbl_sync.set_text(&format!("{}s Buffer", cfg.replay_duration_sec)); rec_sub_lbl_sync.set_text(&format!("{} FPS • {} Mbps", cfg.fps, cfg.record_bitrate_kbps / 1000)); 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(740, 280); }); // ========================================================================= // LIVE STATUS POLLER (Syncs Daemon States, Vectors & Timers Every 300ms) // ========================================================================= let rec_status_sync = record_status_lbl.clone(); let rec_toggle_item_sync = record_toggle_item.clone(); let rec_state_sync2 = Arc::clone(&record_active_state); let rec_icon_sync = record_icon_area.clone(); let r_status_sync2 = replay_status_lbl.clone(); let r_toggle_item_sync2 = replay_toggle_item.clone(); let r_state_sync2 = Arc::clone(&replay_active_state); let r_icon_sync2 = replay_icon_area.clone(); let sys_level_sync = sys_level_bar.clone(); let mic_level_sync = mic_level_bar.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(100), move || { if !is_running_clone.load(Ordering::Relaxed) { return gtk::glib::ControlFlow::Break; } if let Ok(st) = ipc::query_status() { // 1. Recording status sync let prev_rec = rec_state_sync2.swap(st.is_recording, Ordering::Relaxed); if st.is_recording { let mins = st.recording_duration_sec / 60; let secs = st.recording_duration_sec % 60; rec_status_sync.set_label(&format!("Recording {:02}:{:02}", mins, secs)); rec_status_sync.style_context().add_class("status-red"); rec_toggle_item_sync.set_label("Stop (Ctrl+Shift+F9)"); } else { rec_status_sync.set_label("Not recording"); rec_status_sync.style_context().remove_class("status-red"); rec_toggle_item_sync.set_label("Start (Ctrl+Shift+F9)"); } if prev_rec != st.is_recording { rec_icon_sync.queue_draw(); } // 2. Replay status sync let prev_replay = r_state_sync2.swap(st.is_replay_active, Ordering::Relaxed); if st.is_replay_active { r_status_sync2.set_label("On"); r_status_sync2.style_context().add_class("status-green"); r_toggle_item_sync2.set_label("Turn off"); } else { r_status_sync2.set_label("Off"); r_status_sync2.style_context().remove_class("status-green"); r_toggle_item_sync2.set_label("Turn on"); } if prev_replay != st.is_replay_active { r_icon_sync2.queue_draw(); } // 3. Audio VU meter levels sync sys_level_sync.set_value(st.system_level_peak as f64); mic_level_sync.set_value(st.mic_level_peak as f64); } gtk::glib::ControlFlow::Continue }); window.connect_destroy(move |_| { is_running.store(false, Ordering::Relaxed); }); window.show_all(); }); app.run_with_args(&[] as &[&str]); } }