diff --git a/packaging/windows/vrec-installer.nsi b/packaging/windows/vrec-installer.nsi index 2543704..ab11a6f 100644 --- a/packaging/windows/vrec-installer.nsi +++ b/packaging/windows/vrec-installer.nsi @@ -14,7 +14,7 @@ RequestExecutionLevel user !define MUI_FINISHPAGE_RUN "$INSTDIR\vrec-ui.exe" !define MUI_FINISHPAGE_RUN_PARAMETERS "--menu" -!define MUI_FINISHPAGE_RUN_TEXT "Launch vrec Studio Overlay" +!define MUI_FINISHPAGE_RUN_TEXT "Launch vrec" !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_CONFIRM @@ -28,8 +28,7 @@ Section "vrec" SecMain ; Create Start Menu and Desktop shortcuts CreateDirectory "$SMPROGRAMS\vrec" - CreateShortcut "$SMPROGRAMS\vrec\vrec Studio.lnk" "$INSTDIR\vrec-ui.exe" "--menu" - CreateShortcut "$SMPROGRAMS\vrec\vrec Background Engine.lnk" "$INSTDIR\vrec-daemon.exe" "" + CreateShortcut "$SMPROGRAMS\vrec\vrec.lnk" "$INSTDIR\vrec-ui.exe" "--menu" CreateShortcut "$DESKTOP\vrec.lnk" "$INSTDIR\vrec-ui.exe" "--menu" ; Register uninstaller with Windows Control Panel / Settings diff --git a/src/bin/vrec-daemon.rs b/src/bin/vrec-daemon.rs index 42cf3a2..b44bde9 100644 --- a/src/bin/vrec-daemon.rs +++ b/src/bin/vrec-daemon.rs @@ -1,3 +1,5 @@ +#![cfg_attr(target_os = "windows", windows_subsystem = "windows")] + use vrec::capture::{Frame, FrameSource}; use vrec::encoder::VideoEncoder; use vrec::ring::Packet; @@ -99,9 +101,20 @@ async fn main() -> Result<(), Box> { #[cfg(windows)] let listener = { - let l = std::net::TcpListener::bind("127.0.0.1:42069")?; - println!("Daemon listening on TCP IPC: 127.0.0.1:42069"); - l + match std::net::TcpListener::bind("127.0.0.1:42069") { + Ok(l) => { + println!("Daemon listening on TCP IPC: 127.0.0.1:42069"); + l + } + Err(e) => { + if vrec::ipc::query_status().is_ok() { + println!("Another instance of vrec-daemon is already running. Exiting cleanly."); + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(500)); + std::net::TcpListener::bind("127.0.0.1:42069")? + } + } }; let cmd_tx_ipc = cmd_tx.clone(); @@ -114,75 +127,90 @@ async fn main() -> Result<(), Box> { for stream in listener.incoming() { match stream { Ok(mut stream) => { - let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(1000))); - let _ = stream.set_write_timeout(Some(std::time::Duration::from_millis(1000))); - let mut len_buf = [0u8; 4]; - if stream.read_exact(&mut len_buf).is_ok() { - let len = u32::from_le_bytes(len_buf) as usize; - let mut payload = vec![0u8; len]; - if stream.read_exact(&mut payload).is_ok() - && let Ok(cmd) = serde_json::from_slice::(&payload) { - match cmd { - Command::GetStatus => { - let rec = is_recording_state_ipc.load(Ordering::SeqCst); - let start_ts = record_start_state_ipc.load(Ordering::SeqCst); - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); - let duration = if rec && start_ts > 0 { - now.saturating_sub(start_ts) - } else { - 0 - }; - let cfg = vrec::config::VrecConfig::load(); - let status = DaemonStatus { - is_recording: rec, - recording_duration_sec: duration, - is_replay_active: replay_enabled_state_ipc.load(Ordering::SeqCst), - audio_muted: audio_muted_state_ipc.load(Ordering::SeqCst), - audio_mode: cfg.audio_mode, - show_cursor: cfg.show_cursor, - mic_volume: cfg.mic_volume, - system_volume: cfg.system_volume, - }; - if let Ok(resp) = serde_json::to_vec(&status) { - let len_resp = (resp.len() as u32).to_le_bytes(); - let _ = stream.write_all(&len_resp); - let _ = stream.write_all(&resp); + let cmd_tx_ipc = cmd_tx_ipc.clone(); + let is_recording_state_ipc = Arc::clone(&is_recording_state_ipc); + let record_start_state_ipc = Arc::clone(&record_start_state_ipc); + let replay_enabled_state_ipc = Arc::clone(&replay_enabled_state_ipc); + let audio_muted_state_ipc = Arc::clone(&audio_muted_state_ipc); + + std::thread::spawn(move || { + let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(1500))); + let _ = stream.set_write_timeout(Some(std::time::Duration::from_millis(1500))); + let mut len_buf = [0u8; 4]; + if stream.read_exact(&mut len_buf).is_ok() { + let len = u32::from_le_bytes(len_buf) as usize; + let mut payload = vec![0u8; len]; + if stream.read_exact(&mut payload).is_ok() + && let Ok(cmd) = serde_json::from_slice::(&payload) { + match cmd { + Command::GetStatus => { + let rec = is_recording_state_ipc.load(Ordering::SeqCst); + let start_ts = record_start_state_ipc.load(Ordering::SeqCst); + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); + let duration = if rec && start_ts > 0 { + now.saturating_sub(start_ts) + } else { + 0 + }; + let cfg = vrec::config::VrecConfig::load(); + let status = DaemonStatus { + is_recording: rec, + recording_duration_sec: duration, + is_replay_active: replay_enabled_state_ipc.load(Ordering::SeqCst), + audio_muted: audio_muted_state_ipc.load(Ordering::SeqCst), + audio_mode: cfg.audio_mode, + show_cursor: cfg.show_cursor, + mic_volume: cfg.mic_volume, + system_volume: cfg.system_volume, + }; + if let Ok(resp) = serde_json::to_vec(&status) { + let len_resp = (resp.len() as u32).to_le_bytes(); + let _ = stream.write_all(&len_resp); + let _ = stream.write_all(&resp); + } + }, + Command::ToggleCursor => { + let mut cfg = vrec::config::VrecConfig::load(); + cfg.show_cursor = !cfg.show_cursor; + let _ = cfg.save(); + #[cfg(target_os = "linux")] + { + let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_default(); + if session_type.to_lowercase() == "wayland" { + println!("Cursor display toggled to: {}. Restarting Wayland session...", cfg.show_cursor); + let _ = cmd_tx_ipc.send(Command::StopRecording); + std::thread::sleep(std::time::Duration::from_millis(150)); + std::process::exit(0); + } + } + println!("Cursor display toggled to: {}", cfg.show_cursor); + }, + Command::CycleAudioMode => { + let mut cfg = vrec::config::VrecConfig::load(); + cfg.audio_mode = match cfg.audio_mode.as_str() { + "system" => "mic", + "mic" => "both", + "both" => "muted", + _ => "system", + }.to_string(); + let _ = cfg.save(); + println!("Audio mode cycled to: {}", cfg.audio_mode); + let _ = cmd_tx_ipc.try_send(Command::ReloadConfig); + }, + Command::StopDaemon => { + println!("StopDaemon requested: Finalizing active recordings..."); + vrec::hyprland_binds::unregister_hyprland_binds(&vrec::config::VrecConfig::load()); + let _ = cmd_tx_ipc.send(Command::StopRecording); + std::thread::sleep(std::time::Duration::from_millis(350)); + std::process::exit(0); + }, + other => { + let _ = cmd_tx_ipc.try_send(other); } - }, - Command::ToggleCursor => { - let mut cfg = vrec::config::VrecConfig::load(); - cfg.show_cursor = !cfg.show_cursor; - let _ = cfg.save(); - println!("Cursor display toggled to: {}. Restarting daemon for new capture session...", cfg.show_cursor); - let _ = cmd_tx_ipc.send(Command::StopRecording); - std::thread::sleep(std::time::Duration::from_millis(150)); - std::process::exit(0); - }, - Command::CycleAudioMode => { - let mut cfg = vrec::config::VrecConfig::load(); - cfg.audio_mode = match cfg.audio_mode.as_str() { - "system" => "mic", - "mic" => "both", - "both" => "muted", - _ => "system", - }.to_string(); - let _ = cfg.save(); - println!("Audio mode cycled to: {}", cfg.audio_mode); - let _ = cmd_tx_ipc.try_send(Command::ReloadConfig); - }, - Command::StopDaemon => { - println!("StopDaemon requested: Finalizing active recordings..."); - vrec::hyprland_binds::unregister_hyprland_binds(&vrec::config::VrecConfig::load()); - let _ = cmd_tx_ipc.send(Command::StopRecording); - std::thread::sleep(std::time::Duration::from_millis(350)); - std::process::exit(0); - }, - other => { - let _ = cmd_tx_ipc.try_send(other); } } - } - } + } + }); } Err(err) => { eprintln!("Socket error: {}", err); @@ -253,7 +281,7 @@ async fn main() -> Result<(), Box> { Frame::Raw { width, height, .. } => (*width, *height), Frame::DmaBuf { width, height, .. } => (*width, *height), #[cfg(target_os = "windows")] - Frame::D3D11Texture { .. } => (1920, 1080), + Frame::D3D11Texture { width, height, .. } => (*width, *height), }; let _ = frame_tx.send(first_frame); diff --git a/src/bin/vrec-ui.rs b/src/bin/vrec-ui.rs index 7f345d2..33d932e 100644 --- a/src/bin/vrec-ui.rs +++ b/src/bin/vrec-ui.rs @@ -1,3 +1,5 @@ +#![cfg_attr(target_os = "windows", windows_subsystem = "windows")] + use vrec::ipc::{Command, send_command, query_status}; use vrec::overlay::show_notification; use std::env; @@ -35,7 +37,8 @@ fn ensure_wayland_env() { } fn get_daemon_cmd() -> std::process::Command { - if let Ok(mut path) = std::env::current_exe() { + #[allow(unused_mut)] + let mut cmd = if let Ok(mut path) = std::env::current_exe() { path.pop(); #[cfg(target_os = "windows")] let candidate = path.join("vrec-daemon.exe"); @@ -43,33 +46,102 @@ fn get_daemon_cmd() -> std::process::Command { let candidate = path.join("vrec-daemon"); if candidate.exists() { - return std::process::Command::new(candidate); + std::process::Command::new(candidate) + } else { + #[cfg(target_os = "windows")] + { + std::process::Command::new("vrec-daemon.exe") + } + #[cfg(not(target_os = "windows"))] + { + std::process::Command::new("vrec-daemon") + } } - } + } else { + #[cfg(target_os = "windows")] + { + std::process::Command::new("vrec-daemon.exe") + } + #[cfg(not(target_os = "windows"))] + { + std::process::Command::new("vrec-daemon") + } + }; + #[cfg(target_os = "windows")] - return std::process::Command::new("vrec-daemon.exe"); - #[cfg(not(target_os = "windows"))] - return std::process::Command::new("vrec-daemon"); + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + } + + cmd +} + +fn get_ui_cmd() -> std::process::Command { + #[allow(unused_mut)] + let mut cmd = if let Ok(path) = std::env::current_exe() { + std::process::Command::new(path) + } else { + #[cfg(target_os = "windows")] + { + std::process::Command::new("vrec-ui.exe") + } + #[cfg(not(target_os = "windows"))] + { + std::process::Command::new("vrec-ui") + } + }; + + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + } + + cmd } fn ensure_daemon_running() { if query_status().is_err() { - println!("vrec-daemon not running. Auto-launching vrec-daemon..."); let _ = get_daemon_cmd() .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn(); - for _ in 0..20 { - std::thread::sleep(std::time::Duration::from_millis(150)); + for _ in 0..15 { + std::thread::sleep(std::time::Duration::from_millis(100)); if query_status().is_ok() { - println!("vrec-daemon successfully connected."); break; } } } } +fn ensure_hotkeys_running() { + #[cfg(target_os = "windows")] + { + unsafe { + use windows::Win32::Foundation::{CloseHandle, GetLastError, WIN32_ERROR}; + use windows::Win32::System::Threading::CreateMutexW; + + if let Ok(handle) = CreateMutexW(None, false, windows::core::w!("Global\\vrec_hotkeys_single_instance")) { + if GetLastError() == WIN32_ERROR(183) { // ERROR_ALREADY_EXISTS + let _ = CloseHandle(handle); + return; + } + let _ = CloseHandle(handle); + } + } + + let _ = get_ui_cmd() + .arg("--hotkeys") + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn(); + } +} + fn send_with_notification(cmd: Command, success_msg: &str) -> Result<(), Box> { ensure_daemon_running(); match send_command(cmd) { @@ -135,6 +207,7 @@ fn main() -> Result<(), Box> { } "--menu" => { ensure_daemon_running(); + ensure_hotkeys_running(); vrec::overlay_egui::run_egui_overlay(); return Ok(()); } @@ -197,10 +270,23 @@ fn main() -> Result<(), Box> { } } else { ensure_daemon_running(); + ensure_hotkeys_running(); vrec::overlay_egui::run_egui_overlay(); return Ok(()); } + #[cfg(target_os = "windows")] + let _mutex = unsafe { + use windows::Win32::Foundation::{GetLastError, WIN32_ERROR}; + use windows::Win32::System::Threading::CreateMutexW; + + let handle = CreateMutexW(None, true, windows::core::w!("Global\\vrec_hotkeys_single_instance")); + if GetLastError() == WIN32_ERROR(183) { + return Ok(()); + } + handle.ok() + }; + println!("Starting vrec UI/Hotkey process..."); let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string()); @@ -223,7 +309,13 @@ fn main() -> Result<(), Box> { println!("X11 session detected."); } - let manager = GlobalHotKeyManager::new()?; + let manager = match GlobalHotKeyManager::new() { + Ok(m) => m, + Err(e) => { + eprintln!("Failed to create global hotkey manager: {}", e); + return Ok(()); + } + }; let hotkey_menu = HotKey::new(Some(Modifiers::ALT), Code::KeyZ); let hotkey_save = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR); let hotkey_record = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9); @@ -241,7 +333,7 @@ fn main() -> Result<(), Box> { if let Ok(event) = receiver.try_recv() { if event.id == hotkey_menu.id() { ensure_daemon_running(); - vrec::overlay_egui::run_egui_overlay(); + let _ = get_ui_cmd().arg("--menu").spawn(); } else if event.id == hotkey_save.id() { let _ = send_with_notification(Command::SaveReplay, "Replay saved"); } else if event.id == hotkey_record.id() { diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 4cbb601..6dfc390 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -35,6 +35,8 @@ pub enum Frame { #[cfg(target_os = "windows")] D3D11Texture { handle: usize, + width: u32, + height: u32, }, } diff --git a/src/capture/windows.rs b/src/capture/windows.rs index d3e95e5..cece705 100644 --- a/src/capture/windows.rs +++ b/src/capture/windows.rs @@ -16,8 +16,8 @@ pub struct WindowsCapture { context: ID3D11DeviceContext, duplication: IDXGIOutputDuplication, gpu_texture: ID3D11Texture2D, - width: u32, - height: u32, + pub width: u32, + pub height: u32, } #[cfg(target_os = "windows")] @@ -30,14 +30,40 @@ impl WindowsCapture { // Initialize COM library on capture thread let _ = CoInitializeEx(None, COINIT_MULTITHREADED); - // Create D3D11 hardware device + // Create DXGI Factory to find the display adapter with active outputs + let factory: IDXGIFactory1 = CreateDXGIFactory1()?; + let mut chosen_adapter: Option = None; + let mut chosen_output: Option = None; + + let mut a_idx = 0; + while let Ok(adapter) = factory.EnumAdapters1(a_idx) { + let mut o_idx = 0; + while let Ok(output) = adapter.EnumOutputs(o_idx) { + if let Ok(output1) = output.cast::() { + chosen_adapter = Some(adapter); + chosen_output = Some(output1); + break; + } + o_idx += 1; + } + if chosen_adapter.is_some() { + break; + } + a_idx += 1; + } + + let (adapter, output1) = match (chosen_adapter, chosen_output) { + (Some(a), Some(o)) => (a, o), + _ => return Err("No active display output found for Windows desktop capture".into()), + }; + let mut device: Option = None; let mut context: Option = None; let mut feature_level = D3D_FEATURE_LEVEL_11_0; D3D11CreateDevice( - None, - D3D_DRIVER_TYPE_HARDWARE, + Some(&adapter), + D3D_DRIVER_TYPE_UNKNOWN, HMODULE(std::ptr::null_mut()), D3D11_CREATE_DEVICE_BGRA_SUPPORT, Some(&[D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0]), @@ -50,21 +76,14 @@ impl WindowsCapture { let device = device.ok_or("Failed to create D3D11 device")?; let context = context.ok_or("Failed to create D3D11 context")?; - // Retrieve DXGI Output for the primary display - let dxgi_device: IDXGIDevice = device.cast()?; - let adapter = dxgi_device.GetAdapter()?; - let output = adapter.EnumOutputs(0)?; - let output1: IDXGIOutput1 = output.cast()?; - - let desc = output.GetDesc()?; - let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left) as u32; - let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top) as u32; + let desc = output1.GetDesc()?; + let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs(); + let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs(); // Initialize Desktop Duplication let duplication = output1.DuplicateOutput(&device)?; // Allocate a dedicated VRAM-resident GPU texture for 100% zero-copy capture - // (Frame stays on GPU die, zero PCIe bus readback, 0% CPU consumption) let gpu_desc = D3D11_TEXTURE2D_DESC { Width: width, Height: height, @@ -99,10 +118,17 @@ impl WindowsCapture { unsafe { let dxgi_device: IDXGIDevice = self.device.cast()?; let adapter = dxgi_device.GetAdapter()?; - let output = adapter.EnumOutputs(0)?; - let output1: IDXGIOutput1 = output.cast()?; - self.duplication = output1.DuplicateOutput(&self.device)?; - Ok(()) + let mut o_idx = 0; + while let Ok(output) = adapter.EnumOutputs(o_idx) { + if let Ok(output1) = output.cast::() { + if let Ok(dup) = output1.DuplicateOutput(&self.device) { + self.duplication = dup; + return Ok(()); + } + } + o_idx += 1; + } + Err("Failed to reinitialize desktop duplication".into()) } } } @@ -120,24 +146,26 @@ impl FrameSource for WindowsCapture { if let Some(resource) = desktop_resource { let texture: ID3D11Texture2D = resource.cast()?; - // Copy directly on the GPU from the desktop buffer to our persistent VRAM texture + // Copy directly on the GPU from desktop buffer to persistent VRAM texture self.context.CopyResource(&self.gpu_texture, &texture); // Immediately release the desktop frame back to the DWM compositor let _ = self.duplication.ReleaseFrame(); - // Pass the VRAM texture handle directly to the hardware encoder (Zero CPU copy!) return Ok(Frame::D3D11Texture { handle: self.gpu_texture.as_raw() as usize, + width: self.width, + height: self.height, }); } let _ = self.duplication.ReleaseFrame(); } Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => { - // Display is idle (no new pixels), yield current texture without spinning CPU std::thread::sleep(std::time::Duration::from_millis(8)); return Ok(Frame::D3D11Texture { handle: self.gpu_texture.as_raw() as usize, + width: self.width, + height: self.height, }); } Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => { @@ -154,6 +182,8 @@ impl FrameSource for WindowsCapture { Ok(Frame::D3D11Texture { handle: self.gpu_texture.as_raw() as usize, + width: self.width, + height: self.height, }) } } diff --git a/src/encoder.rs b/src/encoder.rs index 3b19b62..0469649 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -336,7 +336,7 @@ impl VaapiEncoder { av_frame_free(&mut drm_frame); } #[cfg(target_os = "windows")] - Frame::D3D11Texture { handle } => { + Frame::D3D11Texture { handle, .. } => { println!("D3D11: Mock encoding D3D11 texture (Windows zero-copy). Handle: {}", handle); } } diff --git a/src/ipc.rs b/src/ipc.rs index ad9a475..ec241ab 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -87,11 +87,12 @@ pub fn send_command(cmd: Command) -> Result<(), Box { - let _ = stream.set_write_timeout(Some(Duration::from_millis(1000))); - let _ = stream.set_read_timeout(Some(Duration::from_millis(1000))); + let _ = stream.set_write_timeout(Some(Duration::from_millis(600))); + let _ = stream.set_read_timeout(Some(Duration::from_millis(600))); if stream.write_all(&len_buf).is_ok() && stream.write_all(&payload).is_ok() { return Ok(()); } @@ -128,14 +129,15 @@ pub fn query_status() -> Result { - let _ = stream.set_write_timeout(Some(Duration::from_millis(1000))); - let _ = stream.set_read_timeout(Some(Duration::from_millis(1000))); + let _ = stream.set_write_timeout(Some(Duration::from_millis(600))); + let _ = stream.set_read_timeout(Some(Duration::from_millis(600))); if stream.write_all(&len_buf).is_err() || stream.write_all(&payload).is_err() { continue; diff --git a/src/overlay.rs b/src/overlay.rs index a8ede99..cfd290e 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -16,79 +16,96 @@ pub fn show_notification_overlay() { } pub fn show_notification(message: &str) { - #[cfg(unix)] - if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() { - return; - } - if gtk::init().is_err() { + #[cfg(target_os = "windows")] + { + let msg = message.to_string(); + std::thread::spawn(move || { + use std::os::windows::process::CommandExt; + let script = format!( + "[reflection.assembly]::loadwithpartialname('System.Windows.Forms') | Out-Null; \ + $notify = new-object system.windows.forms.notifyicon; \ + $notify.icon = [System.Drawing.SystemIcons]::Information; \ + $notify.visible = $true; \ + $notify.showballoontip(2000, 'vrec', '{}', [system.windows.forms.tooltipicon]::Info); \ + Start-Sleep -Seconds 2; \ + $notify.dispose()", + msg.replace('\'', "''") + ); + let _ = std::process::Command::new("powershell") + .args(["-NoProfile", "-WindowStyle", "Hidden", "-Command", &script]) + .creation_flags(0x08000000) + .output(); + }); return; } - let app = Application::builder() - .application_id("com.vrec.notification") - .build(); + #[cfg(not(target_os = "windows"))] + { + if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() { + return; + } + if gtk::init().is_err() { + return; + } - let msg_text = message.to_string(); - app.connect_activate(move |app| { - let window = ApplicationWindow::builder() - .application(app) - .default_width(320) - .default_height(60) + let app = Application::builder() + .application_id("com.vrec.notification") .build(); - #[cfg(target_os = "linux")] - { - window.init_layer_shell(); - window.set_layer(Layer::Overlay); - window.set_namespace("vrec-notification"); - window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30); - window.set_anchor(gtk_layer_shell::Edge::Top, true); - } + let msg_text = message.to_string(); + app.connect_activate(move |app| { + let window = ApplicationWindow::builder() + .application(app) + .default_width(320) + .default_height(60) + .build(); - #[cfg(not(target_os = "linux"))] - { - window.set_decorated(false); - window.set_keep_above(true); - window.set_skip_taskbar_hint(true); - window.set_position(gtk::WindowPosition::Center); - } - - let css_provider = CssProvider::new(); - let css = r#" - window { - background-color: #12141a; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.15); - box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8); + #[cfg(target_os = "linux")] + { + window.init_layer_shell(); + window.set_layer(Layer::Overlay); + window.set_namespace("vrec-notification"); + window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30); + window.set_anchor(gtk_layer_shell::Edge::Top, true); } - label { - color: #ffffff; - font-weight: 700; - font-size: 14px; - padding: 10px 24px; + + let css_provider = CssProvider::new(); + let css = r#" + window { + background-color: #12141a; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8); + } + label { + color: #ffffff; + font-weight: 700; + font-size: 14px; + padding: 10px 24px; + } + "#; + let _ = css_provider.load_from_data(css.as_bytes()); + if let Some(screen) = gdk::Screen::default() { + StyleContext::add_provider_for_screen( + &screen, + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); } - "#; - let _ = css_provider.load_from_data(css.as_bytes()); - if let Some(screen) = gdk::Screen::default() { - StyleContext::add_provider_for_screen( - &screen, - &css_provider, - gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); - } - let label = Label::new(Some(&msg_text)); - window.add(&label); - window.show_all(); + let label = Label::new(Some(&msg_text)); + window.add(&label); + window.show_all(); - let window_clone = window.clone(); - gtk::glib::timeout_add_local(Duration::from_millis(2200), move || { - window_clone.close(); - gtk::glib::ControlFlow::Break + let window_clone = window.clone(); + gtk::glib::timeout_add_local(Duration::from_millis(2200), move || { + window_clone.close(); + gtk::glib::ControlFlow::Break + }); }); - }); - app.run_with_args(&[] as &[&str]); + app.run_with_args(&[] as &[&str]); + } } pub fn show_menu_overlay() { diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index 35e9165..44480e4 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -10,8 +10,6 @@ use crate::ipc::{self, Command, DaemonStatus}; pub enum ShadowPlayView { MainHud, Settings, - Gallery, - AudioMixer, } #[derive(Debug, Clone)] @@ -52,62 +50,16 @@ fn scan_recordings(dir_str: &str) -> Vec { clips } -fn format_file_size(bytes: u64) -> String { - if bytes >= 1024 * 1024 * 1024 { - format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) - } else if bytes >= 1024 * 1024 { - format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) - } else if bytes >= 1024 { - format!("{:.0} KB", bytes as f64 / 1024.0) - } else { - format!("{} B", bytes) - } -} - -fn format_system_time(time: SystemTime) -> String { - if let Ok(dur) = SystemTime::now().duration_since(time) { - let secs = dur.as_secs(); - if secs < 60 { - "Just now".to_string() - } else if secs < 3600 { - format!("{} min ago", secs / 60) - } else if secs < 86400 { - format!("{} hr ago", secs / 3600) - } else { - format!("{} days ago", secs / 86400) - } - } else { - "Recent".to_string() - } -} - // Cross-platform helper to reveal or open directories fn open_folder(path: &std::path::Path) { let p = path.to_path_buf(); std::thread::spawn(move || { #[cfg(target_os = "windows")] { - let _ = std::process::Command::new("explorer").arg(&p).spawn(); - } - #[cfg(target_os = "macos")] - { - let _ = std::process::Command::new("open").arg(&p).spawn(); - } - #[cfg(not(any(target_os = "windows", target_os = "macos")))] - { - let _ = std::process::Command::new("xdg-open").arg(&p).spawn(); - } - }); -} - -// Cross-platform helper to launch media files with default player -fn open_file(path: &std::path::Path) { - let p = path.to_path_buf(); - std::thread::spawn(move || { - #[cfg(target_os = "windows")] - { - let _ = std::process::Command::new("cmd") - .args(["/c", "start", "", &p.to_string_lossy()]) + use std::os::windows::process::CommandExt; + let _ = std::process::Command::new("explorer") + .arg(&p) + .creation_flags(0x08000000) .spawn(); } #[cfg(target_os = "macos")] @@ -127,15 +79,17 @@ fn pick_folder(current_dir: &str, tx: Sender) { std::thread::spawn(move || { #[cfg(target_os = "windows")] { + use std::os::windows::process::CommandExt; let script = format!( - "[System.Reflection.Assembly]::LoadWithPartialName('System.windows.forms') | Out-Null; \ + "[System.Reflection.Assembly]::LoadWithPartialName(\x27System.windows.forms\x27) | Out-Null; \ $f = New-Object System.Windows.Forms.FolderBrowserDialog; \ - $f.SelectedPath = '{}'; \ + $f.SelectedPath = \x27{}\x27; \ if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{ Write-Host $f.SelectedPath }}", - cur.replace('\'', "''") + cur.replace("\x27", "\x27\x27") ); if let Ok(out) = std::process::Command::new("powershell") - .args(["-NoProfile", "-Command", &script]) + .args(["-NoProfile", "-WindowStyle", "Hidden", "-Command", &script]) + .creation_flags(0x08000000) .output() { let sel = String::from_utf8_lossy(&out.stdout).trim().to_string(); @@ -188,11 +142,23 @@ fn render_keycap(ui: &mut egui::Ui, text: &str) { }); } -// Helper pill button -fn pill(ui: &mut egui::Ui, text: &str, active: bool) -> bool { - let fill = if active { Color32::from_rgb(118, 185, 0) } else { Color32::from_rgb(24, 28, 38) }; - let stroke = if active { Stroke::new(1.0_f32, Color32::from_rgb(150, 220, 20)) } else { Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 72)) }; - let text_color = if active { Color32::from_rgb(10, 16, 8) } else { Color32::from_rgb(203, 213, 225) }; +// Pill button selector +fn pill_button(ui: &mut egui::Ui, text: &str, active: bool) -> bool { + let fill = if active { + Color32::from_rgb(118, 185, 0) + } else { + Color32::from_rgba_unmultiplied(28, 36, 48, 180) + }; + let stroke = if active { + Stroke::new(1.0_f32, Color32::from_rgb(150, 220, 20)) + } else { + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 20)) + }; + let text_color = if active { + Color32::from_rgb(10, 16, 8) + } else { + Color32::from_rgb(203, 213, 225) + }; let btn = egui::Button::new(egui::RichText::new(text).size(11.0).strong().color(text_color)) .fill(fill) .stroke(stroke) @@ -200,40 +166,39 @@ fn pill(ui: &mut egui::Ui, text: &str, active: bool) -> bool { ui.add(btn).clicked() } -// Studio level VU Meter -fn render_vu_meter(ui: &mut egui::Ui, level_pct: f32, active: bool, anim_phase: f32) { - let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 12.0), egui::Sense::hover()); - let painter = ui.painter(); +// Card bottom action button +fn card_action_button(ui: &mut egui::Ui, text: &str, highlight: bool, danger: bool) -> bool { + let fill = if danger { + Color32::from_rgb(220, 38, 38) + } else if highlight { + Color32::from_rgb(118, 185, 0) + } else { + Color32::from_rgba_unmultiplied(32, 42, 58, 220) + }; + let stroke = if danger { + Stroke::new(1.0_f32, Color32::from_rgb(239, 68, 68)) + } else if highlight { + Stroke::new(1.0_f32, Color32::from_rgb(150, 220, 20)) + } else { + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 28)) + }; + let text_color = if danger || !highlight { + Color32::WHITE + } else { + Color32::from_rgb(10, 16, 8) + }; + let btn = egui::Button::new( + egui::RichText::new(text) + .size(12.0) + .strong() + .color(text_color), + ) + .fill(fill) + .stroke(stroke) + .corner_radius(CornerRadius::same(7_u8)) + .min_size(Vec2::new(ui.available_width(), 32.0)); - painter.rect_filled(rect, CornerRadius::same(3_u8), Color32::from_rgb(12, 16, 22)); - painter.rect_stroke(rect, CornerRadius::same(3_u8), Stroke::new(1.0_f32, Color32::from_rgb(28, 36, 48)), egui::StrokeKind::Inside); - - if !active { - return; - } - - let wave = ((anim_phase * 6.0).sin() * 0.15 + (anim_phase * 14.0).cos() * 0.10).max(-0.2); - let fill_ratio = ((level_pct / 100.0) * (0.65 + wave)).clamp(0.02, 1.0); - let filled_width = rect.width() * fill_ratio; - - let num_segments = 24; - let seg_w = (rect.width() - (num_segments as f32 - 1.0) * 2.0) / num_segments as f32; - for i in 0..num_segments { - let seg_x = rect.min.x + i as f32 * (seg_w + 2.0); - if seg_x > rect.min.x + filled_width { - break; - } - let seg_rect = egui::Rect::from_min_size(egui::pos2(seg_x, rect.min.y + 2.0), Vec2::new(seg_w, rect.height() - 4.0)); - let pos_frac = i as f32 / num_segments as f32; - let seg_color = if pos_frac < 0.65 { - Color32::from_rgb(118, 185, 0) // NVIDIA Green - } else if pos_frac < 0.85 { - Color32::from_rgb(234, 179, 8) // Amber - } else { - Color32::from_rgb(239, 68, 68) // Red - }; - painter.rect_filled(seg_rect, CornerRadius::same(2_u8), seg_color); - } + ui.add(btn).clicked() } // Geometric Icon Renderers @@ -257,8 +222,8 @@ fn draw_replay_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, co let arrow_tip = points[0]; let tangent = Vec2::new(-start_angle.sin(), start_angle.cos()); let normal = Vec2::new(start_angle.cos(), start_angle.sin()); - let p1 = arrow_tip - tangent * 7.0 + normal * 5.0; - let p2 = arrow_tip - tangent * 7.0 - normal * 5.0; + let p1 = arrow_tip - tangent * 6.5 + normal * 4.5; + let p2 = arrow_tip - tangent * 6.5 - normal * 4.5; painter.add(egui::Shape::convex_polygon( vec![arrow_tip, p1, p2], color, @@ -267,7 +232,7 @@ fn draw_replay_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, co // Centered play triangle let tri_r = radius * 0.35; - let tri_center = center + Vec2::new(1.5, 0.0); + let tri_center = center + Vec2::new(1.0, 0.0); let t_tip = tri_center + Vec2::new(tri_r, 0.0); let t_top = tri_center + Vec2::new(-tri_r * 0.6, -tri_r * 0.86); let t_bot = tri_center + Vec2::new(-tri_r * 0.6, tri_r * 0.86); @@ -281,7 +246,7 @@ fn draw_replay_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, co fn draw_record_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, is_recording: bool, anim_time: f32) { if is_recording { let pulse = ((anim_time * 5.0).sin() * 0.5 + 0.5) * 4.0; - let glow_color = Color32::from_rgba_unmultiplied(239, 68, 68, 55); + let glow_color = Color32::from_rgba_unmultiplied(239, 68, 68, 60); painter.circle_filled(center, radius + 4.0 + pulse, glow_color); painter.circle_stroke(center, radius, Stroke::new(3.0_f32, Color32::from_rgb(239, 68, 68))); painter.circle_filled(center, radius * 0.45, Color32::from_rgb(239, 68, 68)); @@ -291,98 +256,6 @@ fn draw_record_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, is } } -fn draw_gallery_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, color: Color32) { - let w = radius * 1.45; - let h = radius * 1.05; - let rect = egui::Rect::from_center_size(center, Vec2::new(w, h)); - painter.rect_stroke(rect, CornerRadius::same(4_u8), Stroke::new(2.5_f32, color), egui::StrokeKind::Inside); - - let tri_r = radius * 0.35; - let tri_center = center + Vec2::new(1.0, 0.0); - let t_tip = tri_center + Vec2::new(tri_r, 0.0); - let t_top = tri_center + Vec2::new(-tri_r * 0.6, -tri_r * 0.86); - let t_bot = tri_center + Vec2::new(-tri_r * 0.6, tri_r * 0.86); - painter.add(egui::Shape::convex_polygon( - vec![t_tip, t_top, t_bot], - color, - Stroke::NONE, - )); - - let notch_color = Color32::from_rgb(100, 116, 139); - for dy in [-h * 0.28, h * 0.28] { - painter.rect_filled( - egui::Rect::from_center_size(egui::pos2(rect.min.x + 4.0, center.y + dy), Vec2::new(3.0, 3.0)), - CornerRadius::same(1_u8), - notch_color, - ); - painter.rect_filled( - egui::Rect::from_center_size(egui::pos2(rect.max.x - 4.0, center.y + dy), Vec2::new(3.0, 3.0)), - CornerRadius::same(1_u8), - notch_color, - ); - } -} - -fn draw_mic_icon(painter: &egui::Painter, center: egui::Pos2, size: f32, color: Color32, muted: bool) { - let body_w = size * 0.38; - let body_h = size * 0.60; - let body_rect = egui::Rect::from_center_size(center - Vec2::new(0.0, size * 0.12), Vec2::new(body_w, body_h)); - painter.rect_filled(body_rect, CornerRadius::same((body_w / 2.0) as u8), color); - - let cradle_r = size * 0.36; - let cradle_center = center - Vec2::new(0.0, size * 0.06); - let stroke = Stroke::new(2.0_f32, color); - use std::f32::consts::PI; - let steps = 12; - let mut arc = Vec::with_capacity(steps + 1); - for i in 0..=steps { - let t = i as f32 / steps as f32; - let angle = 0.0 + t * PI; - arc.push(cradle_center + Vec2::new(angle.cos() * cradle_r, angle.sin() * cradle_r)); - } - for window in arc.windows(2) { - painter.line_segment([window[0], window[1]], stroke); - } - - let stem_top = cradle_center + Vec2::new(0.0, cradle_r); - let stem_bot = stem_top + Vec2::new(0.0, size * 0.20); - painter.line_segment([stem_top, stem_bot], stroke); - painter.line_segment( - [stem_bot - Vec2::new(size * 0.22, 0.0), stem_bot + Vec2::new(size * 0.22, 0.0)], - stroke, - ); - - if muted { - let slash_stroke = Stroke::new(2.5_f32, Color32::from_rgb(239, 68, 68)); - let p1 = center - Vec2::new(size * 0.5, size * 0.5); - let p2 = center + Vec2::new(size * 0.5, size * 0.5); - painter.line_segment([p1, p2], slash_stroke); - } -} - -fn draw_cursor_icon(painter: &egui::Painter, center: egui::Pos2, size: f32, color: Color32, visible: bool) { - let p0 = center + Vec2::new(-size * 0.35, -size * 0.45); - let p1 = p0 + Vec2::new(0.0, size * 0.90); - let p2 = p0 + Vec2::new(size * 0.28, size * 0.65); - let p3 = p0 + Vec2::new(size * 0.58, size * 0.90); - let p4 = p0 + Vec2::new(size * 0.72, size * 0.78); - let p5 = p0 + Vec2::new(size * 0.42, size * 0.52); - let p6 = p0 + Vec2::new(size * 0.72, size * 0.52); - - let poly = vec![p0, p1, p2, p3, p4, p5, p6]; - let fill = if visible { color } else { Color32::from_rgb(71, 85, 105) }; - let border = if visible { Color32::BLACK } else { Color32::from_rgb(30, 41, 59) }; - painter.add(egui::Shape::convex_polygon(poly, fill, Stroke::new(1.5_f32, border))); - - if !visible { - let slash_stroke = Stroke::new(2.0_f32, Color32::from_rgb(239, 68, 68)); - painter.line_segment( - [center - Vec2::new(size * 0.45, size * 0.45), center + Vec2::new(size * 0.45, size * 0.45)], - slash_stroke, - ); - } -} - fn draw_gear_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, color: Color32) { let stroke = Stroke::new(2.0_f32, color); painter.circle_stroke(center, radius * 0.75, stroke); @@ -405,29 +278,6 @@ fn draw_close_icon(painter: &egui::Painter, center: egui::Pos2, size: f32, color painter.line_segment([center - Vec2::new(half, -half), center + Vec2::new(half, -half)], stroke); } -fn icon_button(ui: &mut egui::Ui, size: Vec2, hover_desc: &str) -> (egui::Response, egui::Painter, egui::Rect) { - let (rect, mut response) = ui.allocate_exact_size(size, egui::Sense::click()); - let painter = ui.painter(); - let bg_color = if response.is_pointer_button_down_on() { - Color32::from_rgb(34, 44, 60) - } else if response.hovered() { - Color32::from_rgb(26, 34, 46) - } else { - Color32::from_rgb(16, 20, 28) - }; - let border_color = if response.hovered() { - Color32::from_rgb(80, 95, 120) - } else { - Color32::from_rgb(36, 46, 62) - }; - painter.rect_filled(rect, CornerRadius::same(6_u8), bg_color); - painter.rect_stroke(rect, CornerRadius::same(6_u8), Stroke::new(1.0_f32, border_color), egui::StrokeKind::Inside); - if !hover_desc.is_empty() { - response = response.on_hover_text(hover_desc); - } - (response, painter.clone(), rect) -} - pub struct VrecOverlayApp { config: VrecConfig, status: DaemonStatus, @@ -437,6 +287,7 @@ pub struct VrecOverlayApp { replay_sec: u32, bitrate_mbps: u32, target_fps: u32, + video_codec: String, audio_mode_idx: usize, show_cursor: bool, mic_volume_pct: u32, @@ -447,8 +298,7 @@ pub struct VrecOverlayApp { folder_tx: Sender, folder_rx: Receiver, clips: Vec, - gallery_filter_idx: usize, - last_clip_scan: Instant, + initial_pos_set: bool, } impl Default for VrecOverlayApp { @@ -465,6 +315,7 @@ impl VrecOverlayApp { let target_fps = config.fps; let output_dir = config.output_directory.clone(); let show_cursor = config.show_cursor; + let video_codec = config.video_codec.clone(); let mic_volume_pct = (config.mic_volume * 100.0).round().clamp(0.0, 200.0) as u32; let system_volume_pct = (config.system_volume * 100.0).round().clamp(0.0, 200.0) as u32; let audio_mode_idx = match config.audio_mode.as_str() { @@ -496,6 +347,7 @@ impl VrecOverlayApp { replay_sec, bitrate_mbps, target_fps, + video_codec, audio_mode_idx, show_cursor, mic_volume_pct, @@ -506,41 +358,480 @@ impl VrecOverlayApp { folder_tx, folder_rx, clips, - gallery_filter_idx: 0, - last_clip_scan: Instant::now(), + initial_pos_set: false, } } pub fn refresh_clips(&mut self) { self.clips = scan_recordings(&self.output_dir); - self.last_clip_scan = Instant::now(); } fn poll_async_events(&mut self) { while let Ok(s) = self.status_rx.try_recv() { - if self.current_view == ShadowPlayView::MainHud { - self.show_cursor = s.show_cursor; - } self.status = s; self.daemon_connected = true; } - if let Ok(chosen_dir) = self.folder_rx.try_recv() { - let trimmed = chosen_dir.trim(); - if !trimmed.is_empty() { - self.output_dir = trimmed.to_string(); + while let Ok(new_dir) = self.folder_rx.try_recv() { + if !new_dir.is_empty() { + self.output_dir = new_dir.clone(); + self.config.output_directory = new_dir; + let _ = self.config.save(); + let _ = ipc::send_command(Command::ReloadConfig); self.refresh_clips(); - self.set_msg("Save directory updated!"); } } - - if self.current_view == ShadowPlayView::Gallery && self.last_clip_scan.elapsed() > Duration::from_secs(2) { - self.refresh_clips(); - } } - fn set_msg(&mut self, text: &str) { - self.status_msg = Some((text.to_string(), Instant::now())); + pub fn switch_view(&mut self, view: ShadowPlayView, ctx: &egui::Context) { + self.current_view = view; + let target_h = match view { + ShadowPlayView::MainHud => 240.0, + ShadowPlayView::Settings => 560.0, + }; + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(780.0, target_h))); + } + + fn render_main_hud(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { + let anim_time = self.anim_time; + let is_recording = self.status.is_recording; + let rec_dur = self.status.recording_duration_sec; + let is_replay_active = self.status.is_replay_active; + + // Top Header bar + ui.horizontal(|ui| { + // VREC Green badge + let (brand_rect, _) = ui.allocate_exact_size(Vec2::new(44.0, 22.0), egui::Sense::hover()); + ui.painter().rect_filled(brand_rect, CornerRadius::same(4_u8), Color32::from_rgb(118, 185, 0)); + ui.painter().text( + brand_rect.center(), + egui::Align2::CENTER_CENTER, + "VREC", + FontId::proportional(11.5), + Color32::from_rgb(10, 16, 8), + ); + + ui.add_space(8.0); + ui.label( + egui::RichText::new("SHADOWPLAY OVERLAY") + .font(FontId::proportional(12.0)) + .strong() + .color(Color32::from_rgb(226, 232, 240)), + ); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Close button + let (close_rect, close_resp) = ui.allocate_exact_size(Vec2::new(22.0, 22.0), egui::Sense::click()); + let close_color = if close_resp.hovered() { Color32::from_rgb(239, 68, 68) } else { Color32::from_rgb(148, 163, 184) }; + draw_close_icon(ui.painter(), close_rect.center(), 12.0, close_color); + if close_resp.clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + + ui.add_space(10.0); + + // Status message or notification banner + if let Some((msg, ts)) = &self.status_msg { + if ts.elapsed() < Duration::from_secs(3) { + ui.label( + egui::RichText::new(msg) + .size(11.0) + .strong() + .color(Color32::from_rgb(118, 185, 0)), + ); + } + } else if is_recording { + let pulse = ((anim_time * 5.0).sin() * 0.5 + 0.5) * 2.0; + let (dot_rect, _) = ui.allocate_exact_size(Vec2::new(10.0, 10.0), egui::Sense::hover()); + ui.painter().circle_filled(dot_rect.center(), 4.0 + pulse, Color32::from_rgba_unmultiplied(239, 68, 68, 80)); + ui.painter().circle_filled(dot_rect.center(), 3.5, Color32::from_rgb(239, 68, 68)); + let mins = rec_dur / 60; + let secs = rec_dur % 60; + ui.label( + egui::RichText::new(format!("REC {:02}:{:02}", mins, secs)) + .size(11.5) + .strong() + .color(Color32::from_rgb(239, 68, 68)), + ); + } else { + let status_text = if self.daemon_connected { "READY" } else { "CONNECTING..." }; + let status_color = if self.daemon_connected { Color32::from_rgb(74, 222, 128) } else { Color32::from_rgb(100, 116, 139) }; + ui.label( + egui::RichText::new(status_text) + .size(10.0) + .strong() + .color(status_color), + ); + } + }); + }); + + ui.add_space(10.0); + + // 3 Transparent Cards + let total_width = ui.available_width(); + let card_gap = 12.0; + let card_w = ((total_width - 2.0 * card_gap) / 3.0).floor(); + let card_h = 165.0; + + ui.horizontal(|ui| { + // CARD 1: INSTANT REPLAY + let replay_border = if is_replay_active { + Color32::from_rgba_unmultiplied(118, 185, 0, 120) + } else { + Color32::from_rgba_unmultiplied(255, 255, 255, 22) + }; + egui::Frame::NONE + .fill(Color32::from_rgba_unmultiplied(18, 24, 34, 185)) + .stroke(Stroke::new(1.0_f32, replay_border)) + .corner_radius(CornerRadius::same(12_u8)) + .inner_margin(Margin::symmetric(14_i8, 12_i8)) + .show(ui, |ui| { + ui.set_width(card_w - 28.0); + ui.set_height(card_h - 24.0); + + // Top row + ui.horizontal(|ui| { + ui.label(egui::RichText::new("INSTANT REPLAY").size(11.5).strong().color(Color32::WHITE)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let (txt, col) = if is_replay_active { ("ACTIVE", Color32::from_rgb(118, 185, 0)) } else { ("OFF", Color32::from_rgb(148, 163, 184)) }; + ui.label(egui::RichText::new(txt).size(10.0).strong().color(col)); + }); + }); + + ui.add_space(6.0); + + // Icon & subtitle + let (icon_rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 44.0), egui::Sense::hover()); + let replay_color = if is_replay_active { Color32::from_rgb(118, 185, 0) } else { Color32::from_rgb(148, 163, 184) }; + draw_replay_icon(ui.painter(), icon_rect.center(), 16.0, replay_color); + + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new(format!("{}s Rolling Buffer", self.replay_sec)).size(10.5).color(Color32::from_rgb(148, 163, 184))); + }); + + ui.add_space(8.0); + + // Action button + if card_action_button(ui, "Save Replay", true, false) { + let _ = ipc::send_command(Command::SaveReplay); + self.status_msg = Some(("Replay Saved!".to_string(), Instant::now())); + } + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new("Ctrl + Shift + R").size(9.0).color(Color32::from_rgb(100, 116, 139))); + }); + }); + + ui.add_space(card_gap); + + // CARD 2: RECORD + let record_border = if is_recording { + Color32::from_rgba_unmultiplied(239, 68, 68, 180) + } else { + Color32::from_rgba_unmultiplied(255, 255, 255, 22) + }; + egui::Frame::NONE + .fill(Color32::from_rgba_unmultiplied(18, 24, 34, 185)) + .stroke(Stroke::new(1.0_f32, record_border)) + .corner_radius(CornerRadius::same(12_u8)) + .inner_margin(Margin::symmetric(14_i8, 12_i8)) + .show(ui, |ui| { + ui.set_width(card_w - 28.0); + ui.set_height(card_h - 24.0); + + // Top row + ui.horizontal(|ui| { + ui.label(egui::RichText::new("RECORD").size(11.5).strong().color(Color32::WHITE)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let (txt, col) = if is_recording { ("RECORDING", Color32::from_rgb(239, 68, 68)) } else { ("READY", Color32::from_rgb(148, 163, 184)) }; + ui.label(egui::RichText::new(txt).size(10.0).strong().color(col)); + }); + }); + + ui.add_space(6.0); + + // Icon & subtitle + let (icon_rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 44.0), egui::Sense::hover()); + draw_record_icon(ui.painter(), icon_rect.center(), 16.0, is_recording, anim_time); + + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new(format!("{} FPS • {} Mbps", self.target_fps, self.bitrate_mbps)).size(10.5).color(Color32::from_rgb(148, 163, 184))); + }); + + ui.add_space(8.0); + + // Action button + if is_recording { + if card_action_button(ui, "Stop Recording", false, true) { + let _ = ipc::send_command(Command::StopRecording); + self.status_msg = Some(("Recording Saved!".to_string(), Instant::now())); + } + } else { + if card_action_button(ui, "Start Recording", false, false) { + let _ = ipc::send_command(Command::StartRecording); + self.status_msg = Some(("Recording Started".to_string(), Instant::now())); + } + } + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new("Ctrl + Shift + F9").size(9.0).color(Color32::from_rgb(100, 116, 139))); + }); + }); + + ui.add_space(card_gap); + + // CARD 3: SETTINGS + egui::Frame::NONE + .fill(Color32::from_rgba_unmultiplied(18, 24, 34, 185)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 22))) + .corner_radius(CornerRadius::same(12_u8)) + .inner_margin(Margin::symmetric(14_i8, 12_i8)) + .show(ui, |ui| { + ui.set_width(card_w - 28.0); + ui.set_height(card_h - 24.0); + + // Top row + ui.horizontal(|ui| { + ui.label(egui::RichText::new("SETTINGS").size(11.5).strong().color(Color32::WHITE)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(egui::RichText::new("CONFIG").size(10.0).strong().color(Color32::from_rgb(96, 165, 250))); + }); + }); + + ui.add_space(6.0); + + // Icon & subtitle + let (icon_rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 44.0), egui::Sense::hover()); + draw_gear_icon(ui.painter(), icon_rect.center(), 16.0, Color32::from_rgb(203, 213, 225)); + + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new("Cursor, Audio, Video & Storage").size(10.5).color(Color32::from_rgb(148, 163, 184))); + }); + + ui.add_space(8.0); + + // Action button + if card_action_button(ui, "Open Settings", false, false) { + self.switch_view(ShadowPlayView::Settings, ctx); + } + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new("All Preferences").size(9.0).color(Color32::from_rgb(100, 116, 139))); + }); + }); + }); + } + + fn render_settings_view(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { + // Header with Back button + ui.horizontal(|ui| { + if ui.button(egui::RichText::new("< Back to Overlay").size(12.0).strong().color(Color32::from_rgb(118, 185, 0))).clicked() { + self.switch_view(ShadowPlayView::MainHud, ctx); + return; + } + + ui.add_space(16.0); + ui.label(egui::RichText::new("VREC SETTINGS").font(FontId::proportional(13.0)).strong().color(Color32::WHITE)); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let (close_rect, close_resp) = ui.allocate_exact_size(Vec2::new(22.0, 22.0), egui::Sense::click()); + let close_color = if close_resp.hovered() { Color32::from_rgb(239, 68, 68) } else { Color32::from_rgb(148, 163, 184) }; + draw_close_icon(ui.painter(), close_rect.center(), 12.0, close_color); + if close_resp.clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + }); + }); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(6.0); + + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + // CATEGORY 1: DISPLAY & MOUSE CURSOR + ui.label(egui::RichText::new("DISPLAY & CAPTURE").size(11.5).strong().color(Color32::from_rgb(118, 185, 0))); + ui.add_space(4.0); + + // MOUSE CURSOR TOGGLE + let mut show_cur = self.show_cursor; + if ui.checkbox(&mut show_cur, egui::RichText::new("Record Mouse Cursor").size(12.0).strong().color(Color32::WHITE)).changed() { + self.show_cursor = show_cur; + self.config.show_cursor = show_cur; + let _ = self.config.save(); + let _ = ipc::send_command(Command::ToggleCursor); + self.status_msg = Some((format!("Cursor {}", if show_cur { "Visible" } else { "Hidden" }), Instant::now())); + } + ui.label(egui::RichText::new("Include the mouse pointer in recordings and instant replays.").size(10.5).color(Color32::from_rgb(148, 163, 184))); + + ui.add_space(10.0); + + // Framerate + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Target Framerate:").size(11.0).strong().color(Color32::WHITE)); + for fps in [30, 60, 120, 144] { + if pill_button(ui, &format!("{} FPS", fps), self.target_fps == fps) { + self.target_fps = fps; + self.config.fps = fps; + let _ = self.config.save(); + let _ = ipc::send_command(Command::ReloadConfig); + } + } + }); + + ui.add_space(8.0); + + // Bitrate Slider + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Video Bitrate:").size(11.0).strong().color(Color32::WHITE)); + let mut br = self.bitrate_mbps; + if ui.add(egui::Slider::new(&mut br, 5..=100).text("Mbps").step_by(5.0)).changed() { + self.bitrate_mbps = br; + self.config.record_bitrate_kbps = br * 1000; + self.config.replay_bitrate_kbps = br * 1000; + let _ = self.config.save(); + let _ = ipc::send_command(Command::ReloadConfig); + } + }); + + ui.add_space(8.0); + + // Video Codec + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Video Codec:").size(11.0).strong().color(Color32::WHITE)); + for (codec_key, label) in [("h264", "H.264 (NVENC/VAAPI)"), ("hevc", "HEVC (H.265)"), ("av1", "AV1")] { + if pill_button(ui, label, self.video_codec == codec_key) { + self.video_codec = codec_key.to_string(); + self.config.video_codec = codec_key.to_string(); + let _ = self.config.save(); + let _ = ipc::send_command(Command::ReloadConfig); + } + } + }); + + ui.add_space(16.0); + ui.separator(); + ui.add_space(6.0); + + // CATEGORY 2: INSTANT REPLAY BUFFER + ui.label(egui::RichText::new("INSTANT REPLAY BUFFER").size(11.5).strong().color(Color32::from_rgb(118, 185, 0))); + ui.add_space(4.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Buffer Length:").size(11.0).strong().color(Color32::WHITE)); + for sec in [15, 30, 60, 120, 300] { + let label = if sec >= 60 { format!("{}m", sec / 60) } else { format!("{}s", sec) }; + if pill_button(ui, &label, self.replay_sec == sec) { + self.replay_sec = sec; + self.config.replay_duration_sec = sec; + let _ = self.config.save(); + let _ = ipc::send_command(Command::ReloadConfig); + } + } + }); + + ui.add_space(16.0); + ui.separator(); + ui.add_space(6.0); + + // CATEGORY 3: AUDIO & SOUND + ui.label(egui::RichText::new("AUDIO & SOUND ROUTING").size(11.5).strong().color(Color32::from_rgb(118, 185, 0))); + ui.add_space(4.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Audio Source:").size(11.0).strong().color(Color32::WHITE)); + let modes = [("system", "System Audio"), ("mic", "Microphone"), ("both", "Both Combined"), ("muted", "Muted")]; + for (idx, (m_id, m_label)) in modes.iter().enumerate() { + if pill_button(ui, m_label, self.audio_mode_idx == idx) { + self.audio_mode_idx = idx; + self.config.audio_mode = m_id.to_string(); + let _ = self.config.save(); + let _ = ipc::send_command(Command::ReloadConfig); + } + } + }); + + ui.add_space(8.0); + + // System Volume + ui.horizontal(|ui| { + ui.label(egui::RichText::new("System Volume:").size(11.0).strong().color(Color32::WHITE)); + let mut sys_v = self.system_volume_pct; + if ui.add(egui::Slider::new(&mut sys_v, 0..=150).suffix("%")).changed() { + self.system_volume_pct = sys_v; + self.config.system_volume = sys_v as f32 / 100.0; + let _ = self.config.save(); + let _ = ipc::send_command(Command::ReloadConfig); + } + }); + + ui.add_space(6.0); + + // Mic Volume + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Microphone Volume:").size(11.0).strong().color(Color32::WHITE)); + let mut mic_v = self.mic_volume_pct; + if ui.add(egui::Slider::new(&mut mic_v, 0..=150).suffix("%")).changed() { + self.mic_volume_pct = mic_v; + self.config.mic_volume = mic_v as f32 / 100.0; + let _ = self.config.save(); + let _ = ipc::send_command(Command::ReloadConfig); + } + }); + + ui.add_space(16.0); + ui.separator(); + ui.add_space(6.0); + + // CATEGORY 4: STORAGE & OUTPUT + ui.label(egui::RichText::new("STORAGE & RECORDINGS").size(11.5).strong().color(Color32::from_rgb(118, 185, 0))); + ui.add_space(4.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Directory:").size(11.0).strong().color(Color32::WHITE)); + let mut dir_str = self.output_dir.clone(); + if ui.add(egui::TextEdit::singleline(&mut dir_str).desired_width(280.0)).changed() { + self.output_dir = dir_str.clone(); + self.config.output_directory = dir_str; + let _ = self.config.save(); + } + + if ui.button(egui::RichText::new("Browse...").size(11.0)).clicked() { + pick_folder(&self.output_dir, self.folder_tx.clone()); + } + + if ui.button(egui::RichText::new("Open Folder").size(11.0)).clicked() { + open_folder(&VrecConfig::expand_tilde(&self.output_dir)); + } + }); + + ui.label(egui::RichText::new(format!("{} video recordings found in destination folder.", self.clips.len())).size(10.5).color(Color32::from_rgb(148, 163, 184))); + + ui.add_space(16.0); + ui.separator(); + ui.add_space(6.0); + + // CATEGORY 5: KEYBOARD SHORTCUTS + ui.label(egui::RichText::new("GLOBAL SHORTCUTS").size(11.5).strong().color(Color32::from_rgb(118, 185, 0))); + ui.add_space(6.0); + + let shortcuts = [ + ("Alt + Z", "Open / Close Overlay Dock"), + ("Ctrl + Shift + R", "Save Instant Replay Clip"), + ("Ctrl + Shift + F9", "Start / Stop Recording"), + ("Ctrl + Shift + F10", "Toggle Mouse Cursor In Recordings"), + ]; + + for (keys, action) in shortcuts { + ui.horizontal(|ui| { + render_keycap(ui, keys); + ui.add_space(8.0); + ui.label(egui::RichText::new(action).size(11.0).color(Color32::from_rgb(203, 213, 225))); + }); + ui.add_space(3.0); + } + + ui.add_space(10.0); + }); } } @@ -550,791 +841,43 @@ impl eframe::App for VrecOverlayApp { self.anim_time += 0.033; ctx.request_repaint_after(Duration::from_millis(50)); + // Position window a little at the top like an overlay! + if !self.initial_pos_set + && let Some(monitor_size) = ctx.input(|i| i.viewport().monitor_size) + && monitor_size.x > 100.0 + && monitor_size.y > 100.0 + { + let win_w = 780.0; + let x = ((monitor_size.x - win_w) / 2.0).max(10.0); + let y = 45.0; // a little at the top like an overlay! + ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(x, y))); + self.initial_pos_set = true; + } + if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { if self.current_view != ShadowPlayView::MainHud { - self.current_view = ShadowPlayView::MainHud; + self.switch_view(ShadowPlayView::MainHud, ctx); } else { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } } let mut visuals = egui::Visuals::dark(); - visuals.panel_fill = Color32::from_rgb(12, 15, 20); - visuals.window_fill = Color32::from_rgb(12, 15, 20); - visuals.window_stroke = Stroke::new(1.5_f32, Color32::from_rgb(36, 44, 58)); - visuals.window_corner_radius = CornerRadius::same(14_u8); + visuals.panel_fill = Color32::TRANSPARENT; + visuals.window_fill = Color32::TRANSPARENT; ctx.set_visuals(visuals); egui::CentralPanel::default() .frame( egui::Frame::NONE - .fill(Color32::from_rgb(12, 15, 20)) - .stroke(Stroke::new(1.5_f32, Color32::from_rgb(36, 44, 58))) + .fill(Color32::from_rgba_unmultiplied(12, 16, 24, 230)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 22))) .corner_radius(CornerRadius::same(14_u8)) - .inner_margin(Margin::same(18_i8)), + .inner_margin(Margin::same(14_i8)), ) - .show(ctx, |ui| { - // TOP HEADER BAR - ui.horizontal(|ui| { - // NVIDIA Green Brand Badge - let (brand_rect, _) = ui.allocate_exact_size(Vec2::new(50.0, 24.0), egui::Sense::hover()); - ui.painter().rect_filled(brand_rect, CornerRadius::same(4_u8), Color32::from_rgb(118, 185, 0)); - ui.painter().text( - brand_rect.center(), - egui::Align2::CENTER_CENTER, - "VREC", - FontId::proportional(12.0), - Color32::from_rgb(10, 16, 8), - ); - - ui.add_space(6.0); - ui.vertical(|ui| { - ui.label( - egui::RichText::new("SHADOWPLAY") - .font(FontId::proportional(13.0)) - .strong() - .color(Color32::WHITE), - ); - }); - - ui.add_space(16.0); - - // Central Status Capsule - if self.status.is_recording { - let dur = self.status.recording_duration_sec; - let text = format!("● RECORDING {:02}:{:02}", dur / 60, dur % 60); - egui::Frame::NONE - .fill(Color32::from_rgba_unmultiplied(239, 68, 68, 35)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(239, 68, 68))) - .corner_radius(CornerRadius::same(12_u8)) - .inner_margin(Margin::symmetric(12_i8, 4_i8)) - .show(ui, |ui| { - ui.label(egui::RichText::new(text).font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(248, 113, 113))); - }); - } else if self.status.is_replay_active { - let text = format!("● INSTANT REPLAY ACTIVE ({}s)", self.replay_sec); - egui::Frame::NONE - .fill(Color32::from_rgba_unmultiplied(118, 185, 0, 30)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(118, 185, 0))) - .corner_radius(CornerRadius::same(12_u8)) - .inner_margin(Margin::symmetric(12_i8, 4_i8)) - .show(ui, |ui| { - ui.label(egui::RichText::new(text).font(FontId::monospace(10.5)).strong().color(Color32::from_rgb(163, 230, 53))); - }); - } else { - egui::Frame::NONE - .fill(Color32::from_rgb(20, 26, 36)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 72))) - .corner_radius(CornerRadius::same(12_u8)) - .inner_margin(Margin::symmetric(12_i8, 4_i8)) - .show(ui, |ui| { - ui.label(egui::RichText::new("STANDBY").font(FontId::monospace(10.5)).color(Color32::from_rgb(148, 163, 184))); - }); - } - - // Toast message inline if active - if let Some((ref msg, ts)) = self.status_msg { - if ts.elapsed() < Duration::from_secs(3) { - ui.add_space(10.0); - ui.label(egui::RichText::new(format!("✓ {}", msg)).font(FontId::proportional(11.0)).color(Color32::from_rgb(118, 185, 0))); - } else { - self.status_msg = None; - } - } - - // Right quick controls - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // Close button - let (btn_close, p_close, r_close) = icon_button(ui, Vec2::new(32.0, 32.0), "Close Overlay (Esc)"); - draw_close_icon(&p_close, r_close.center(), 12.0, Color32::from_rgb(148, 163, 184)); - if btn_close.clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - - // Settings gear button - let (btn_gear, p_gear, r_gear) = icon_button(ui, Vec2::new(32.0, 32.0), "Capture Settings"); - let gear_col = if self.current_view == ShadowPlayView::Settings { Color32::from_rgb(118, 185, 0) } else { Color32::from_rgb(203, 213, 225) }; - draw_gear_icon(&p_gear, r_gear.center(), 8.5, gear_col); - if btn_gear.clicked() { - self.current_view = if self.current_view == ShadowPlayView::Settings { ShadowPlayView::MainHud } else { ShadowPlayView::Settings }; - } - - // Gallery button - let (btn_gal, p_gal, r_gal) = icon_button(ui, Vec2::new(32.0, 32.0), "Recordings Gallery"); - let gal_col = if self.current_view == ShadowPlayView::Gallery { Color32::from_rgb(14, 165, 233) } else { Color32::from_rgb(203, 213, 225) }; - draw_gallery_icon(&p_gal, r_gal.center(), 9.5, gal_col); - if btn_gal.clicked() { - self.refresh_clips(); - self.current_view = if self.current_view == ShadowPlayView::Gallery { ShadowPlayView::MainHud } else { ShadowPlayView::Gallery }; - } - - // Cursor toggle button - let cursor_tip = if self.show_cursor { "Cursor: Visible (Click to Hide)" } else { "Cursor: Hidden (Click to Show)" }; - let (btn_cur, p_cur, r_cur) = icon_button(ui, Vec2::new(32.0, 32.0), cursor_tip); - let cur_col = if self.show_cursor { Color32::from_rgb(118, 185, 0) } else { Color32::from_rgb(100, 116, 139) }; - draw_cursor_icon(&p_cur, r_cur.center(), 17.0, cur_col, self.show_cursor); - if btn_cur.clicked() { - self.show_cursor = !self.show_cursor; - self.config.show_cursor = self.show_cursor; - let _ = self.config.save(); - let _ = ipc::send_command(Command::ToggleCursor); - self.set_msg(if self.show_cursor { "Cursor: Visible in recording" } else { "Cursor: Hidden from recording" }); - } - - // Audio Mixer button - let audio_tip = format!("Audio Mode: {} (Click to configure)", self.status.audio_mode); - let (btn_mic, p_mic, r_mic) = icon_button(ui, Vec2::new(32.0, 32.0), &audio_tip); - let mic_col = if self.current_view == ShadowPlayView::AudioMixer { - Color32::from_rgb(118, 185, 0) - } else if self.status.audio_muted { - Color32::from_rgb(239, 68, 68) - } else { - Color32::from_rgb(203, 213, 225) - }; - draw_mic_icon(&p_mic, r_mic.center(), 17.0, mic_col, self.status.audio_muted); - if btn_mic.clicked() { - self.current_view = if self.current_view == ShadowPlayView::AudioMixer { ShadowPlayView::MainHud } else { ShadowPlayView::AudioMixer }; - } - }); - }); - - ui.add_space(14.0); - - // VIEW ROUTING - match self.current_view { - ShadowPlayView::MainHud => { - // THE 3 ICONIC SHADOWPLAY HERO TILES - ui.horizontal(|ui| { - let available_w = ui.available_width(); - let card_w = ((available_w - 32.0) / 3.0).clamp(240.0, 270.0); - let card_h = 285.0; - - // CARD 1: INSTANT REPLAY - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.2_f32, if self.status.is_replay_active { Color32::from_rgb(55, 85, 25) } else { Color32::from_rgb(34, 44, 58) })) - .corner_radius(CornerRadius::same(12_u8)) - .inner_margin(Margin::symmetric(14_i8, 16_i8)) - .show(ui, |ui| { - ui.set_width(card_w); - ui.set_height(card_h); - ui.vertical_centered(|ui| { - let (btn, p, r) = icon_button(ui, Vec2::new(82.0, 82.0), "Save Instant Replay (Ctrl+Shift+R)"); - let circle_center = r.center(); - let fill_color = if self.status.is_replay_active { Color32::from_rgb(24, 44, 16) } else { Color32::from_rgb(20, 26, 36) }; - let stroke_color = if btn.hovered() { Color32::from_rgb(150, 220, 20) } else if self.status.is_replay_active { Color32::from_rgb(118, 185, 0) } else { Color32::from_rgb(60, 75, 95) }; - p.circle_filled(circle_center, 38.0, fill_color); - p.circle_stroke(circle_center, 38.0, Stroke::new(2.5_f32, stroke_color)); - draw_replay_icon(&p, circle_center, 22.0, stroke_color); - - if btn.clicked() { - let _ = ipc::send_command(Command::SaveReplay); - self.set_msg("Instant Replay saved!"); - self.refresh_clips(); - } - - ui.add_space(10.0); - ui.label( - egui::RichText::new("Instant Replay") - .font(FontId::proportional(15.0)) - .strong() - .color(Color32::WHITE), - ); - - if self.status.is_replay_active { - ui.label(egui::RichText::new("● ON").font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(118, 185, 0))); - } else { - ui.label(egui::RichText::new("OFF").font(FontId::monospace(11.0)).color(Color32::from_rgb(100, 116, 139))); - } - - ui.add_space(12.0); - let save_btn = egui::Button::new(egui::RichText::new("Save Replay").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(10, 16, 8))) - .fill(Color32::from_rgb(118, 185, 0)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(150, 220, 20))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([card_w - 28.0, 32.0], save_btn).clicked() { - let _ = ipc::send_command(Command::SaveReplay); - self.set_msg("Instant Replay saved!"); - self.refresh_clips(); - } - - ui.add_space(6.0); - let buffer_lbl = egui::Button::new(egui::RichText::new(format!("Buffer: {}s", self.replay_sec)).font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184))) - .fill(Color32::from_rgb(22, 28, 38)) - .corner_radius(CornerRadius::same(5_u8)); - if ui.add_sized([card_w - 28.0, 24.0], buffer_lbl).clicked() { - self.current_view = ShadowPlayView::Settings; - } - }); - }); - - ui.add_space(16.0); - - // CARD 2: RECORD - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.2_f32, if self.status.is_recording { Color32::from_rgb(140, 35, 35) } else { Color32::from_rgb(34, 44, 58) })) - .corner_radius(CornerRadius::same(12_u8)) - .inner_margin(Margin::symmetric(14_i8, 16_i8)) - .show(ui, |ui| { - ui.set_width(card_w); - ui.set_height(card_h); - ui.vertical_centered(|ui| { - let rec_tip = if self.status.is_recording { "Stop Recording (Ctrl+Shift+F9)" } else { "Start Recording (Ctrl+Shift+F9)" }; - let (btn, p, r) = icon_button(ui, Vec2::new(82.0, 82.0), rec_tip); - let circle_center = r.center(); - let fill_color = if self.status.is_recording { Color32::from_rgb(45, 12, 14) } else { Color32::from_rgb(20, 26, 36) }; - let stroke_color = if self.status.is_recording { Color32::from_rgb(239, 68, 68) } else if btn.hovered() { Color32::from_rgb(203, 213, 225) } else { Color32::from_rgb(71, 85, 105) }; - p.circle_filled(circle_center, 38.0, fill_color); - p.circle_stroke(circle_center, 38.0, Stroke::new(2.5_f32, stroke_color)); - draw_record_icon(&p, circle_center, 22.0, self.status.is_recording, self.anim_time); - - if btn.clicked() { - let _ = ipc::send_command(Command::ToggleRecording); - if self.status.is_recording { - self.set_msg("Recording stopped and saved!"); - self.refresh_clips(); - } else { - self.set_msg("Recording started"); - } - } - - ui.add_space(10.0); - ui.label( - egui::RichText::new("Record") - .font(FontId::proportional(15.0)) - .strong() - .color(Color32::WHITE), - ); - - if self.status.is_recording { - let dur = self.status.recording_duration_sec; - ui.label(egui::RichText::new(format!("● {:02}:{:02}", dur / 60, dur % 60)).font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(239, 68, 68))); - } else { - ui.label(egui::RichText::new("OFF").font(FontId::monospace(11.0)).color(Color32::from_rgb(100, 116, 139))); - } - - ui.add_space(12.0); - if self.status.is_recording { - let stop_btn = egui::Button::new(egui::RichText::new("Stop Recording").font(FontId::proportional(12.0)).strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(239, 68, 68)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(248, 113, 113))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([card_w - 28.0, 32.0], stop_btn).clicked() { - let _ = ipc::send_command(Command::StopRecording); - self.set_msg("Recording saved to disk!"); - self.refresh_clips(); - } - } else { - let start_btn = egui::Button::new(egui::RichText::new("Start Recording").font(FontId::proportional(12.0)).strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(30, 41, 59)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(71, 85, 105))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([card_w - 28.0, 32.0], start_btn).clicked() { - let _ = ipc::send_command(Command::StartRecording); - self.set_msg("Recording started"); - } - } - - ui.add_space(6.0); - let q_lbl = egui::Button::new(egui::RichText::new(format!("{} fps • {} Mbps", self.target_fps, self.bitrate_mbps)).font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184))) - .fill(Color32::from_rgb(22, 28, 38)) - .corner_radius(CornerRadius::same(5_u8)); - if ui.add_sized([card_w - 28.0, 24.0], q_lbl).clicked() { - self.current_view = ShadowPlayView::Settings; - } - }); - }); - - ui.add_space(16.0); - - // CARD 3: GALLERY - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.2_f32, Color32::from_rgb(34, 44, 58))) - .corner_radius(CornerRadius::same(12_u8)) - .inner_margin(Margin::symmetric(14_i8, 16_i8)) - .show(ui, |ui| { - ui.set_width(card_w); - ui.set_height(card_h); - ui.vertical_centered(|ui| { - let (btn, p, r) = icon_button(ui, Vec2::new(82.0, 82.0), "Browse Recordings & Clips"); - let circle_center = r.center(); - let fill_color = Color32::from_rgb(14, 26, 38); - let stroke_color = if btn.hovered() { Color32::from_rgb(56, 189, 248) } else { Color32::from_rgb(14, 165, 233) }; - p.circle_filled(circle_center, 38.0, fill_color); - p.circle_stroke(circle_center, 38.0, Stroke::new(2.5_f32, stroke_color)); - draw_gallery_icon(&p, circle_center, 20.0, stroke_color); - - if btn.clicked() { - self.refresh_clips(); - self.current_view = ShadowPlayView::Gallery; - } - - ui.add_space(10.0); - ui.label( - egui::RichText::new("Gallery") - .font(FontId::proportional(15.0)) - .strong() - .color(Color32::WHITE), - ); - - ui.label(egui::RichText::new(format!("{} CLIPS", self.clips.len())).font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(56, 189, 248))); - - ui.add_space(12.0); - let gal_btn = egui::Button::new(egui::RichText::new("View Clips").font(FontId::proportional(12.0)).strong().color(Color32::WHITE)) - .fill(Color32::from_rgb(2, 132, 199)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(56, 189, 248))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([card_w - 28.0, 32.0], gal_btn).clicked() { - self.refresh_clips(); - self.current_view = ShadowPlayView::Gallery; - } - - ui.add_space(6.0); - let folder_btn = egui::Button::new(egui::RichText::new("Open Folder").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184))) - .fill(Color32::from_rgb(22, 28, 38)) - .corner_radius(CornerRadius::same(5_u8)); - if ui.add_sized([card_w - 28.0, 24.0], folder_btn).clicked() { - let resolved = VrecConfig::expand_tilde(&self.output_dir); - let _ = std::fs::create_dir_all(&resolved); - open_folder(&resolved); - } - }); - }); - }); - - ui.add_space(14.0); - - // BOTTOM STATUS BAR - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Shortcuts:").font(FontId::proportional(11.0)).color(Color32::from_rgb(100, 116, 139))); - render_keycap(ui, &self.config.menu_hotkey); - ui.label(egui::RichText::new("Menu").font(FontId::proportional(10.0)).color(Color32::from_rgb(148, 163, 184))); - - ui.add_space(8.0); - render_keycap(ui, &self.config.save_hotkey); - ui.label(egui::RichText::new("Save").font(FontId::proportional(10.0)).color(Color32::from_rgb(148, 163, 184))); - - ui.add_space(8.0); - render_keycap(ui, &self.config.record_hotkey); - ui.label(egui::RichText::new("Record").font(FontId::proportional(10.0)).color(Color32::from_rgb(148, 163, 184))); - - ui.add_space(8.0); - render_keycap(ui, &self.config.cursor_hotkey); - ui.label(egui::RichText::new("Cursor").font(FontId::proportional(10.0)).color(Color32::from_rgb(148, 163, 184))); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if self.daemon_connected { - let (r, _) = ui.allocate_exact_size(Vec2::new(8.0, 8.0), egui::Sense::hover()); - ui.painter().circle_filled(r.center(), 3.5, Color32::from_rgb(118, 185, 0)); - ui.label(egui::RichText::new("Daemon Online").font(FontId::monospace(9.5)).color(Color32::from_rgb(118, 185, 0))); - } else { - let (r, _) = ui.allocate_exact_size(Vec2::new(8.0, 8.0), egui::Sense::hover()); - ui.painter().circle_filled(r.center(), 3.5, Color32::from_rgb(239, 68, 68)); - ui.label(egui::RichText::new("Daemon Offline").font(FontId::monospace(9.5)).color(Color32::from_rgb(239, 68, 68))); - } - - ui.add_space(10.0); - #[cfg(target_os = "windows")] - let codec_str = "D3D11 / NVENC H.264"; - #[cfg(not(target_os = "windows"))] - let codec_str = "Hardware VAAPI H.264"; - ui.label(egui::RichText::new(codec_str).font(FontId::monospace(9.5)).color(Color32::from_rgb(100, 116, 139))); - }); - }); - } - - ShadowPlayView::Settings => { - // SUBVIEW: SETTINGS - ui.horizontal(|ui| { - let back_btn = egui::Button::new(egui::RichText::new("< Back to Overlay").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(118, 185, 0))) - .fill(Color32::from_rgb(20, 32, 16)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(60, 95, 25))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add(back_btn).clicked() { - self.current_view = ShadowPlayView::MainHud; - } - - ui.add_space(8.0); - ui.label(egui::RichText::new("SHADOWPLAY SETTINGS").font(FontId::proportional(14.0)).strong().color(Color32::WHITE)); - }); - - ui.add_space(10.0); - - egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| { - // Save Directory Card - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 58))) - .corner_radius(CornerRadius::same(8_u8)) - .inner_margin(Margin::same(12_i8)) - .show(ui, |ui| { - ui.label(egui::RichText::new("SAVE DESTINATION DIRECTORY").font(FontId::proportional(11.5)).strong().color(Color32::from_rgb(118, 185, 0))); - ui.add_space(6.0); - ui.horizontal(|ui| { - ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(520.0)); - - let browse_btn = egui::Button::new(egui::RichText::new("Browse...").color(Color32::WHITE)) - .fill(Color32::from_rgb(37, 99, 235)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250))) - .corner_radius(CornerRadius::same(5_u8)); - if ui.add(browse_btn).clicked() { - let cur = VrecConfig::expand_tilde(&self.output_dir).to_string_lossy().to_string(); - pick_folder(&cur, self.folder_tx.clone()); - } - - let open_btn = egui::Button::new(egui::RichText::new("Open").color(Color32::from_rgb(203, 213, 225))) - .fill(Color32::from_rgb(30, 41, 59)) - .corner_radius(CornerRadius::same(5_u8)); - if ui.add(open_btn).clicked() { - let dir = VrecConfig::expand_tilde(&self.output_dir); - let _ = std::fs::create_dir_all(&dir); - open_folder(&dir); - } - }); - - ui.add_space(6.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Quick Presets:").font(FontId::proportional(10.0)).color(Color32::from_rgb(100, 116, 139))); - let v_base = dirs::video_dir().or_else(|| dirs::home_dir().map(|h| h.join("Videos"))); - let d_base = dirs::download_dir().or_else(|| dirs::home_dir().map(|h| h.join("Downloads"))); - if let Some(ref vb) = v_base { - let v_dir = vb.join("vrec").to_string_lossy().to_string(); - let c_dir = vb.join("Captures").to_string_lossy().to_string(); - if pill(ui, "Videos/vrec", self.output_dir == v_dir) { self.output_dir = v_dir; } - if pill(ui, "Videos/Captures", self.output_dir == c_dir) { self.output_dir = c_dir; } - } - if let Some(ref db) = d_base { - let d_dir = db.join("vrec").to_string_lossy().to_string(); - if pill(ui, "Downloads/vrec", self.output_dir == d_dir) { self.output_dir = d_dir; } - } - }); - }); - - ui.add_space(8.0); - - // Two-column Settings - ui.columns(2, |cols| { - // Column 1: Video Quality & Cursor - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 58))) - .corner_radius(CornerRadius::same(8_u8)) - .inner_margin(Margin::same(12_i8)) - .show(&mut cols[0], |ui| { - ui.label(egui::RichText::new("VIDEO QUALITY & ENCODING").font(FontId::proportional(11.5)).strong().color(Color32::from_rgb(118, 185, 0))); - ui.add_space(6.0); - - ui.label(egui::RichText::new("Target Framerate").font(FontId::proportional(10.5)).color(Color32::from_rgb(203, 213, 225))); - ui.horizontal(|ui| { - ui.add(egui::DragValue::new(&mut self.target_fps).range(15..=240).suffix(" fps")); - if pill(ui, "60", self.target_fps == 60) { self.target_fps = 60; } - if pill(ui, "120", self.target_fps == 120) { self.target_fps = 120; } - if pill(ui, "144", self.target_fps == 144) { self.target_fps = 144; } - }); - - ui.add_space(8.0); - ui.label(egui::RichText::new("Video Bitrate").font(FontId::proportional(10.5)).color(Color32::from_rgb(203, 213, 225))); - ui.horizontal(|ui| { - ui.add(egui::DragValue::new(&mut self.bitrate_mbps).range(2..=120).suffix(" Mbps")); - if pill(ui, "10M", self.bitrate_mbps == 10) { self.bitrate_mbps = 10; } - if pill(ui, "20M", self.bitrate_mbps == 20) { self.bitrate_mbps = 20; } - if pill(ui, "30M", self.bitrate_mbps == 30) { self.bitrate_mbps = 30; } - }); - - ui.add_space(8.0); - ui.label(egui::RichText::new("Mouse Cursor").font(FontId::proportional(10.5)).color(Color32::from_rgb(203, 213, 225))); - ui.horizontal(|ui| { - if pill(ui, "Show Cursor", self.show_cursor) { self.show_cursor = true; } - if pill(ui, "Hide Cursor", !self.show_cursor) { self.show_cursor = false; } - }); - - ui.add_space(6.0); - #[cfg(target_os = "windows")] - let codec_info = "Codec: Hardware D3D11 / NVENC H.264"; - #[cfg(not(target_os = "windows"))] - let codec_info = "Codec: Hardware VAAPI H.264 (NV12 Direct Buffer Sharing)"; - ui.label(egui::RichText::new(codec_info).font(FontId::monospace(9.0)).color(Color32::from_rgb(100, 116, 139))); - }); - - // Column 2: Replay Buffer & Hotkeys - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 58))) - .corner_radius(CornerRadius::same(8_u8)) - .inner_margin(Margin::same(12_i8)) - .show(&mut cols[1], |ui| { - ui.label(egui::RichText::new("REPLAY BUFFER & SHORTCUTS").font(FontId::proportional(11.5)).strong().color(Color32::from_rgb(168, 85, 247))); - ui.add_space(6.0); - - ui.label(egui::RichText::new("Buffer Duration").font(FontId::proportional(10.5)).color(Color32::from_rgb(203, 213, 225))); - ui.horizontal(|ui| { - ui.add(egui::DragValue::new(&mut self.replay_sec).range(5..=600).suffix(" s")); - if pill(ui, "30s", self.replay_sec == 30) { self.replay_sec = 30; } - if pill(ui, "60s", self.replay_sec == 60) { self.replay_sec = 60; } - if pill(ui, "120s", self.replay_sec == 120) { self.replay_sec = 120; } - }); - - ui.add_space(10.0); - ui.label(egui::RichText::new("Configured Global Hotkeys").font(FontId::proportional(10.5)).color(Color32::from_rgb(203, 213, 225))); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Menu:").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184))); - render_keycap(ui, &self.config.menu_hotkey); - ui.add_space(4.0); - ui.label(egui::RichText::new("Save:").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184))); - render_keycap(ui, &self.config.save_hotkey); - }); - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Record:").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184))); - render_keycap(ui, &self.config.record_hotkey); - ui.add_space(4.0); - ui.label(egui::RichText::new("Cursor:").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184))); - render_keycap(ui, &self.config.cursor_hotkey); - }); - }); - }); - - ui.add_space(10.0); - - // Apply Button - let apply_btn = egui::Button::new(egui::RichText::new("Save & Apply All Settings").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(10, 16, 8))) - .fill(Color32::from_rgb(118, 185, 0)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(150, 220, 20))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([260.0, 34.0], apply_btn).clicked() { - self.config.output_directory = self.output_dir.clone(); - let resolved = VrecConfig::expand_tilde(&self.output_dir); - let _ = std::fs::create_dir_all(&resolved); - self.config.replay_duration_sec = self.replay_sec; - self.config.record_bitrate_kbps = self.bitrate_mbps * 1000; - self.config.replay_bitrate_kbps = self.bitrate_mbps * 1000; - self.config.fps = self.target_fps; - self.config.show_cursor = self.show_cursor; - self.config.mic_volume = self.mic_volume_pct as f32 / 100.0; - self.config.system_volume = self.system_volume_pct as f32 / 100.0; - self.config.audio_mode = match self.audio_mode_idx { - 1 => "mic", - 2 => "both", - 3 => "muted", - _ => "system", - }.to_string(); - let _ = self.config.save(); - let _ = ipc::send_command(Command::ReloadConfig); - self.refresh_clips(); - self.set_msg("Settings saved and reloaded!"); - } - }); - } - - ShadowPlayView::Gallery => { - // SUBVIEW: RECORDINGS GALLERY - ui.horizontal(|ui| { - let back_btn = egui::Button::new(egui::RichText::new("< Back to Overlay").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(14, 165, 233))) - .fill(Color32::from_rgb(16, 28, 42)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(30, 60, 90))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add(back_btn).clicked() { - self.current_view = ShadowPlayView::MainHud; - } - - ui.add_space(8.0); - ui.label(egui::RichText::new("RECORDINGS GALLERY").font(FontId::proportional(14.0)).strong().color(Color32::WHITE)); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let open_btn = egui::Button::new(egui::RichText::new("Open Folder").size(11.0).color(Color32::WHITE)) - .fill(Color32::from_rgb(37, 99, 235)) - .corner_radius(CornerRadius::same(5_u8)); - if ui.add_sized([90.0, 24.0], open_btn).clicked() { - let resolved = VrecConfig::expand_tilde(&self.output_dir); - let _ = std::fs::create_dir_all(&resolved); - open_folder(&resolved); - } - - let refresh_btn = egui::Button::new(egui::RichText::new("Refresh").size(11.0).color(Color32::from_rgb(203, 213, 225))) - .fill(Color32::from_rgb(30, 41, 59)) - .corner_radius(CornerRadius::same(5_u8)); - if ui.add_sized([70.0, 24.0], refresh_btn).clicked() { - self.refresh_clips(); - } - - if pill(ui, "Recordings", self.gallery_filter_idx == 2) { self.gallery_filter_idx = 2; } - if pill(ui, "Replays", self.gallery_filter_idx == 1) { self.gallery_filter_idx = 1; } - if pill(ui, "All", self.gallery_filter_idx == 0) { self.gallery_filter_idx = 0; } - }); - }); - - ui.add_space(10.0); - - let filtered: Vec<&VideoClipInfo> = self.clips.iter().filter(|c| { - match self.gallery_filter_idx { - 1 => c.is_replay, - 2 => !c.is_replay, - _ => true, - } - }).collect(); - - egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| { - if filtered.is_empty() { - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 58))) - .corner_radius(CornerRadius::same(10_u8)) - .inner_margin(Margin::same(24_i8)) - .show(ui, |ui| { - ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("NO RECORDINGS FOUND").font(FontId::monospace(13.0)).strong().color(Color32::from_rgb(148, 163, 184))); - ui.add_space(4.0); - ui.label(egui::RichText::new("Capture your first instant replay or manual recording to see it here.").font(FontId::proportional(11.0)).color(Color32::from_rgb(100, 116, 139))); - }); - }); - } else { - for clip in filtered { - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 28)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(30, 38, 52))) - .corner_radius(CornerRadius::same(6_u8)) - .inner_margin(Margin::symmetric(12_i8, 8_i8)) - .show(ui, |ui| { - ui.horizontal(|ui| { - // Type badge - if clip.is_replay { - egui::Frame::NONE - .fill(Color32::from_rgba_unmultiplied(118, 185, 0, 30)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(118, 185, 0))) - .corner_radius(CornerRadius::same(4_u8)) - .inner_margin(Margin::symmetric(6_i8, 2_i8)) - .show(ui, |ui| { - ui.label(egui::RichText::new("REPLAY").font(FontId::monospace(9.5)).strong().color(Color32::from_rgb(163, 230, 53))); - }); - } else { - egui::Frame::NONE - .fill(Color32::from_rgba_unmultiplied(239, 68, 68, 30)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(239, 68, 68))) - .corner_radius(CornerRadius::same(4_u8)) - .inner_margin(Margin::symmetric(6_i8, 2_i8)) - .show(ui, |ui| { - ui.label(egui::RichText::new("REC").font(FontId::monospace(9.5)).strong().color(Color32::from_rgb(248, 113, 113))); - }); - } - - ui.add_space(8.0); - ui.vertical(|ui| { - ui.label(egui::RichText::new(&clip.filename).font(FontId::monospace(11.5)).strong().color(Color32::from_rgb(241, 245, 249))); - ui.label(egui::RichText::new(format!("{} • {}", format_file_size(clip.size_bytes), format_system_time(clip.modified))).font(FontId::proportional(10.0)).color(Color32::from_rgb(148, 163, 184))); - }); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let play_btn = egui::Button::new(egui::RichText::new("Play Video").size(11.0).color(Color32::WHITE)) - .fill(Color32::from_rgb(16, 185, 129)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(52, 211, 153))) - .corner_radius(CornerRadius::same(5_u8)); - if ui.add_sized([85.0, 26.0], play_btn).clicked() { - open_file(&clip.path); - } - }); - }); - }); - ui.add_space(4.0); - } - } - }); - } - - ShadowPlayView::AudioMixer => { - // SUBVIEW: AUDIO MIXER - ui.horizontal(|ui| { - let back_btn = egui::Button::new(egui::RichText::new("< Back to Overlay").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(118, 185, 0))) - .fill(Color32::from_rgb(20, 32, 16)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(60, 95, 25))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add(back_btn).clicked() { - self.current_view = ShadowPlayView::MainHud; - } - - ui.add_space(8.0); - ui.label(egui::RichText::new("AUDIO MIXER & ROUTING").font(FontId::proportional(14.0)).strong().color(Color32::WHITE)); - }); - - ui.add_space(10.0); - - egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| { - // Audio Routing Mode Card - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 58))) - .corner_radius(CornerRadius::same(8_u8)) - .inner_margin(Margin::same(12_i8)) - .show(ui, |ui| { - ui.label(egui::RichText::new("ACTIVE AUDIO ROUTING MODE").font(FontId::proportional(11.5)).strong().color(Color32::from_rgb(118, 185, 0))); - ui.add_space(8.0); - ui.horizontal(|ui| { - if pill(ui, "System Desktop Audio", self.audio_mode_idx == 0) { self.audio_mode_idx = 0; } - if pill(ui, "Microphone Only", self.audio_mode_idx == 1) { self.audio_mode_idx = 1; } - if pill(ui, "Combined (Stereo)", self.audio_mode_idx == 2) { self.audio_mode_idx = 2; } - if pill(ui, "Mute All", self.audio_mode_idx == 3) { self.audio_mode_idx = 3; } - }); - }); - - ui.add_space(10.0); - - // Sliders & VU Meters - egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 27)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 58))) - .corner_radius(CornerRadius::same(8_u8)) - .inner_margin(Margin::same(12_i8)) - .show(ui, |ui| { - // Microphone Section - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Microphone Input Gain").font(FontId::proportional(11.0)).strong().color(Color32::from_rgb(203, 213, 225))); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(egui::RichText::new(format!("{}%", self.mic_volume_pct)).font(FontId::monospace(11.0)).color(Color32::from_rgb(118, 185, 0))); - }); - }); - ui.add(egui::Slider::new(&mut self.mic_volume_pct, 0..=200).suffix("%")); - ui.add_space(4.0); - let mic_active = self.audio_mode_idx == 1 || self.audio_mode_idx == 2; - render_vu_meter(ui, self.mic_volume_pct as f32, mic_active, self.anim_time); - - ui.add_space(12.0); - - // System Audio Section - ui.horizontal(|ui| { - ui.label(egui::RichText::new("System Desktop Audio Gain").font(FontId::proportional(11.0)).strong().color(Color32::from_rgb(203, 213, 225))); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(egui::RichText::new(format!("{}%", self.system_volume_pct)).font(FontId::monospace(11.0)).color(Color32::from_rgb(118, 185, 0))); - }); - }); - ui.add(egui::Slider::new(&mut self.system_volume_pct, 0..=200).suffix("%")); - ui.add_space(4.0); - let sys_active = self.audio_mode_idx == 0 || self.audio_mode_idx == 2; - render_vu_meter(ui, self.system_volume_pct as f32, sys_active, self.anim_time + 1.5); - }); - - ui.add_space(10.0); - - let apply_btn = egui::Button::new(egui::RichText::new("Save & Apply Audio Settings").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(10, 16, 8))) - .fill(Color32::from_rgb(118, 185, 0)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(150, 220, 20))) - .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([260.0, 34.0], apply_btn).clicked() { - self.config.mic_volume = self.mic_volume_pct as f32 / 100.0; - self.config.system_volume = self.system_volume_pct as f32 / 100.0; - self.config.audio_mode = match self.audio_mode_idx { - 1 => "mic", - 2 => "both", - 3 => "muted", - _ => "system", - }.to_string(); - let _ = self.config.save(); - let _ = ipc::send_command(Command::ReloadConfig); - self.set_msg("Audio settings applied!"); - } - }); - } - } + .show(ctx, |ui| match self.current_view { + ShadowPlayView::MainHud => self.render_main_hud(ctx, ui), + ShadowPlayView::Settings => self.render_settings_view(ctx, ui), }); } } @@ -1344,10 +887,11 @@ pub fn run_egui_overlay() { viewport: egui::ViewportBuilder::default() .with_title("vrec ShadowPlay") .with_app_id("vrec-overlay") - .with_inner_size([880.0, 420.0]) - .with_min_inner_size([800.0, 380.0]) - .with_max_inner_size([1100.0, 600.0]) - .with_resizable(true) + .with_position([400.0, 45.0]) + .with_inner_size([780.0, 240.0]) + .with_min_inner_size([740.0, 220.0]) + .with_max_inner_size([1100.0, 800.0]) + .with_resizable(false) .with_decorations(false) .with_transparent(true) .with_always_on_top(),