From 524f771cbfdb6c0abbfb0bb84deae1383d62a16b Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Fri, 4 Sep 2026 20:16:01 +0530 Subject: [PATCH] feat(audio, ui): add studio audio mixer, soft limiter, and revamped cyber-slate overlay --- src/bin/vrec-daemon.rs | 12 +- src/bin/vrec-ui.rs | 19 ++ src/capture/audio.rs | 62 ++++-- src/config.rs | 10 + src/ipc.rs | 12 ++ src/overlay_egui.rs | 460 +++++++++++++++++++++++++++++++---------- 6 files changed, 444 insertions(+), 131 deletions(-) diff --git a/src/bin/vrec-daemon.rs b/src/bin/vrec-daemon.rs index f364e9f..7fbe827 100644 --- a/src/bin/vrec-daemon.rs +++ b/src/bin/vrec-daemon.rs @@ -125,10 +125,12 @@ async fn main() -> Result<(), Box> { let (cmd_tx, cmd_rx) = bounded::(10); let (mux_tx, mux_rx) = bounded::>(1); let (audio_tx, audio_rx) = bounded::>(500); - let audio_capture_initial = vrec::capture::audio::AudioCapture::new_with_device_and_mode( + let audio_capture_initial = vrec::capture::audio::AudioCapture::new_with_device_mode_and_volumes( audio_tx.clone(), Some(&initial_config.audio_device), &initial_config.audio_mode, + initial_config.mic_volume, + initial_config.system_volume, ).ok(); // Automatically register dynamic keybindings on Hyprland if active (no config edits needed) @@ -301,13 +303,15 @@ async fn main() -> Result<(), Box> { println!("Replay buffer resized to {} packets.", new_capacity); } vrec::hyprland_binds::register_hyprland_binds(&config); - audio_capture = match vrec::capture::audio::AudioCapture::new_with_device_and_mode( + audio_capture = match vrec::capture::audio::AudioCapture::new_with_device_mode_and_volumes( audio_tx_clone.clone(), Some(&config.audio_device), &config.audio_mode, + config.mic_volume, + config.system_volume, ) { Ok((c, _, _)) => { - println!("Audio capture reloaded for mode: {}", config.audio_mode); + println!("Audio capture reloaded for mode: {} (mic: {:.0}%, sys: {:.0}%)", config.audio_mode, config.mic_volume * 100.0, config.system_volume * 100.0); Some(c) } Err(e) => { @@ -512,6 +516,8 @@ async fn main() -> Result<(), Box> { audio_muted: audio_muted_state.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(); diff --git a/src/bin/vrec-ui.rs b/src/bin/vrec-ui.rs index 38d5b3a..165d83e 100644 --- a/src/bin/vrec-ui.rs +++ b/src/bin/vrec-ui.rs @@ -136,6 +136,24 @@ fn main() -> Result<(), Box> { "--reload" => { return send_command(Command::ReloadConfig); } + "--status" => { + match query_status() { + Ok(st) => { + println!("Daemon Status:"); + println!(" State: {}", if st.is_recording { format!("RECORDING ({}s)", st.recording_duration_sec) } else { "IDLE".to_string() }); + println!(" Instant Replay: {}", if st.is_replay_active { "ACTIVE" } else { "OFF" }); + println!(" Audio Mode: {}", st.audio_mode); + println!(" Mic Volume: {:.0}%", st.mic_volume * 100.0); + println!(" System Volume: {:.0}%", st.system_volume * 100.0); + println!(" Show Cursor: {}", st.show_cursor); + return Ok(()); + } + Err(e) => { + eprintln!("Failed to connect to vrec daemon: {}", e); + std::process::exit(1); + } + } + } "--quit" => { return send_command(Command::StopDaemon); } @@ -144,6 +162,7 @@ fn main() -> Result<(), Box> { println!("Usage:"); println!(" vrec-ui Run global hotkey manager in background"); println!(" vrec-ui --menu Open overlay menu"); + println!(" vrec-ui --status Query current daemon status"); println!(" vrec-ui --save Save instant replay and show notification"); println!(" vrec-ui --record Toggle normal recording on/off (with notification)"); println!(" vrec-ui --cursor Toggle mouse cursor recording on/off (with notification)"); diff --git a/src/capture/audio.rs b/src/capture/audio.rs index 67490e5..eecefd2 100644 --- a/src/capture/audio.rs +++ b/src/capture/audio.rs @@ -54,6 +54,17 @@ pub fn list_input_devices() -> Vec { names } +#[inline] +pub fn soft_limit(x: f32) -> f32 { + if x.abs() <= 0.75 { + x + } else if x > 0.0 { + 0.75 + 0.24 * ((x - 0.75) / 0.24).tanh() + } else { + -0.75 - 0.24 * ((-x - 0.75) / 0.24).tanh() + } +} + #[cfg(unix)] fn is_parec_available() -> bool { std::process::Command::new("parec") @@ -69,6 +80,7 @@ fn is_parec_available() -> bool { fn spawn_parec_stream( device: &str, sender: Sender>, + gain: f32, ) -> Result> { use std::io::Read; @@ -113,7 +125,8 @@ fn spawn_parec_stream( combined[i * 4 + 2], combined[i * 4 + 3], ]; - floats.push(f32::from_le_bytes(b)); + let raw = f32::from_le_bytes(b); + floats.push(soft_limit(raw * gain)); } remainder.clear(); @@ -133,20 +146,30 @@ fn spawn_parec_stream( impl AudioCapture { pub fn new(sender: Sender>) -> Result<(Self, u32, u16), Box> { - Self::new_with_device_and_mode(sender, None, "system") + Self::new_with_device_mode_and_volumes(sender, None, "system", 0.60, 1.0) } pub fn new_with_device( sender: Sender>, device_name: Option<&str>, ) -> Result<(Self, u32, u16), Box> { - Self::new_with_device_and_mode(sender, device_name, "system") + Self::new_with_device_mode_and_volumes(sender, device_name, "system", 0.60, 1.0) } pub fn new_with_device_and_mode( sender: Sender>, device_name: Option<&str>, audio_mode: &str, + ) -> Result<(Self, u32, u16), Box> { + Self::new_with_device_mode_and_volumes(sender, device_name, audio_mode, 0.60, 1.0) + } + + pub fn new_with_device_mode_and_volumes( + sender: Sender>, + device_name: Option<&str>, + audio_mode: &str, + mic_volume: f32, + system_volume: f32, ) -> Result<(Self, u32, u16), Box> { if audio_mode == "muted" { println!("Audio capture mode: MUTED (No audio recorded)"); @@ -165,8 +188,8 @@ impl AudioCapture { let mic_target = device_name .filter(|d| *d != "default" && !d.trim().is_empty()) .unwrap_or("@DEFAULT_SOURCE@"); - let child = spawn_parec_stream(mic_target, sender)?; - println!("Audio capture mode: MICROPHONE ONLY [{}] (48000 Hz, 2 ch)", mic_target); + let child = spawn_parec_stream(mic_target, sender, mic_volume)?; + println!("Audio capture mode: MICROPHONE ONLY [{}] (vol: {:.0}%, 48000 Hz, 2 ch)", mic_target, mic_volume * 100.0); return Ok((Self { _processes: vec![child], _streams: Vec::new(), @@ -176,11 +199,11 @@ impl AudioCapture { let (sys_tx, sys_rx) = crossbeam_channel::bounded::>(100); let (mic_tx, mic_rx) = crossbeam_channel::bounded::>(100); - let sys_child = spawn_parec_stream("@DEFAULT_MONITOR@", sys_tx)?; + let sys_child = spawn_parec_stream("@DEFAULT_MONITOR@", sys_tx, system_volume)?; let mic_target = device_name .filter(|d| *d != "default" && !d.trim().is_empty()) .unwrap_or("@DEFAULT_SOURCE@"); - let mic_child = spawn_parec_stream(mic_target, mic_tx)?; + let mic_child = spawn_parec_stream(mic_target, mic_tx, mic_volume)?; let out_tx = sender; thread::spawn(move || { @@ -201,7 +224,7 @@ impl AudioCapture { for _ in 0..avail { let s = sys_q.pop_front().unwrap_or(0.0); let m = mic_q.pop_front().unwrap_or(0.0); - mixed.push((s + m * 0.9).clamp(-1.0, 1.0)); + mixed.push(soft_limit(s + m)); } let _ = out_tx.try_send(mixed); } else if sys_q.len() > 4800 { @@ -216,7 +239,7 @@ impl AudioCapture { } }); - println!("Audio capture mode: BOTH (System [@DEFAULT_MONITOR@] + Mic [{}]) (48000 Hz, 2 ch)", mic_target); + println!("Audio capture mode: BOTH (System [@DEFAULT_MONITOR@, {:.0}%] + Mic [{}, {:.0}%]) (48000 Hz, 2 ch)", system_volume * 100.0, mic_target, mic_volume * 100.0); return Ok((Self { _processes: vec![sys_child, mic_child], _streams: Vec::new(), @@ -224,8 +247,8 @@ impl AudioCapture { } _ => { // System audio only - let child = spawn_parec_stream("@DEFAULT_MONITOR@", sender)?; - println!("Audio capture mode: SYSTEM AUDIO ONLY [@DEFAULT_MONITOR@] (48000 Hz, 2 ch)"); + let child = spawn_parec_stream("@DEFAULT_MONITOR@", sender, system_volume)?; + println!("Audio capture mode: SYSTEM AUDIO ONLY [@DEFAULT_MONITOR@] (vol: {:.0}%, 48000 Hz, 2 ch)", system_volume * 100.0); return Ok((Self { _processes: vec![child], _streams: Vec::new(), @@ -272,7 +295,7 @@ impl AudioCapture { host.default_input_device() }; - let build_stream = |dev: &cpal::Device, tx: Sender>| -> Result<(cpal::Stream, u32, u16), Box> { + let build_stream = |dev: &cpal::Device, tx: Sender>, gain: f32| -> Result<(cpal::Stream, u32, u16), Box> { let config = dev.default_input_config()?; let sample_rate = config.sample_rate(); let channels = config.channels(); @@ -285,7 +308,8 @@ impl AudioCapture { dev.build_input_stream( stream_config, move |data: &[f32], _: &_| { - let _ = tx.try_send(data.to_vec()); + let f32_data: Vec = data.iter().map(|&s| soft_limit(s * gain)).collect(); + let _ = tx.try_send(f32_data); }, err_fn, None, @@ -295,7 +319,7 @@ impl AudioCapture { dev.build_input_stream( stream_config, move |data: &[i16], _: &_| { - let f32_data: Vec = data.iter().map(|&s| s as f32 / i16::MAX as f32).collect(); + let f32_data: Vec = data.iter().map(|&s| soft_limit((s as f32 / i16::MAX as f32) * gain)).collect(); let _ = tx.try_send(f32_data); }, err_fn, @@ -312,7 +336,7 @@ impl AudioCapture { "mic" => { let dev = find_mic_device().ok_or("No microphone device found")?; let name = get_device_name(&dev); - let (stream, sr, ch) = build_stream(&dev, sender)?; + let (stream, sr, ch) = build_stream(&dev, sender, mic_volume)?; println!("Audio capture mode: MICROPHONE ONLY [{}] ({} Hz, {} ch)", name, sr, ch); Ok((Self { #[cfg(unix)] @@ -330,8 +354,8 @@ impl AudioCapture { let (sys_tx, sys_rx) = crossbeam_channel::bounded::>(100); let (mic_tx, mic_rx) = crossbeam_channel::bounded::>(100); - let (sys_stream, sr, ch) = build_stream(&sys_dev, sys_tx)?; - let (mic_stream, _, _) = build_stream(&mic_dev, mic_tx)?; + let (sys_stream, sr, ch) = build_stream(&sys_dev, sys_tx, system_volume)?; + let (mic_stream, _, _) = build_stream(&mic_dev, mic_tx, mic_volume)?; let out_tx = sender; thread::spawn(move || { @@ -352,7 +376,7 @@ impl AudioCapture { for _ in 0..avail { let s = sys_q.pop_front().unwrap_or(0.0); let m = mic_q.pop_front().unwrap_or(0.0); - mixed.push((s + m * 0.9).clamp(-1.0, 1.0)); + mixed.push(soft_limit(s + m)); } let _ = out_tx.try_send(mixed); } else if sys_q.len() > 4800 { @@ -377,7 +401,7 @@ impl AudioCapture { _ => { let dev = find_system_device().ok_or("No audio device available")?; let name = get_device_name(&dev); - let (stream, sr, ch) = build_stream(&dev, sender)?; + let (stream, sr, ch) = build_stream(&dev, sender, system_volume)?; println!("Audio capture mode: SYSTEM SOUNDS ONLY [{}] ({} Hz, {} ch)", name, sr, ch); Ok((Self { #[cfg(unix)] diff --git a/src/config.rs b/src/config.rs index 000b93c..3cf444e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,8 @@ fn default_save_hotkey() -> String { "Ctrl+Shift+R".to_string() } fn default_menu_hotkey() -> String { "Alt+Z".to_string() } fn default_record_hotkey() -> String { "Ctrl+Shift+F9".to_string() } fn default_cursor_hotkey() -> String { "Ctrl+Shift+F10".to_string() } +fn default_mic_volume() -> f32 { 0.60 } +fn default_system_volume() -> f32 { 1.00 } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct VrecConfig { @@ -42,6 +44,10 @@ pub struct VrecConfig { pub audio_device: String, #[serde(default = "default_audio_mode")] pub audio_mode: String, + #[serde(default = "default_mic_volume")] + pub mic_volume: f32, + #[serde(default = "default_system_volume")] + pub system_volume: f32, #[serde(default)] pub autostart: bool, @@ -74,6 +80,8 @@ impl Default for VrecConfig { show_cursor: true, audio_device: "default".to_string(), audio_mode: "system".to_string(), + mic_volume: default_mic_volume(), + system_volume: default_system_volume(), autostart: false, language: "en".to_string(), @@ -151,5 +159,7 @@ mod tests { assert_eq!(parsed.replay_duration_sec, 60); assert_eq!(parsed.audio_mode, "system"); assert!(parsed.show_cursor); + assert!((parsed.mic_volume - 0.60).abs() < 1e-4); + assert!((parsed.system_volume - 1.00).abs() < 1e-4); } } diff --git a/src/ipc.rs b/src/ipc.rs index a8a528f..5e77b24 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -25,12 +25,24 @@ pub struct DaemonStatus { pub audio_mode: String, #[serde(default = "default_true")] pub show_cursor: bool, + #[serde(default = "default_mic_volume")] + pub mic_volume: f32, + #[serde(default = "default_system_volume")] + pub system_volume: f32, } fn default_true() -> bool { true } +fn default_mic_volume() -> f32 { + 0.60 +} + +fn default_system_volume() -> f32 { + 1.00 +} + fn default_audio_mode_str() -> String { "system".to_string() } diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index ffd3a05..9f16cbf 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -8,6 +8,7 @@ use crate::ipc::{self, Command, DaemonStatus}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OverlayTab { Dashboard, + AudioMixer, Settings, } @@ -22,6 +23,9 @@ pub struct VrecOverlayApp { target_fps: u32, audio_mode_idx: usize, show_cursor: bool, + mic_volume_pct: u32, + system_volume_pct: u32, + anim_time: f32, status_msg: Option<(String, Instant)>, status_rx: Receiver, folder_tx: Sender, @@ -36,6 +40,8 @@ impl VrecOverlayApp { let target_fps = config.fps; let output_dir = config.output_directory.clone(); let show_cursor = config.show_cursor; + 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() { "mic" => 1, "both" => 2, @@ -50,7 +56,7 @@ impl VrecOverlayApp { if let Ok(s) = ipc::query_status() { let _ = status_tx.send(s); } - std::thread::sleep(Duration::from_millis(300)); + std::thread::sleep(Duration::from_millis(250)); } }); @@ -68,6 +74,9 @@ impl VrecOverlayApp { target_fps, audio_mode_idx, show_cursor, + mic_volume_pct, + system_volume_pct, + anim_time: 0.0, status_msg: None, status_rx, folder_tx, @@ -102,23 +111,75 @@ impl VrecOverlayApp { // Helper to render realistic mechanical keyboard keycap badges fn render_keycap(ui: &mut egui::Ui, text: &str) { egui::Frame::NONE - .fill(Color32::from_rgb(20, 25, 36)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(46, 54, 72))) - .corner_radius(CornerRadius::same(4_u8)) - .inner_margin(Margin::symmetric(7_i8, 3_i8)) + .fill(Color32::from_rgb(18, 24, 38)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(45, 58, 82))) + .corner_radius(CornerRadius::same(5_u8)) + .inner_margin(Margin::symmetric(8_i8, 3_i8)) .show(ui, |ui| { ui.label( egui::RichText::new(text) .font(FontId::monospace(10.0)) + .strong() .color(Color32::from_rgb(148, 163, 184)), ); }); } +// Helper pill button +fn pill(ui: &mut egui::Ui, text: &str, active: bool) -> bool { + let fill = if active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(22, 28, 40) }; + let stroke = if active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(42, 53, 75)) }; + let btn = egui::Button::new(egui::RichText::new(text).size(11.0).color(Color32::from_rgb(240, 246, 252))) + .fill(fill) + .stroke(stroke) + .corner_radius(CornerRadius::same(5_u8)); + ui.add(btn).clicked() +} + +// Render 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(), 16.0), egui::Sense::hover()); + let painter = ui.painter(); + + // Background track + painter.rect_filled(rect, CornerRadius::same(3_u8), Color32::from_rgb(13, 17, 24)); + painter.rect_stroke(rect, CornerRadius::same(3_u8), Stroke::new(1.0_f32, Color32::from_rgb(28, 36, 52)), egui::StrokeKind::Inside); + + if !active { + return; + } + + // Dynamic animated activity simulation based on gain level + 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; + + // Segmented bar styling + 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(34, 197, 94) // Green (-40dB to -12dB) + } else if pos_frac < 0.85 { + Color32::from_rgb(234, 179, 8) // Yellow (-12dB to -3dB) + } else { + Color32::from_rgb(239, 68, 68) // Red (Limit/Peak) + }; + painter.rect_filled(seg_rect, CornerRadius::same(2_u8), seg_color); + } +} + impl eframe::App for VrecOverlayApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.poll_async_events(); - ctx.request_repaint_after(Duration::from_millis(100)); + self.anim_time += 0.033; + ctx.request_repaint_after(Duration::from_millis(50)); // Handle Escape key to close if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { @@ -241,27 +302,39 @@ impl eframe::App for VrecOverlayApp { // Tab Switcher Pills let settings_active = self.current_tab == OverlayTab::Settings; - let settings_fill = if settings_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(24, 29, 41) }; - let settings_stroke = if settings_active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 75)) }; - let settings_btn = egui::Button::new(egui::RichText::new("Settings").size(12.0).color(Color32::WHITE)) + let settings_fill = if settings_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(20, 26, 38) }; + let settings_stroke = if settings_active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(40, 50, 72)) }; + let settings_btn = egui::Button::new(egui::RichText::new("Settings").size(11.5).color(Color32::WHITE)) .fill(settings_fill) .stroke(settings_stroke) .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([78.0, 26.0], settings_btn).clicked() { + if ui.add_sized([72.0, 26.0], settings_btn).clicked() { self.current_tab = OverlayTab::Settings; - ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(820.0, 490.0))); + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(840.0, 520.0))); + } + + let audio_active = self.current_tab == OverlayTab::AudioMixer; + let audio_fill = if audio_active { Color32::from_rgb(124, 58, 237) } else { Color32::from_rgb(20, 26, 38) }; + let audio_stroke = if audio_active { Stroke::new(1.0_f32, Color32::from_rgb(168, 85, 247)) } else { Stroke::new(1.0_f32, Color32::from_rgb(40, 50, 72)) }; + let audio_btn = egui::Button::new(egui::RichText::new("Audio Mixer").size(11.5).color(Color32::WHITE)) + .fill(audio_fill) + .stroke(audio_stroke) + .corner_radius(CornerRadius::same(6_u8)); + if ui.add_sized([86.0, 26.0], audio_btn).clicked() { + self.current_tab = OverlayTab::AudioMixer; + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(840.0, 480.0))); } let dash_active = self.current_tab == OverlayTab::Dashboard; - let dash_fill = if dash_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(24, 29, 41) }; - let dash_stroke = if dash_active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 75)) }; - let dash_btn = egui::Button::new(egui::RichText::new("Dashboard").size(12.0).color(Color32::WHITE)) + let dash_fill = if dash_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(20, 26, 38) }; + let dash_stroke = if dash_active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(40, 50, 72)) }; + let dash_btn = egui::Button::new(egui::RichText::new("Dashboard").size(11.5).color(Color32::WHITE)) .fill(dash_fill) .stroke(dash_stroke) .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([84.0, 26.0], dash_btn).clicked() { + if ui.add_sized([80.0, 26.0], dash_btn).clicked() { self.current_tab = OverlayTab::Dashboard; - ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(820.0, 280.0))); + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(840.0, 310.0))); } }); }); @@ -295,47 +368,47 @@ impl eframe::App for VrecOverlayApp { let replay_border = if self.status.is_replay_active { Stroke::new(1.0_f32, Color32::from_rgb(16, 185, 129)) } else { - Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58)) + Stroke::new(1.0_f32, Color32::from_rgb(32, 42, 60)) }; egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 29)) + .fill(Color32::from_rgb(15, 19, 28)) .stroke(replay_border) .corner_radius(CornerRadius::same(10_u8)) .inner_margin(Margin::same(14_i8)) .show(&mut cols[0], |ui| { - ui.set_min_height(140.0); + ui.set_min_height(160.0); ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("INSTANT REPLAY").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(148, 163, 184))); - let buf_label = format!("Rolling Buffer: {}s", self.config.replay_duration_sec); + ui.label(egui::RichText::new("INSTANT REPLAY").font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(148, 163, 184))); + let buf_label = format!("Buffer: {} seconds in RAM", self.config.replay_duration_sec); ui.label(egui::RichText::new(buf_label).font(FontId::proportional(11.0)).color(Color32::from_rgb(56, 189, 248))); - ui.add_space(12.0); - let save_btn = egui::Button::new(egui::RichText::new("Save Replay").strong().color(Color32::WHITE)) + ui.add_space(14.0); + let save_btn = egui::Button::new(egui::RichText::new("Save Instant Replay").strong().color(Color32::WHITE)) .fill(Color32::from_rgb(16, 185, 129)) .corner_radius(CornerRadius::same(7_u8)); - if ui.add_sized([160.0, 34.0], save_btn).clicked() { + if ui.add_sized([180.0, 36.0], save_btn).clicked() { let _ = ipc::send_command(Command::SaveReplay); self.set_msg("Replay save command sent"); } - ui.add_space(8.0); + ui.add_space(10.0); render_keycap(ui, &self.config.save_hotkey); }); }); - // Card 2: Screen Record + // Card 2: Screen Recording let rec_border = if self.status.is_recording { Stroke::new(1.0_f32, Color32::from_rgb(239, 68, 68)) } else { - Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58)) + Stroke::new(1.0_f32, Color32::from_rgb(32, 42, 60)) }; egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 29)) + .fill(Color32::from_rgb(15, 19, 28)) .stroke(rec_border) .corner_radius(CornerRadius::same(10_u8)) .inner_margin(Margin::same(14_i8)) .show(&mut cols[1], |ui| { - ui.set_min_height(140.0); + ui.set_min_height(160.0); ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("SCREEN RECORDING").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(148, 163, 184))); + ui.label(egui::RichText::new("SCREEN RECORDING").font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(148, 163, 184))); let timer_text = if self.status.is_recording { let d = self.status.recording_duration_sec; format!("Active: {:02}:{:02}:{:02}", d / 3600, (d % 3600) / 60, d % 60) @@ -344,68 +417,72 @@ impl eframe::App for VrecOverlayApp { }; let timer_color = if self.status.is_recording { Color32::from_rgb(248, 113, 113) } else { Color32::from_rgb(100, 116, 139) }; ui.label(egui::RichText::new(timer_text).font(FontId::proportional(11.0)).color(timer_color)); - ui.add_space(12.0); + ui.add_space(14.0); let (rec_label, rec_color) = if self.status.is_recording { ("Stop Recording", Color32::from_rgb(220, 38, 38)) } else { - ("Start Record", Color32::from_rgb(37, 99, 235)) + ("Start Recording", Color32::from_rgb(37, 99, 235)) }; let rec_btn = egui::Button::new(egui::RichText::new(rec_label).strong().color(Color32::WHITE)) .fill(rec_color) .corner_radius(CornerRadius::same(7_u8)); - if ui.add_sized([160.0, 34.0], rec_btn).clicked() { + if ui.add_sized([180.0, 36.0], rec_btn).clicked() { let _ = ipc::send_command(Command::ToggleRecording); self.set_msg("Recording toggled"); } - ui.add_space(8.0); + ui.add_space(10.0); render_keycap(ui, &self.config.record_hotkey); }); }); - // Card 3: Audio Route + // Card 3: Audio Deck egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 29)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58))) + .fill(Color32::from_rgb(15, 19, 28)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(32, 42, 60))) .corner_radius(CornerRadius::same(10_u8)) .inner_margin(Margin::same(14_i8)) .show(&mut cols[2], |ui| { - ui.set_min_height(140.0); + ui.set_min_height(160.0); ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("AUDIO ROUTE").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(148, 163, 184))); + ui.label(egui::RichText::new("AUDIO & CURSOR").font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(148, 163, 184))); let mode_name = match self.status.audio_mode.as_str() { - "mic" => "Microphone Only", - "both" => "System + Mic (Mixed)", - "muted" => "Muted", - _ => "System Audio Only", + "mic" => format!("Mic Only ({}%)", self.mic_volume_pct), + "both" => format!("System + Mic ({}%)", self.mic_volume_pct), + "muted" => "Muted".to_string(), + _ => format!("System Audio ({}%)", self.system_volume_pct), }; ui.label(egui::RichText::new(mode_name).font(FontId::proportional(11.0)).color(Color32::from_rgb(168, 85, 247))); - ui.add_space(12.0); + ui.add_space(14.0); let audio_btn = egui::Button::new(egui::RichText::new("Cycle Audio Mode").strong().color(Color32::from_rgb(226, 232, 240))) - .fill(Color32::from_rgb(30, 41, 59)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(51, 65, 85))) + .fill(Color32::from_rgb(28, 36, 52)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(46, 60, 86))) .corner_radius(CornerRadius::same(7_u8)); - if ui.add_sized([160.0, 34.0], audio_btn).clicked() { + if ui.add_sized([180.0, 36.0], audio_btn).clicked() { let _ = ipc::send_command(Command::CycleAudioMode); self.set_msg("Audio mode cycled"); } - ui.add_space(8.0); - render_keycap(ui, "Cycle"); + ui.add_space(10.0); + ui.horizontal(|ui| { + render_keycap(ui, "Cycle"); + ui.add_space(2.0); + render_keycap(ui, &self.config.cursor_hotkey); + }); }); }); }); - ui.add_space(10.0); + ui.add_space(8.0); // Bottom info ribbon egui::Frame::NONE - .fill(Color32::from_rgb(14, 17, 24)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(28, 34, 48))) + .fill(Color32::from_rgb(13, 16, 24)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(26, 32, 46))) .corner_radius(CornerRadius::same(6_u8)) .inner_margin(Margin::symmetric(12_i8, 6_i8)) .show(ui, |ui| { ui.horizontal(|ui| { ui.label( - egui::RichText::new(format!("Hardware Engine: VAAPI / NV12 Zero-Copy • {} FPS • {} Mbps", self.config.fps, self.bitrate_mbps)) + egui::RichText::new(format!("Hardware Engine: VAAPI NV12 Direct • {} FPS • {} Mbps", self.config.fps, self.bitrate_mbps)) .font(FontId::monospace(10.0)) .color(Color32::from_rgb(100, 116, 139)), ); @@ -420,23 +497,192 @@ impl eframe::App for VrecOverlayApp { }); } - // VIEW: SETTINGS - if self.current_tab == OverlayTab::Settings { - let pill = |ui: &mut egui::Ui, text: &str, active: bool| -> bool { - let fill = if active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(24, 30, 42) }; - let stroke = if active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 75)) }; - let btn = egui::Button::new(egui::RichText::new(text).size(11.0).color(Color32::from_rgb(240, 246, 252))) - .fill(fill) - .stroke(stroke) - .corner_radius(CornerRadius::same(5_u8)); - ui.add(btn).clicked() - }; + // TAB 2: AUDIO MIXER & LEVELS (Dedicated Studio Audio Tab) + if self.current_tab == OverlayTab::AudioMixer { + egui::ScrollArea::vertical().max_height(380.0).show(ui, |ui| { + // Master Channel Strips + ui.columns(2, |ch_cols| { + // Strip 1: Microphone Input + egui::Frame::NONE + .fill(Color32::from_rgb(15, 19, 28)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64))) + .corner_radius(CornerRadius::same(10_u8)) + .inner_margin(Margin::same(14_i8)) + .show(&mut ch_cols[0], |ui| { + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("MICROPHONE INPUT") + .font(FontId::proportional(13.0)) + .strong() + .color(Color32::from_rgb(56, 189, 248)), + ); + 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(13.0)) + .strong() + .color(Color32::from_rgb(56, 189, 248)), + ); + }); + }); - egui::ScrollArea::vertical().max_height(350.0).show(ui, |ui| { - // Section 1: Destination Directory (Addressed explicitly per user request) + ui.add_space(4.0); + ui.label( + egui::RichText::new("PulseAudio/PipeWire Native Source (@DEFAULT_SOURCE@)") + .font(FontId::monospace(9.0)) + .color(Color32::from_rgb(100, 116, 139)), + ); + + ui.add_space(8.0); + ui.add(egui::Slider::new(&mut self.mic_volume_pct, 0..=150).text("Gain")); + + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Presets:").font(FontId::proportional(10.0)).color(Color32::from_rgb(100, 116, 139))); + if pill(ui, "30% Low", self.mic_volume_pct == 30) { self.mic_volume_pct = 30; } + if pill(ui, "60% Optimal", self.mic_volume_pct == 60) { self.mic_volume_pct = 60; } + if pill(ui, "80% Loud", self.mic_volume_pct == 80) { self.mic_volume_pct = 80; } + if pill(ui, "100% Max", self.mic_volume_pct == 100) { self.mic_volume_pct = 100; } + }); + + ui.add_space(10.0); + ui.label(egui::RichText::new("Live Input Peak Meter").font(FontId::proportional(10.5)).color(Color32::from_rgb(148, 163, 184))); + let is_mic_active = self.status.audio_mode == "mic" || self.status.audio_mode == "both"; + render_vu_meter(ui, self.mic_volume_pct as f32, is_mic_active, self.anim_time); + + ui.add_space(8.0); + egui::Frame::NONE + .fill(Color32::from_rgba_unmultiplied(16, 185, 129, 20)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(16, 185, 129))) + .corner_radius(CornerRadius::same(5_u8)) + .inner_margin(Margin::symmetric(8_i8, 4_i8)) + .show(ui, |ui| { + ui.label( + egui::RichText::new("Anti-Clipping Soft-Knee Limiter Active") + .font(FontId::monospace(9.5)) + .color(Color32::from_rgb(52, 211, 153)), + ); + }); + }); + + // Strip 2: System Audio Output + egui::Frame::NONE + .fill(Color32::from_rgb(15, 19, 28)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64))) + .corner_radius(CornerRadius::same(10_u8)) + .inner_margin(Margin::same(14_i8)) + .show(&mut ch_cols[1], |ui| { + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("SYSTEM AUDIO MONITOR") + .font(FontId::proportional(13.0)) + .strong() + .color(Color32::from_rgb(168, 85, 247)), + ); + 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(13.0)) + .strong() + .color(Color32::from_rgb(168, 85, 247)), + ); + }); + }); + + ui.add_space(4.0); + ui.label( + egui::RichText::new("PipeWire Audio Monitor Sink (@DEFAULT_MONITOR@)") + .font(FontId::monospace(9.0)) + .color(Color32::from_rgb(100, 116, 139)), + ); + + ui.add_space(8.0); + ui.add(egui::Slider::new(&mut self.system_volume_pct, 0..=150).text("Level")); + + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Presets:").font(FontId::proportional(10.0)).color(Color32::from_rgb(100, 116, 139))); + if pill(ui, "50% Half", self.system_volume_pct == 50) { self.system_volume_pct = 50; } + if pill(ui, "75% Soft", self.system_volume_pct == 75) { self.system_volume_pct = 75; } + if pill(ui, "100% Full", self.system_volume_pct == 100) { self.system_volume_pct = 100; } + }); + + ui.add_space(10.0); + ui.label(egui::RichText::new("Live System Output Level").font(FontId::proportional(10.5)).color(Color32::from_rgb(148, 163, 184))); + let is_sys_active = self.status.audio_mode != "muted" && self.status.audio_mode != "mic"; + render_vu_meter(ui, self.system_volume_pct as f32, is_sys_active, self.anim_time * 0.85); + + ui.add_space(8.0); + egui::Frame::NONE + .fill(Color32::from_rgba_unmultiplied(124, 58, 237, 20)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(139, 92, 246))) + .corner_radius(CornerRadius::same(5_u8)) + .inner_margin(Margin::symmetric(8_i8, 4_i8)) + .show(ui, |ui| { + ui.label( + egui::RichText::new("Direct PulseAudio Hardware Interceptor") + .font(FontId::monospace(9.5)) + .color(Color32::from_rgb(196, 181, 253)), + ); + }); + }); + }); + + ui.add_space(10.0); + + // Section 2: Audio Routing Selection egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 29)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58))) + .fill(Color32::from_rgb(15, 19, 28)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64))) + .corner_radius(CornerRadius::same(10_u8)) + .inner_margin(Margin::same(12_i8)) + .show(ui, |ui| { + ui.label( + egui::RichText::new("STREAM AUDIO ROUTING") + .font(FontId::proportional(12.0)) + .strong() + .color(Color32::from_rgb(203, 213, 225)), + ); + ui.add_space(6.0); + ui.horizontal(|ui| { + if pill(ui, "System Audio Only", 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, "Mixed (System + Mic)", self.audio_mode_idx == 2) { self.audio_mode_idx = 2; } + if pill(ui, "Muted (Silent Video)", self.audio_mode_idx == 3) { self.audio_mode_idx = 3; } + }); + }); + + ui.add_space(10.0); + + // Quick Apply Button for Audio + ui.horizontal(|ui| { + let apply_audio_btn = egui::Button::new(egui::RichText::new("Apply Audio Mixer Settings").strong().color(Color32::WHITE)) + .fill(Color32::from_rgb(124, 58, 237)) + .corner_radius(CornerRadius::same(6_u8)); + if ui.add_sized([220.0, 32.0], apply_audio_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 mixer levels saved and reloaded!"); + } + }); + }); + } + + // TAB 3: SETTINGS + if self.current_tab == OverlayTab::Settings { + egui::ScrollArea::vertical().max_height(410.0).show(ui, |ui| { + // Section 1: Save Destination Directory + egui::Frame::NONE + .fill(Color32::from_rgb(15, 19, 28)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64))) .corner_radius(CornerRadius::same(8_u8)) .inner_margin(Margin::same(12_i8)) .show(ui, |ui| { @@ -449,9 +695,8 @@ impl eframe::App for VrecOverlayApp { ui.add_space(6.0); ui.horizontal(|ui| { ui.label(egui::RichText::new("Folder Path:").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225))); - ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(380.0)); + ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(420.0)); - // Browse button: opens native folder dialog asynchronously let browse_btn = egui::Button::new(egui::RichText::new("Browse...").color(Color32::WHITE)) .fill(Color32::from_rgb(37, 99, 235)) .corner_radius(CornerRadius::same(5_u8)); @@ -496,24 +741,22 @@ impl eframe::App for VrecOverlayApp { ui.horizontal(|ui| { ui.label(egui::RichText::new("Presets:").font(FontId::proportional(10.0)).color(Color32::from_rgb(100, 116, 139))); let v_dir = format!("{}/Videos/vrec", std::env::var("HOME").unwrap_or_default()); + let c_dir = format!("{}/Videos/Captures", std::env::var("HOME").unwrap_or_default()); let d_dir = format!("{}/Downloads/vrec", std::env::var("HOME").unwrap_or_default()); - if pill(ui, "~/Videos/vrec", self.output_dir == v_dir) { - self.output_dir = v_dir; - } - if pill(ui, "~/Downloads/vrec", self.output_dir == d_dir) { - self.output_dir = d_dir; - } + 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 pill(ui, "~/Downloads/vrec", self.output_dir == d_dir) { self.output_dir = d_dir; } }); }); ui.add_space(8.0); - // Section 2: Split 2 columns for Video & Audio Tuning + // Section 2: Split Columns for Video and Replay ui.columns(2, |settings_cols| { - // Column 1: Video & Encoding + // Column 1: Video Quality & Cursor egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 29)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58))) + .fill(Color32::from_rgb(15, 19, 28)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64))) .corner_radius(CornerRadius::same(8_u8)) .inner_margin(Margin::same(12_i8)) .show(&mut settings_cols[0], |ui| { @@ -560,15 +803,15 @@ impl eframe::App for VrecOverlayApp { ui.label(egui::RichText::new("Codec: Hardware VAAPI H.264 (NV12 Direct)").font(FontId::monospace(9.0)).color(Color32::from_rgb(100, 116, 139))); }); - // Column 2: Replay & Audio Routing + // Column 2: Replay Buffer & Hotkeys egui::Frame::NONE - .fill(Color32::from_rgb(16, 20, 29)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58))) + .fill(Color32::from_rgb(15, 19, 28)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64))) .corner_radius(CornerRadius::same(8_u8)) .inner_margin(Margin::same(12_i8)) .show(&mut settings_cols[1], |ui| { ui.label( - egui::RichText::new("REPLAY & AUDIO") + egui::RichText::new("REPLAY BUFFER & HOTKEYS") .font(FontId::proportional(12.0)) .strong() .color(Color32::from_rgb(168, 85, 247)), @@ -576,7 +819,7 @@ impl eframe::App for VrecOverlayApp { ui.add_space(8.0); // Replay Duration - ui.label(egui::RichText::new("Replay Buffer Duration").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225))); + ui.label(egui::RichText::new("Buffer Duration").font(FontId::proportional(11.0)).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; } @@ -585,26 +828,23 @@ impl eframe::App for VrecOverlayApp { if pill(ui, "300s", self.replay_sec == 300) { self.replay_sec = 300; } }); - ui.add_space(10.0); - - // Audio Mode - ui.label(egui::RichText::new("Audio Source Routing").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225))); - egui::ComboBox::from_id_salt("audio_routing_cb") - .selected_text(match self.audio_mode_idx { - 1 => "Microphone Only", - 2 => "Both (System + Mic)", - 3 => "Muted", - _ => "System Audio Only", - }) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.audio_mode_idx, 0, "System Audio Only"); - ui.selectable_value(&mut self.audio_mode_idx, 1, "Microphone Only"); - ui.selectable_value(&mut self.audio_mode_idx, 2, "Both (System + Mic)"); - ui.selectable_value(&mut self.audio_mode_idx, 3, "Muted"); - }); - - ui.add_space(8.0); - ui.label(egui::RichText::new("PulseAudio/PipeWire Native Capture (48kHz)").font(FontId::monospace(9.0)).color(Color32::from_rgb(100, 116, 139))); + ui.add_space(12.0); + ui.label(egui::RichText::new("Configured Global Hotkeys").font(FontId::proportional(11.0)).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); + }); }); }); @@ -615,7 +855,7 @@ impl eframe::App for VrecOverlayApp { let apply_btn = egui::Button::new(egui::RichText::new("Save & Apply All Settings").strong().color(Color32::WHITE)) .fill(Color32::from_rgb(37, 99, 235)) .corner_radius(CornerRadius::same(6_u8)); - if ui.add_sized([220.0, 34.0], apply_btn).clicked() { + if ui.add_sized([230.0, 34.0], apply_btn).clicked() { self.config.output_directory = self.output_dir.clone(); let _ = std::fs::create_dir_all(&self.output_dir); self.config.replay_duration_sec = self.replay_sec; @@ -623,6 +863,8 @@ impl eframe::App for VrecOverlayApp { 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", @@ -653,9 +895,9 @@ pub fn run_egui_overlay() { viewport: egui::ViewportBuilder::default() .with_title("vrec") .with_app_id("vrec-overlay") - .with_inner_size([820.0, 280.0]) - .with_min_inner_size([720.0, 240.0]) - .with_max_inner_size([920.0, 560.0]) + .with_inner_size([840.0, 310.0]) + .with_min_inner_size([720.0, 260.0]) + .with_max_inner_size([960.0, 600.0]) .with_resizable(true) .with_decorations(false) .with_transparent(true)