feat(ui): frosted glass styling, centered icons, 50px top margin, removed X button, and modernized settings GUI

This commit is contained in:
katR 2026-09-05 13:11:04 +05:30
parent e4655cde0b
commit 2bf1e2f96e
2 changed files with 378 additions and 251 deletions

View file

@ -62,6 +62,41 @@ pub fn hotkey_to_hyprland(hotkey: &str) -> Option<(String, String)> {
Some((mod_str, key.to_string()))
}
pub fn hotkey_to_hyprland_lua(hotkey: &str) -> Option<String> {
let parts: Vec<&str> = hotkey.split('+').map(|s| s.trim()).collect();
if parts.is_empty() {
return None;
}
let mut mods = Vec::new();
let mut key = "";
for (i, &part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
key = part;
} else {
let m = match part.to_lowercase().as_str() {
"ctrl" | "control" => "CONTROL",
"shift" => "SHIFT",
"alt" => "ALT",
"super" | "win" | "meta" => "SUPER",
_ => continue,
};
mods.push(m);
}
}
if key.is_empty() {
return None;
}
if mods.is_empty() {
Some(key.to_string())
} else {
Some(format!("{} + {}", mods.join(" + "), key))
}
}
#[cfg(unix)]
/// Dynamically inject binds and window rules into running Hyprland without touching hyprland.conf
pub fn register_hyprland_binds(config: &VrecConfig) {
@ -69,8 +104,19 @@ pub fn register_hyprland_binds(config: &VrecConfig) {
return;
}
// Register overlay window rules so the UI floats at the top cleanly
let rules = [
// Register modern Hyprland (0.55+) layer rules for genuine frosted glass blur & floating window rules
let _ = Command::new("hyprctl")
.args(["eval", r#"hl.layer_rule({ match = { namespace = "vrec-overlay" }, blur = true, ignore_alpha = 0.1 })"#])
.output();
let _ = Command::new("hyprctl")
.args(["eval", r#"hl.layer_rule({ match = { namespace = "vrec-notification" }, blur = true, ignore_alpha = 0.1 })"#])
.output();
let _ = Command::new("hyprctl")
.args(["eval", r#"hl.window_rule({ match = { class = "vrec-overlay" }, float = true, pin = true, stay_focused = true })"#])
.output();
// Also register legacy window rules for backward compatibility with older Hyprland versions
let legacy_rules = [
"float, class:^(vrec-overlay)$",
"move 50% 45, class:^(vrec-overlay)$",
"pin, class:^(vrec-overlay)$",
@ -82,35 +128,42 @@ pub fn register_hyprland_binds(config: &VrecConfig) {
"stayfocused, class:^(vrec-hud)$",
"noborder, class:^(vrec-hud)$",
];
for rule in rules {
for rule in legacy_rules {
let _ = Command::new("hyprctl")
.args(["keyword", "windowrulev2", rule])
.output();
}
let binds = [
(&config.menu_hotkey, "exec, vrec-ui --menu"),
(&config.save_hotkey, "exec, vrec-ui --save"),
(&config.record_hotkey, "exec, vrec-ui --record"),
(&config.cursor_hotkey, "exec, vrec-ui --cursor"),
(&config.menu_hotkey, "vrec-ui --menu"),
(&config.save_hotkey, "vrec-ui --save"),
(&config.record_hotkey, "vrec-ui --record"),
(&config.cursor_hotkey, "vrec-ui --cursor"),
];
for (hotkey, action) in binds {
for (hotkey, cmd) in binds {
// Try modern Hyprland Lua eval first
if let Some(lua_combo) = hotkey_to_hyprland_lua(hotkey) {
let lua_script = format!(r#"hl.bind("{}", hl.dsp.exec_cmd("{}"))"#, lua_combo, cmd);
if let Ok(out) = Command::new("hyprctl").args(["eval", &lua_script]).output()
&& out.status.success() && String::from_utf8_lossy(&out.stdout).contains("ok") {
println!("Hyprland dynamic bind active (Lua): {}", lua_combo);
continue;
}
}
// Fallback to legacy keyword bind
if let Some((mods, key)) = hotkey_to_hyprland(hotkey) {
let bind_arg = if mods.is_empty() {
format!("{}, {}", key, action)
format!("{}, exec, {}", key, cmd)
} else {
format!("{}, {}, {}", mods, key, action)
format!("{}, {}, exec, {}", mods, key, cmd)
};
let res = Command::new("hyprctl")
.args(["keyword", "bind", &bind_arg])
.output();
if let Ok(out) = res {
if !out.status.success() {
eprintln!("hyprctl keyword bind note: {:?}", String::from_utf8_lossy(&out.stderr));
} else {
println!("Hyprland dynamic bind active: {}", bind_arg);
}
if let Ok(out) = res && out.status.success() {
println!("Hyprland dynamic bind active (legacy): {}", bind_arg);
}
}
}
@ -134,6 +187,12 @@ pub fn unregister_hyprland_binds(config: &VrecConfig) {
];
for hotkey in hotkeys {
if let Some(lua_combo) = hotkey_to_hyprland_lua(hotkey) {
let lua_script = format!(r#"hl.unbind("{}")"#, lua_combo);
let _ = Command::new("hyprctl")
.args(["eval", &lua_script])
.output();
}
if let Some((mods, key)) = hotkey_to_hyprland(hotkey) {
let unbind_arg = if mods.is_empty() {
key
@ -222,5 +281,25 @@ mod tests {
Some(("SUPER_ALT".to_string(), "S".to_string()))
);
}
#[test]
fn test_hotkey_to_hyprland_lua_conversion() {
assert_eq!(
hotkey_to_hyprland_lua("Ctrl+Shift+R"),
Some("CONTROL + SHIFT + R".to_string())
);
assert_eq!(
hotkey_to_hyprland_lua("Alt+Z"),
Some("ALT + Z".to_string())
);
assert_eq!(
hotkey_to_hyprland_lua("F12"),
Some("F12".to_string())
);
assert_eq!(
hotkey_to_hyprland_lua("Super+Alt+S"),
Some("SUPER + ALT + S".to_string())
);
}
}

View file

@ -1,6 +1,6 @@
use gtk::prelude::*;
use gtk::{
Application, ApplicationWindow, Box, Button, CheckButton, ComboBoxText, CssProvider,
Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider,
DrawingArea, Entry, Label, Orientation, Revealer, RevealerTransitionType, Scale,
ScrolledWindow, SpinButton, Stack, StackTransitionType, StyleContext, Switch,
};
@ -67,7 +67,7 @@ pub fn show_notification(message: &str) {
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_namespace("vrec-notification");
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 45);
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 40);
window.set_anchor(gtk_layer_shell::Edge::Top, true);
}
@ -90,19 +90,19 @@ pub fn show_notification(message: &str) {
background: transparent !important;
}
.notify-box {
background-color: rgba(14, 18, 26, 0.94);
border-radius: 10px;
border: 1px solid #76b900;
box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8);
padding: 8px 20px;
background-color: rgba(18, 24, 36, 0.78);
border-radius: 12px;
border: 1px solid rgba(118, 185, 0, 0.6);
box-shadow: 0px 16px 40px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2);
padding: 8px 22px;
}
.notify-badge {
background-color: #76b900;
color: #0b1204;
font-size: 10px;
font-weight: 800;
font-size: 10.5px;
font-weight: 900;
border-radius: 4px;
padding: 2px 6px;
padding: 2px 7px;
}
.notify-label {
color: #ffffff;
@ -144,88 +144,90 @@ pub fn show_notification(message: &str) {
}
}
// Vector Icon Drawing Helpers (NVIDIA ShadowPlay & GPU Screen Recorder Style)
fn draw_replay_icon(cr: &gtk::cairo::Context, is_active: bool) {
let cx = 28.0;
let cy = 28.0;
let r = 16.0;
// Vector Icon Drawing Helpers (Centered & Antialiased)
fn draw_replay_icon(cr: &gtk::cairo::Context, width: f64, height: f64, is_active: bool) {
let cx = width / 2.0;
let cy = height / 2.0;
let r = 24.0;
if is_active {
cr.set_source_rgb(0.463, 0.725, 0.0); // #76b900 NVIDIA green
} else {
cr.set_source_rgb(0.58, 0.64, 0.72); // #94a3b8
cr.set_source_rgb(0.60, 0.68, 0.78); // #94a3b8
}
cr.set_line_width(3.2);
cr.set_line_width(3.8);
cr.arc(cx, cy, r, 0.25 * PI, 1.80 * PI);
let _ = cr.stroke();
// Arrowhead at top-left
// Arrowhead at start of arc
let a_x = cx + r * (0.25 * PI).cos();
let a_y = cy + r * (0.25 * PI).sin();
cr.move_to(a_x, a_y);
cr.line_to(a_x - 7.0, a_y);
cr.line_to(a_x, a_y - 7.0);
cr.line_to(a_x - 9.0, a_y + 1.0);
cr.line_to(a_x - 1.0, a_y - 9.0);
cr.close_path();
let _ = cr.fill();
// Play triangle in center
let tri_r = 6.0;
cr.move_to(cx + tri_r, cy);
cr.line_to(cx - tri_r * 0.6, cy - tri_r * 0.86);
cr.line_to(cx - tri_r * 0.6, cy + tri_r * 0.86);
// Centered play triangle
let tri_r = 8.5;
let tri_cx = cx + 1.0;
let tri_cy = cy;
cr.move_to(tri_cx + tri_r, tri_cy);
cr.line_to(tri_cx - tri_r * 0.6, tri_cy - tri_r * 0.86);
cr.line_to(tri_cx - tri_r * 0.6, tri_cy + tri_r * 0.86);
cr.close_path();
let _ = cr.fill();
}
fn draw_record_icon(cr: &gtk::cairo::Context, is_recording: bool) {
let cx = 28.0;
let cy = 28.0;
fn draw_record_icon(cr: &gtk::cairo::Context, width: f64, height: f64, is_recording: bool) {
let cx = width / 2.0;
let cy = height / 2.0;
if is_recording {
// Glowing red recording indicator
// Glowing red recording indicator with halo
cr.set_source_rgba(0.937, 0.267, 0.267, 0.25);
cr.arc(cx, cy, 22.0, 0.0, PI * 2.0);
cr.arc(cx, cy, 32.0, 0.0, PI * 2.0);
let _ = cr.fill();
cr.set_source_rgb(0.937, 0.267, 0.267); // #ef4444
cr.set_line_width(2.8);
cr.arc(cx, cy, 18.0, 0.0, PI * 2.0);
cr.set_line_width(3.5);
cr.arc(cx, cy, 26.0, 0.0, PI * 2.0);
let _ = cr.stroke();
cr.set_source_rgb(0.937, 0.267, 0.267);
cr.arc(cx, cy, 9.0, 0.0, PI * 2.0);
cr.arc(cx, cy, 13.0, 0.0, PI * 2.0);
let _ = cr.fill();
} else {
cr.set_source_rgb(0.58, 0.64, 0.72); // #94a3b8
cr.set_line_width(2.5);
cr.arc(cx, cy, 17.0, 0.0, PI * 2.0);
cr.set_source_rgb(0.60, 0.68, 0.78); // #94a3b8
cr.set_line_width(3.2);
cr.arc(cx, cy, 25.0, 0.0, PI * 2.0);
let _ = cr.stroke();
cr.set_source_rgb(0.88, 0.91, 0.94);
cr.arc(cx, cy, 8.0, 0.0, PI * 2.0);
cr.arc(cx, cy, 12.0, 0.0, PI * 2.0);
let _ = cr.fill();
}
}
fn draw_gear_icon(cr: &gtk::cairo::Context) {
let cx = 28.0;
let cy = 28.0;
let r = 16.0;
fn draw_gear_icon(cr: &gtk::cairo::Context, width: f64, height: f64) {
let cx = width / 2.0;
let cy = height / 2.0;
let r = 24.0;
cr.set_source_rgb(0.58, 0.64, 0.72); // #94a3b8
cr.set_line_width(2.2);
cr.set_source_rgb(0.60, 0.68, 0.78); // #94a3b8
cr.set_line_width(3.0);
cr.arc(cx, cy, r * 0.75, 0.0, PI * 2.0);
let _ = cr.stroke();
cr.arc(cx, cy, r * 0.30, 0.0, PI * 2.0);
cr.arc(cx, cy, r * 0.32, 0.0, PI * 2.0);
let _ = cr.stroke();
for i in 0..8 {
let angle = i as f64 * (PI / 4.0);
let p_in_x = cx + angle.cos() * (r * 0.68);
let p_in_y = cy + angle.sin() * (r * 0.68);
let p_out_x = cx + angle.cos() * (r * 1.05);
let p_out_y = cy + angle.sin() * (r * 1.05);
cr.set_line_width(3.0);
let p_out_x = cx + angle.cos() * (r * 1.08);
let p_out_y = cy + angle.sin() * (r * 1.08);
cr.set_line_width(4.2);
cr.move_to(p_in_x, p_in_y);
cr.line_to(p_out_x, p_out_y);
let _ = cr.stroke();
@ -262,8 +264,8 @@ pub fn show_menu_overlay() {
window.set_layer(Layer::Overlay);
window.set_namespace("vrec-overlay");
window.set_keyboard_interactivity(true);
// Position at upper ~20-25% of the screen, just like GPU Screen Recorder UI
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 110);
// Shifted higher towards the top (50px margin) per user feedback
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 50);
window.set_anchor(gtk_layer_shell::Edge::Top, true);
}
@ -294,13 +296,13 @@ pub fn show_menu_overlay() {
stack.set_homogeneous(false);
// =========================================================================
// PAGE 1: 3 SQUARE CARDS WITH DROPDOWN MENUS (NVIDIA ShadowPlay Exact Match)
// PAGE 1: 3 FROSTED GLASS CARDS WITH CONTEXTUAL DROPDOWNS
// =========================================================================
let hud_page = Box::new(Orientation::Vertical, 8);
let hud_page = Box::new(Orientation::Vertical, 10);
hud_page.style_context().add_class("hud-wrapper");
hud_page.set_size_request(720, -1);
hud_page.set_size_request(740, -1);
// Top Bar (Floating frosted strip)
// Top Bar (Frosted glass header strip, no "X" button)
let header_box = Box::new(Orientation::Horizontal, 10);
header_box.style_context().add_class("header-bar");
@ -315,43 +317,50 @@ pub fn show_menu_overlay() {
let spacer = Box::new(Orientation::Horizontal, 0);
let close_btn = Button::with_label("✕");
close_btn.style_context().add_class("close-btn");
let esc_hint = Label::new(Some("Esc to Close"));
esc_hint.style_context().add_class("esc-pill");
header_box.pack_start(&brand_badge, false, false, 0);
header_box.pack_start(&title_label, false, false, 4);
header_box.pack_start(&status_banner, false, false, 12);
header_box.pack_start(&spacer, true, true, 0);
header_box.pack_start(&close_btn, false, false, 0);
header_box.pack_start(&esc_hint, false, false, 0);
// Row of 3 Cards
let cards_box = Box::new(Orientation::Horizontal, 14);
let cards_box = Box::new(Orientation::Horizontal, 16);
cards_box.style_context().add_class("cards-container");
cards_box.set_halign(gtk::Align::Center);
// State Trackers for Dynamic Drawing
// State Trackers for Vector Redraws
let replay_active_state = Arc::new(AtomicBool::new(config.replay_enabled));
let record_active_state = Arc::new(AtomicBool::new(false));
// -------------------------------------------------------------------------
// CARD 1: INSTANT REPLAY (With Attached Dropdown Menu)
// CARD 1: INSTANT REPLAY
// -------------------------------------------------------------------------
let replay_col = Box::new(Orientation::Vertical, 0);
replay_col.set_size_request(215, -1);
replay_col.set_size_request(225, -1);
let replay_card_btn = Button::new();
replay_card_btn.style_context().add_class("card-btn");
replay_card_btn.set_size_request(215, 180);
replay_card_btn.set_size_request(225, 195);
let replay_card_inner = Box::new(Orientation::Vertical, 6);
replay_card_inner.set_valign(gtk::Align::Center);
replay_card_inner.set_halign(gtk::Align::Center);
let replay_title_lbl = Label::new(Some("INSTANT REPLAY"));
replay_title_lbl.style_context().add_class("card-title");
let replay_icon_area = DrawingArea::new();
replay_icon_area.set_size_request(56, 56);
replay_icon_area.set_size_request(76, 76);
replay_icon_area.set_valign(gtk::Align::Center);
replay_icon_area.set_halign(gtk::Align::Center);
let r_state_clone = Arc::clone(&replay_active_state);
replay_icon_area.connect_draw(move |_, cr| {
draw_replay_icon(cr, r_state_clone.load(Ordering::Relaxed));
replay_icon_area.connect_draw(move |w, cr| {
let width = w.allocated_width() as f64;
let height = w.allocated_height() as f64;
draw_replay_icon(cr, width, height, r_state_clone.load(Ordering::Relaxed));
gtk::glib::Propagation::Proceed
});
@ -365,7 +374,7 @@ pub fn show_menu_overlay() {
replay_sub_lbl.style_context().add_class("card-sub");
replay_card_inner.pack_start(&replay_title_lbl, false, false, 0);
replay_card_inner.pack_start(&replay_icon_area, true, true, 4);
replay_card_inner.pack_start(&replay_icon_area, false, false, 6);
replay_card_inner.pack_start(&replay_status_lbl, false, false, 0);
replay_card_inner.pack_start(&replay_sub_lbl, false, false, 2);
replay_card_btn.add(&replay_card_inner);
@ -375,7 +384,7 @@ pub fn show_menu_overlay() {
replay_revealer.set_transition_type(RevealerTransitionType::SlideDown);
replay_revealer.set_transition_duration(150);
let replay_menu_box = Box::new(Orientation::Vertical, 2);
let replay_menu_box = Box::new(Orientation::Vertical, 3);
replay_menu_box.style_context().add_class("dropdown-menu");
let replay_toggle_item = Button::with_label(if config.replay_enabled { "Turn off" } else { "Turn on" });
@ -396,24 +405,31 @@ pub fn show_menu_overlay() {
replay_col.pack_start(&replay_revealer, false, false, 0);
// -------------------------------------------------------------------------
// CARD 2: RECORD (With Attached Dropdown Menu)
// CARD 2: RECORD
// -------------------------------------------------------------------------
let record_col = Box::new(Orientation::Vertical, 0);
record_col.set_size_request(215, -1);
record_col.set_size_request(225, -1);
let record_card_btn = Button::new();
record_card_btn.style_context().add_class("card-btn");
record_card_btn.set_size_request(215, 180);
record_card_btn.set_size_request(225, 195);
let record_card_inner = Box::new(Orientation::Vertical, 6);
record_card_inner.set_valign(gtk::Align::Center);
record_card_inner.set_halign(gtk::Align::Center);
let record_title_lbl = Label::new(Some("RECORD"));
record_title_lbl.style_context().add_class("card-title");
let record_icon_area = DrawingArea::new();
record_icon_area.set_size_request(56, 56);
record_icon_area.set_size_request(76, 76);
record_icon_area.set_valign(gtk::Align::Center);
record_icon_area.set_halign(gtk::Align::Center);
let rec_state_clone = Arc::clone(&record_active_state);
record_icon_area.connect_draw(move |_, cr| {
draw_record_icon(cr, rec_state_clone.load(Ordering::Relaxed));
record_icon_area.connect_draw(move |w, cr| {
let width = w.allocated_width() as f64;
let height = w.allocated_height() as f64;
draw_record_icon(cr, width, height, rec_state_clone.load(Ordering::Relaxed));
gtk::glib::Propagation::Proceed
});
@ -424,7 +440,7 @@ pub fn show_menu_overlay() {
record_sub_lbl.style_context().add_class("card-sub");
record_card_inner.pack_start(&record_title_lbl, false, false, 0);
record_card_inner.pack_start(&record_icon_area, true, true, 4);
record_card_inner.pack_start(&record_icon_area, false, false, 6);
record_card_inner.pack_start(&record_status_lbl, false, false, 0);
record_card_inner.pack_start(&record_sub_lbl, false, false, 2);
record_card_btn.add(&record_card_inner);
@ -434,7 +450,7 @@ pub fn show_menu_overlay() {
record_revealer.set_transition_type(RevealerTransitionType::SlideDown);
record_revealer.set_transition_duration(150);
let record_menu_box = Box::new(Orientation::Vertical, 2);
let record_menu_box = Box::new(Orientation::Vertical, 3);
record_menu_box.style_context().add_class("dropdown-menu");
let record_toggle_item = Button::with_label("Start (Ctrl+Shift+F9)");
@ -451,23 +467,30 @@ pub fn show_menu_overlay() {
record_col.pack_start(&record_revealer, false, false, 0);
// -------------------------------------------------------------------------
// CARD 3: SETTINGS (Direct Click to Settings Page)
// CARD 3: SETTINGS
// -------------------------------------------------------------------------
let settings_col = Box::new(Orientation::Vertical, 0);
settings_col.set_size_request(215, -1);
settings_col.set_size_request(225, -1);
let settings_card_btn = Button::new();
settings_card_btn.style_context().add_class("card-btn");
settings_card_btn.set_size_request(215, 180);
settings_card_btn.set_size_request(225, 195);
let settings_card_inner = Box::new(Orientation::Vertical, 6);
settings_card_inner.set_valign(gtk::Align::Center);
settings_card_inner.set_halign(gtk::Align::Center);
let settings_title_lbl = Label::new(Some("SETTINGS"));
settings_title_lbl.style_context().add_class("card-title");
let settings_icon_area = DrawingArea::new();
settings_icon_area.set_size_request(56, 56);
settings_icon_area.connect_draw(|_, cr| {
draw_gear_icon(cr);
settings_icon_area.set_size_request(76, 76);
settings_icon_area.set_valign(gtk::Align::Center);
settings_icon_area.set_halign(gtk::Align::Center);
settings_icon_area.connect_draw(|w, cr| {
let width = w.allocated_width() as f64;
let height = w.allocated_height() as f64;
draw_gear_icon(cr, width, height);
gtk::glib::Propagation::Proceed
});
@ -478,14 +501,14 @@ pub fn show_menu_overlay() {
settings_sub_lbl.style_context().add_class("card-sub");
settings_card_inner.pack_start(&settings_title_lbl, false, false, 0);
settings_card_inner.pack_start(&settings_icon_area, true, true, 4);
settings_card_inner.pack_start(&settings_icon_area, false, false, 6);
settings_card_inner.pack_start(&settings_status_lbl, false, false, 0);
settings_card_inner.pack_start(&settings_sub_lbl, false, false, 2);
settings_card_btn.add(&settings_card_inner);
settings_col.pack_start(&settings_card_btn, false, false, 0);
// Pack the 3 columns
// Pack 3 columns
cards_box.pack_start(&replay_col, false, false, 0);
cards_box.pack_start(&record_col, false, false, 0);
cards_box.pack_start(&settings_col, false, false, 0);
@ -494,63 +517,77 @@ pub fn show_menu_overlay() {
hud_page.pack_start(&cards_box, true, true, 0);
// =========================================================================
// PAGE 2: UNIFIED SETTINGS PANEL (Containing Mouse Cursor, Audio, Video, Storage)
// PAGE 2: REVAMPED CLEAN FROSTED GLASS SETTINGS PANEL
// =========================================================================
let settings_page = Box::new(Orientation::Vertical, 10);
let settings_page = Box::new(Orientation::Vertical, 12);
settings_page.style_context().add_class("settings-panel");
settings_page.set_size_request(740, -1);
settings_page.set_size_request(760, -1);
// Settings Header Bar with Back Button
// Header with Back Button (No "X" button)
let settings_header = Box::new(Orientation::Horizontal, 12);
let back_btn = Button::with_label("< Back to Overlay");
back_btn.style_context().add_class("back-btn");
let settings_page_title = Label::new(Some("RECORDER SETTINGS & HARDWARE TUNING"));
let settings_page_title = Label::new(Some("RECORDER SETTINGS"));
settings_page_title.style_context().add_class("settings-title");
let settings_spacer = Box::new(Orientation::Horizontal, 0);
let settings_close_btn = Button::with_label("✕");
settings_close_btn.style_context().add_class("close-btn");
let settings_esc_hint = Label::new(Some("Esc to Close"));
settings_esc_hint.style_context().add_class("esc-pill");
settings_header.pack_start(&back_btn, false, false, 0);
settings_header.pack_start(&settings_page_title, false, false, 8);
settings_header.pack_start(&settings_page_title, false, false, 10);
settings_header.pack_start(&settings_spacer, true, true, 0);
settings_header.pack_start(&settings_close_btn, false, false, 0);
settings_header.pack_start(&settings_esc_hint, false, false, 0);
// Scrolled Settings Body
// Scrollable Body
let scroll = ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE);
scroll.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
scroll.set_min_content_height(420);
scroll.set_max_content_height(460);
scroll.set_min_content_height(440);
scroll.set_max_content_height(480);
let settings_body = Box::new(Orientation::Vertical, 12);
settings_body.set_margin_start(6);
settings_body.set_margin_end(6);
let settings_body = Box::new(Orientation::Vertical, 10);
settings_body.set_margin_start(4);
settings_body.set_margin_end(4);
// -------------------------------------------------------------------------
// SECTION 1: DISPLAY & CAPTURE (Mouse Cursor toggle prominently inside)
// SECTION 1: DISPLAY & CAPTURE (With Mouse Cursor Switch!)
// -------------------------------------------------------------------------
let sec1_lbl = Label::new(Some("DISPLAY & CAPTURE"));
sec1_lbl.style_context().add_class("section-header");
sec1_lbl.set_halign(gtk::Align::Start);
settings_body.pack_start(&sec1_lbl, false, false, 0);
let sec1_card = Box::new(Orientation::Vertical, 8);
sec1_card.style_context().add_class("settings-section-card");
let sec1_hdr = Label::new(Some("DISPLAY & CAPTURE"));
sec1_hdr.style_context().add_class("section-header");
sec1_hdr.set_halign(gtk::Align::Start);
sec1_card.pack_start(&sec1_hdr, false, false, 0);
// 1.1 Mouse Cursor Toggle Row
let cursor_row = Box::new(Orientation::Horizontal, 12);
let cursor_text_box = Box::new(Orientation::Vertical, 2);
let cursor_title = Label::new(Some("Record Mouse Cursor"));
cursor_title.style_context().add_class("setting-row-title");
cursor_title.set_halign(gtk::Align::Start);
let cursor_desc = Label::new(Some("Capture the mouse pointer in gameplay recordings and instant replays"));
cursor_desc.style_context().add_class("sub-info-label");
cursor_desc.set_halign(gtk::Align::Start);
cursor_text_box.pack_start(&cursor_title, false, false, 0);
cursor_text_box.pack_start(&cursor_desc, false, false, 0);
let cursor_switch = Switch::new();
cursor_switch.set_active(config.show_cursor);
cursor_switch.set_valign(gtk::Align::Center);
let cursor_sp = Box::new(Orientation::Horizontal, 0);
cursor_row.pack_start(&cursor_text_box, false, false, 0);
cursor_row.pack_start(&cursor_sp, true, true, 0);
cursor_row.pack_start(&cursor_switch, false, false, 0);
sec1_card.pack_start(&cursor_row, false, false, 4);
let sec1_grid = gtk::Grid::new();
sec1_grid.set_column_spacing(18);
sec1_grid.set_row_spacing(8);
// 1.1 Mouse Cursor Checkbox
let cursor_lbl = Label::new(Some("Mouse Cursor:"));
cursor_lbl.set_halign(gtk::Align::Start);
let cursor_box = Box::new(Orientation::Horizontal, 10);
let cursor_check = CheckButton::with_label("Record Mouse Cursor");
cursor_check.set_active(config.show_cursor);
let cursor_desc = Label::new(Some("(Include mouse pointer in video & replays)"));
cursor_desc.style_context().add_class("sub-info-label");
cursor_box.pack_start(&cursor_check, false, false, 0);
cursor_box.pack_start(&cursor_desc, false, false, 0);
// 1.2 Target Framerate (FPS)
let fps_lbl = Label::new(Some("Target Framerate:"));
fps_lbl.set_halign(gtk::Align::Start);
@ -582,31 +619,33 @@ pub fn show_menu_overlay() {
}
// 1.4 Video Codec
let codec_lbl = Label::new(Some("Video Codec:"));
let codec_lbl = Label::new(Some("Encoder Codec:"));
codec_lbl.set_halign(gtk::Align::Start);
let codec_combo = ComboBoxText::new();
codec_combo.append(Some("h264"), "H.264 / AVC (NVENC / VAAPI - Universal)");
codec_combo.append(Some("h264"), "H.264 / AVC (Fast & Universal)");
codec_combo.append(Some("hevc"), "HEVC / H.265 (High Efficiency)");
codec_combo.append(Some("av1"), "AV1 (Next-Generation High Fidelity)");
codec_combo.append(Some("av1"), "AV1 (Next-Generation Quality)");
codec_combo.set_active_id(Some(&config.video_codec));
sec1_grid.attach(&cursor_lbl, 0, 0, 1, 1);
sec1_grid.attach(&cursor_box, 1, 0, 1, 1);
sec1_grid.attach(&fps_lbl, 0, 1, 1, 1);
sec1_grid.attach(&fps_box, 1, 1, 1, 1);
sec1_grid.attach(&bit_lbl, 0, 2, 1, 1);
sec1_grid.attach(&bit_box, 1, 2, 1, 1);
sec1_grid.attach(&codec_lbl, 0, 3, 1, 1);
sec1_grid.attach(&codec_combo, 1, 3, 1, 1);
settings_body.pack_start(&sec1_grid, false, false, 0);
sec1_grid.attach(&fps_lbl, 0, 0, 1, 1);
sec1_grid.attach(&fps_box, 1, 0, 1, 1);
sec1_grid.attach(&bit_lbl, 0, 1, 1, 1);
sec1_grid.attach(&bit_box, 1, 1, 1, 1);
sec1_grid.attach(&codec_lbl, 0, 2, 1, 1);
sec1_grid.attach(&codec_combo, 1, 2, 1, 1);
sec1_card.pack_start(&sec1_grid, false, false, 2);
settings_body.pack_start(&sec1_card, false, false, 0);
// -------------------------------------------------------------------------
// SECTION 2: INSTANT REPLAY BUFFER
// -------------------------------------------------------------------------
let sec2_lbl = Label::new(Some("INSTANT REPLAY BUFFER"));
sec2_lbl.style_context().add_class("section-header");
sec2_lbl.set_halign(gtk::Align::Start);
settings_body.pack_start(&sec2_lbl, false, false, 4);
let sec2_card = Box::new(Orientation::Vertical, 8);
sec2_card.style_context().add_class("settings-section-card");
let sec2_hdr = Label::new(Some("INSTANT REPLAY BUFFER"));
sec2_hdr.style_context().add_class("section-header");
sec2_hdr.set_halign(gtk::Align::Start);
sec2_card.pack_start(&sec2_hdr, false, false, 0);
let sec2_grid = gtk::Grid::new();
sec2_grid.set_column_spacing(18);
@ -628,15 +667,19 @@ pub fn show_menu_overlay() {
sec2_grid.attach(&dur_lbl, 0, 0, 1, 1);
sec2_grid.attach(&dur_box, 1, 0, 1, 1);
settings_body.pack_start(&sec2_grid, false, false, 0);
sec2_card.pack_start(&sec2_grid, false, false, 2);
settings_body.pack_start(&sec2_card, false, false, 0);
// -------------------------------------------------------------------------
// SECTION 3: AUDIO & SOUND ROUTING
// SECTION 3: AUDIO ROUTING & LEVELS
// -------------------------------------------------------------------------
let sec3_lbl = Label::new(Some("AUDIO & SOUND ROUTING"));
sec3_lbl.style_context().add_class("section-header");
sec3_lbl.set_halign(gtk::Align::Start);
settings_body.pack_start(&sec3_lbl, false, false, 4);
let sec3_card = Box::new(Orientation::Vertical, 8);
sec3_card.style_context().add_class("settings-section-card");
let sec3_hdr = Label::new(Some("AUDIO ROUTING & SOUND"));
sec3_hdr.style_context().add_class("section-header");
sec3_hdr.set_halign(gtk::Align::Start);
sec3_card.pack_start(&sec3_hdr, false, false, 0);
let sec3_grid = gtk::Grid::new();
sec3_grid.set_column_spacing(18);
@ -648,10 +691,10 @@ pub fn show_menu_overlay() {
audio_mode_combo.append(Some("system"), "System Sounds Only (Game / Desktop)");
audio_mode_combo.append(Some("mic"), "Microphone Only");
audio_mode_combo.append(Some("both"), "Both Combined (System Sounds + Microphone)");
audio_mode_combo.append(Some("muted"), "Muted (No Audio Recording)");
audio_mode_combo.append(Some("muted"), "Muted (No Audio)");
audio_mode_combo.set_active_id(Some(&config.audio_mode));
let audio_dev_lbl = Label::new(Some("Input Device:"));
let audio_dev_lbl = Label::new(Some("Recording Device:"));
audio_dev_lbl.set_halign(gtk::Align::Start);
let audio_dev_combo = ComboBoxText::new();
audio_dev_combo.append(Some("default"), "Default Recording Device");
@ -680,15 +723,19 @@ pub fn show_menu_overlay() {
sec3_grid.attach(&sys_vol_scale, 1, 2, 1, 1);
sec3_grid.attach(&mic_vol_lbl, 0, 3, 1, 1);
sec3_grid.attach(&mic_vol_scale, 1, 3, 1, 1);
settings_body.pack_start(&sec3_grid, false, false, 0);
sec3_card.pack_start(&sec3_grid, false, false, 2);
settings_body.pack_start(&sec3_card, false, false, 0);
// -------------------------------------------------------------------------
// SECTION 4: STORAGE & SHORTCUTS
// -------------------------------------------------------------------------
let sec4_lbl = Label::new(Some("STORAGE & SHORTCUTS"));
sec4_lbl.style_context().add_class("section-header");
sec4_lbl.set_halign(gtk::Align::Start);
settings_body.pack_start(&sec4_lbl, false, false, 4);
let sec4_card = Box::new(Orientation::Vertical, 8);
sec4_card.style_context().add_class("settings-section-card");
let sec4_hdr = Label::new(Some("STORAGE & SHORTCUTS"));
sec4_hdr.style_context().add_class("section-header");
sec4_hdr.set_halign(gtk::Align::Start);
sec4_card.pack_start(&sec4_hdr, false, false, 0);
let sec4_grid = gtk::Grid::new();
sec4_grid.set_column_spacing(18);
@ -769,14 +816,15 @@ pub fn show_menu_overlay() {
sec4_grid.attach(&cur_hk_entry, 1, 4, 1, 1);
sec4_grid.attach(&auto_lbl, 0, 5, 1, 1);
sec4_grid.attach(&auto_switch, 1, 5, 1, 1);
settings_body.pack_start(&sec4_grid, false, false, 0);
sec4_card.pack_start(&sec4_grid, false, false, 2);
settings_body.pack_start(&sec4_card, false, false, 0);
scroll.add(&settings_body);
// Apply & Save Settings Button
let apply_btn = Button::with_label("Apply & Save Settings");
apply_btn.style_context().add_class("apply-save-btn");
apply_btn.set_size_request(-1, 42);
apply_btn.set_size_request(-1, 44);
settings_page.pack_start(&settings_header, false, false, 0);
settings_page.pack_start(&scroll, true, true, 4);
@ -788,7 +836,7 @@ pub fn show_menu_overlay() {
window.add(&stack);
// =========================================================================
// CSS STYLING (Translucent NVIDIA Frosted Glass Theme)
// CSS STYLING (Translucent Frosted Glass Theme)
// =========================================================================
let css_provider = CssProvider::new();
let css = r#"
@ -800,12 +848,13 @@ pub fn show_menu_overlay() {
background-color: transparent;
padding: 0px;
}
/* Floating Frosted Glass Top Bar */
.header-bar {
background-color: rgba(10, 14, 20, 0.88);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
padding: 6px 14px;
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.6);
background-color: rgba(18, 26, 38, 0.72);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
padding: 8px 18px;
box-shadow: 0px 12px 32px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.16);
}
.vrec-badge {
background-color: #76b900;
@ -818,45 +867,43 @@ pub fn show_menu_overlay() {
}
.overlay-title {
color: #f1f5f9;
font-size: 12px;
font-size: 12.5px;
font-weight: 800;
letter-spacing: 1px;
letter-spacing: 1.2px;
}
.status-banner {
color: #76b900;
font-size: 11.5px;
font-weight: 700;
}
.close-btn {
background-color: transparent;
.esc-pill {
background-color: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.14);
color: #94a3b8;
border: none;
font-size: 14px;
font-size: 10.5px;
font-weight: 700;
padding: 2px 8px;
border-radius: 6px;
padding: 3px 8px;
}
.close-btn:hover {
color: #ef4444;
}
/* Square Action Cards */
/* Frosted Glass Action Cards */
.card-btn {
background-color: rgba(14, 18, 26, 0.85);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
padding: 14px;
box-shadow: 0px 12px 36px rgba(0, 0, 0, 0.7);
background-color: rgba(20, 28, 42, 0.68);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
padding: 18px;
box-shadow: 0px 16px 40px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.card-btn:hover {
background-color: rgba(10, 14, 20, 0.98);
background-color: rgba(26, 38, 56, 0.82);
border-color: #76b900;
box-shadow: 0 0 16px rgba(118, 185, 0, 0.4);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.65), 0 0 20px rgba(118, 185, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.card-btn-active {
background-color: rgba(10, 14, 20, 0.98);
background-color: rgba(26, 38, 56, 0.90);
border-color: #76b900;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
box-shadow: 0 0 16px rgba(118, 185, 0, 0.4);
box-shadow: 0 0 20px rgba(118, 185, 0, 0.35);
}
.card-title {
color: #ffffff;
@ -866,7 +913,7 @@ pub fn show_menu_overlay() {
}
.card-status {
color: #94a3b8;
font-size: 11px;
font-size: 11.5px;
font-weight: 700;
}
.status-green {
@ -877,57 +924,69 @@ pub fn show_menu_overlay() {
}
.card-sub {
color: #64748b;
font-size: 10px;
font-size: 10.5px;
font-weight: 500;
}
/* Attached Dropdown Menus */
/* Attached Frosted Dropdown Menus */
.dropdown-menu {
background-color: rgba(10, 14, 20, 0.98);
border: 1px solid rgba(255, 255, 255, 0.14);
background-color: rgba(16, 22, 34, 0.88);
border: 1px solid rgba(255, 255, 255, 0.15);
border-top: none;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
padding: 6px;
box-shadow: 0px 16px 36px rgba(0, 0, 0, 0.85);
border-bottom-left-radius: 14px;
border-bottom-right-radius: 14px;
padding: 8px;
box-shadow: 0px 20px 48px rgba(0, 0, 0, 0.75);
}
.dropdown-item {
background-color: transparent;
color: #e2e8f0;
font-size: 12px;
font-size: 12.5px;
font-weight: 700;
border-radius: 6px;
border-radius: 8px;
border: 1px solid transparent;
padding: 8px 12px;
padding: 9px 14px;
}
.dropdown-item:hover {
background-color: rgba(255, 255, 255, 0.08);
border-color: #76b900;
background-color: rgba(118, 185, 0, 0.15);
border-color: rgba(118, 185, 0, 0.5);
color: #76b900;
}
/* Frosted Settings Panel */
.settings-panel {
background-color: rgba(14, 18, 26, 0.96);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 14px;
padding: 16px 22px;
box-shadow: 0px 16px 48px rgba(0, 0, 0, 0.85);
background-color: rgba(15, 22, 32, 0.90);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 18px;
padding: 20px 26px;
box-shadow: 0px 24px 64px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.settings-section-card {
background-color: rgba(24, 32, 46, 0.55);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 14px 18px;
}
.settings-title {
color: #ffffff;
font-size: 13px;
font-size: 13.5px;
font-weight: 800;
letter-spacing: 1px;
}
.setting-row-title {
color: #ffffff;
font-size: 13px;
font-weight: 700;
}
.back-btn {
background-color: transparent;
background-color: rgba(118, 185, 0, 0.12);
color: #76b900;
font-size: 12px;
font-weight: 700;
border: 1px solid rgba(118, 185, 0, 0.4);
border-radius: 6px;
padding: 4px 10px;
font-weight: 800;
border: 1px solid rgba(118, 185, 0, 0.45);
border-radius: 8px;
padding: 6px 14px;
}
.back-btn:hover {
background-color: rgba(118, 185, 0, 0.15);
background-color: rgba(118, 185, 0, 0.22);
color: #8ce000;
}
.section-header {
@ -938,15 +997,15 @@ pub fn show_menu_overlay() {
}
.preset-btn {
background-color: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
color: #cbd5e1;
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
padding: 4px 10px;
}
.preset-btn:hover {
background-color: rgba(118, 185, 0, 0.2);
background-color: rgba(118, 185, 0, 0.22);
border-color: #76b900;
color: #ffffff;
}
@ -957,29 +1016,31 @@ pub fn show_menu_overlay() {
font-weight: 800;
border-radius: 8px;
border: none;
padding: 8px 14px;
padding: 10px 16px;
box-shadow: 0 4px 16px rgba(118, 185, 0, 0.35);
}
.apply-save-btn:hover {
background-color: #8ce000;
}
checkbutton check {
background-color: #1a2230;
switch {
border-radius: 14px;
background-color: #1e2838;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
checkbutton check:checked {
switch:checked {
background-color: #76b900;
border-color: #8ce000;
}
checkbutton label {
color: #ffffff;
font-size: 12px;
font-weight: 700;
switch slider {
background-color: #ffffff;
border-radius: 50%;
min-width: 18px;
min-height: 18px;
}
entry, spinbutton, combobox button {
background-color: #161c28;
background-color: rgba(18, 24, 36, 0.7);
color: #f1f5f9;
border: 1px solid rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 6px;
padding: 5px 8px;
font-size: 12px;
@ -1003,7 +1064,7 @@ pub fn show_menu_overlay() {
min-height: 14px;
}
.sub-info-label {
color: #64748b;
color: #94a3b8;
font-size: 10.5px;
font-weight: 500;
}
@ -1018,13 +1079,8 @@ pub fn show_menu_overlay() {
}
// =========================================================================
// EVENT HANDLERS & NAVIGATION
// EVENT HANDLERS & NAVIGATION (Esc key closes cleanly)
// =========================================================================
let w_close1 = window.clone();
close_btn.connect_clicked(move |_| w_close1.close());
let w_close2 = window.clone();
settings_close_btn.connect_clicked(move |_| w_close2.close());
let w_esc = window.clone();
window.connect_key_press_event(move |_, key| {
if key.keyval() == gdk::keys::constants::Escape {
@ -1038,7 +1094,6 @@ pub fn show_menu_overlay() {
// -------------------------------------------------------------------------
// CARD INTERACTION: TOGGLE ATTACHED DROPDOWNS
// -------------------------------------------------------------------------
// 1. Replay Card Click -> Toggle Replay Dropdown
let rev_r_click = replay_revealer.clone();
let rev_rec_close = record_revealer.clone();
let card_r_btn = replay_card_btn.clone();
@ -1055,7 +1110,6 @@ pub fn show_menu_overlay() {
}
});
// 2. Record Card Click -> Toggle Record Dropdown
let rev_rec_click = record_revealer.clone();
let rev_r_close = replay_revealer.clone();
let card_rec_btn2 = record_card_btn.clone();
@ -1072,12 +1126,11 @@ pub fn show_menu_overlay() {
}
});
// 3. Settings Card Click -> Navigate directly to Settings Page
let stack_to_settings = stack.clone();
let w_to_settings = window.clone();
settings_card_btn.connect_clicked(move |_| {
stack_to_settings.set_visible_child_name("settings");
w_to_settings.resize(740, 560);
w_to_settings.resize(760, 560);
});
// Back to HUD from Settings
@ -1088,8 +1141,7 @@ pub fn show_menu_overlay() {
w_to_hud.resize(740, 280);
});
// Replay Menu Items:
// Item 1: Turn on / Turn off Replay
// Replay Menu Items
let r_toggle_lbl = replay_toggle_item.clone();
let r_status_lbl_sync = replay_status_lbl.clone();
let r_state_sync = Arc::clone(&replay_active_state);
@ -1118,7 +1170,6 @@ pub fn show_menu_overlay() {
card_r_deactivate.style_context().remove_class("card-btn-active");
});
// Item 2: Save Replay
let banner_save = status_banner.clone();
let rev_r_hide2 = replay_revealer.clone();
let card_r_deactivate2 = replay_card_btn.clone();
@ -1134,7 +1185,6 @@ pub fn show_menu_overlay() {
card_r_deactivate2.style_context().remove_class("card-btn-active");
});
// Item 3: Replay Settings
let stack_r_settings = stack.clone();
let w_r_settings = window.clone();
let rev_r_hide3 = replay_revealer.clone();
@ -1143,11 +1193,10 @@ pub fn show_menu_overlay() {
rev_r_hide3.set_reveal_child(false);
card_r_deactivate3.style_context().remove_class("card-btn-active");
stack_r_settings.set_visible_child_name("settings");
w_r_settings.resize(740, 560);
w_r_settings.resize(760, 560);
});
// Record Menu Items:
// Item 1: Start / Stop Record
// Record Menu Items
let rev_rec_hide = record_revealer.clone();
let card_rec_deactivate = record_card_btn.clone();
record_toggle_item.connect_clicked(move |_| {
@ -1156,7 +1205,6 @@ pub fn show_menu_overlay() {
card_rec_deactivate.style_context().remove_class("card-btn-active");
});
// Item 2: Record Settings
let stack_rec_settings = stack.clone();
let w_rec_settings = window.clone();
let rev_rec_hide2 = record_revealer.clone();
@ -1165,14 +1213,12 @@ pub fn show_menu_overlay() {
rev_rec_hide2.set_reveal_child(false);
card_rec_deactivate2.style_context().remove_class("card-btn-active");
stack_rec_settings.set_visible_child_name("settings");
w_rec_settings.resize(740, 560);
w_rec_settings.resize(760, 560);
});
// Direct Mouse Cursor Checkbox Toggle
let cursor_check_sync = cursor_check.clone();
// Direct Mouse Cursor Switch Toggle
let banner_cur = status_banner.clone();
cursor_check.connect_toggled(move |c| {
let active = c.is_active();
cursor_switch.connect_state_set(move |_, active| {
let mut cfg = VrecConfig::load();
if cfg.show_cursor != active {
cfg.show_cursor = active;
@ -1185,6 +1231,7 @@ pub fn show_menu_overlay() {
gtk::glib::ControlFlow::Break
});
}
gtk::glib::Propagation::Proceed
});
// Apply & Save Settings
@ -1193,10 +1240,11 @@ pub fn show_menu_overlay() {
let banner_after_save = status_banner.clone();
let r_sub_lbl_sync = replay_sub_lbl.clone();
let rec_sub_lbl_sync = record_sub_lbl.clone();
let cur_sw_sync = cursor_switch.clone();
apply_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.show_cursor = cursor_check_sync.is_active();
cfg.show_cursor = cur_sw_sync.is_active();
cfg.fps = fps_spin.value() as u32;
let bit_mbps = bit_spin.value() as u32;
cfg.record_bitrate_kbps = bit_mbps * 1000;