feat(ui): complete overhaul matching GPU Screen Recorder UI screenshots with screenshot feature

This commit is contained in:
katR 2026-10-04 17:06:12 +05:30
parent 8831c501a1
commit f9c7ab772e
4 changed files with 652 additions and 180 deletions

View file

@ -302,6 +302,54 @@ fn handle_save_replay() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
send_with_notification(Command::SaveReplay, "INSTANT REPLAY", "Saved to Videos", ToastIcon::Replay)
}
fn handle_screenshot() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let cfg = scythe::config::ScytheConfig::load();
let dir = scythe::config::ScytheConfig::expand_tilde(&cfg.screenshot_directory);
let _ = std::fs::create_dir_all(&dir);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
let ext = if cfg.screenshot_format.to_lowercase() == "jpg" || cfg.screenshot_format.to_lowercase() == "jpeg" { "jpg" } else { "png" };
let filename = format!("Screenshot_{}.{}", now, ext);
let out_path = dir.join(&filename);
#[cfg(target_os = "linux")]
{
let mut grim = std::process::Command::new("grim");
if cfg.screenshot_cursor {
grim.arg("-c");
}
if ext == "jpg" {
grim.args(["-t", "jpeg", "-q", "90"]);
}
grim.arg(&out_path);
let res = grim.status();
if res.is_ok() && out_path.exists() {
show_shadowplay_toast("SCREENSHOT", &format!("Saved to {}", filename), ToastIcon::Screenshot);
return Ok(());
}
let _ = std::process::Command::new("import").args(["-window", "root"]).arg(&out_path).status();
}
#[cfg(target_os = "windows")]
{
let ps_cmd = format!(
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing; \
$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; \
$bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height; \
$g = [System.Drawing.Graphics]::FromImage($bmp); \
$g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size); \
$bmp.Save('{}'); $g.Dispose(); $bmp.Dispose()",
out_path.to_string_lossy().replace('/', "\\")
);
let _ = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &ps_cmd])
.status();
}
show_shadowplay_toast("SCREENSHOT", &format!("Saved to {}", filename), ToastIcon::Screenshot);
Ok(())
}
fn handle_toggle_streaming() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ensure_daemon_running();
let cfg = scythe::config::ScytheConfig::load();
@ -358,6 +406,9 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
show_shadowplay_toast("RECORDING", "Recording saved", ToastIcon::Save);
return Ok(());
}
"--screenshot" => {
return handle_screenshot();
}
"--toast" => {
let title = args.get(2).map(|s| s.as_str()).unwrap_or("SCYTHE");
let subtitle = args.get(3).map(|s| s.as_str()).unwrap_or("");
@ -514,10 +565,19 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
};
#[derive(Default)]
struct RegisteredIds {
menu: Option<u32>,
save: Option<u32>,
rec: Option<u32>,
cur: Option<u32>,
shot: Option<u32>,
}
let mut current_config = scythe::config::ScytheConfig::load();
let mut registered_hotkeys: Vec<HotKey> = Vec::new();
let register_all = |mgr: &GlobalHotKeyManager, cfg: &scythe::config::ScytheConfig, reg: &mut Vec<HotKey>| -> (Option<u32>, Option<u32>, Option<u32>, Option<u32>) {
let register_all = |mgr: &GlobalHotKeyManager, cfg: &scythe::config::ScytheConfig, reg: &mut Vec<HotKey>| -> RegisteredIds {
for hk in reg.drain(..) {
let _ = mgr.unregister(hk);
}
@ -526,40 +586,43 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let m_save = scythe::hotkey::parse_hotkey(&cfg.save_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR)));
let m_rec = scythe::hotkey::parse_hotkey(&cfg.record_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9)));
let m_cur = scythe::hotkey::parse_hotkey(&cfg.cursor_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F10)));
let m_shot = scythe::hotkey::parse_hotkey(&cfg.screenshot_hotkey).or_else(|| Some(HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyS)));
let mut id_menu = None;
let mut id_save = None;
let mut id_rec = None;
let mut id_cur = None;
let mut ids = RegisteredIds::default();
if !is_on_hyprland
&& let Some(hk) = m_menu {
id_menu = Some(hk.id());
ids.menu = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_save {
id_save = Some(hk.id());
ids.save = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_rec {
id_rec = Some(hk.id());
ids.rec = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
if let Some(hk) = m_cur {
id_cur = Some(hk.id());
ids.cur = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
(id_menu, id_save, id_rec, id_cur)
if let Some(hk) = m_shot {
ids.shot = Some(hk.id());
let _ = mgr.register(hk);
reg.push(hk);
}
ids
};
let (mut id_menu, mut id_save, mut id_rec, mut id_cur) = register_all(&manager, &current_config, &mut registered_hotkeys);
let mut current_ids = register_all(&manager, &current_config, &mut registered_hotkeys);
println!("Listening for global hotkeys (Overlay: {}, Replay: {}, Record: {}, Cursor: {})...",
current_config.menu_hotkey, current_config.save_hotkey, current_config.record_hotkey, current_config.cursor_hotkey);
println!("Listening for global hotkeys (Overlay: {}, Replay: {}, Record: {}, Cursor: {}, Screenshot: {})...",
current_config.menu_hotkey, current_config.save_hotkey, current_config.record_hotkey, current_config.cursor_hotkey, current_config.screenshot_hotkey);
let receiver = GlobalHotKeyEvent::receiver();
let mut last_config_check = std::time::Instant::now();
@ -584,13 +647,10 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|| new_cfg.save_hotkey != current_config.save_hotkey
|| new_cfg.record_hotkey != current_config.record_hotkey
|| new_cfg.cursor_hotkey != current_config.cursor_hotkey
|| new_cfg.screenshot_hotkey != current_config.screenshot_hotkey
{
current_config = new_cfg;
let (im, is, ir, ic) = register_all(&manager, &current_config, &mut registered_hotkeys);
id_menu = im;
id_save = is;
id_rec = ir;
id_cur = ic;
current_ids = register_all(&manager, &current_config, &mut registered_hotkeys);
}
}
@ -598,23 +658,27 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if event.state != HotKeyState::Pressed {
continue;
}
if Some(event.id) == id_menu {
if Some(event.id) == current_ids.menu {
std::thread::spawn(|| {
ensure_daemon_running();
let _ = get_ui_cmd().arg("--menu").spawn();
});
} else if Some(event.id) == id_save {
} else if Some(event.id) == current_ids.save {
std::thread::spawn(|| {
let _ = handle_save_replay();
});
} else if Some(event.id) == id_rec {
} else if Some(event.id) == current_ids.rec {
std::thread::spawn(|| {
let _ = handle_toggle_recording();
});
} else if Some(event.id) == id_cur {
} else if Some(event.id) == current_ids.cur {
std::thread::spawn(|| {
let _ = handle_toggle_cursor();
});
} else if Some(event.id) == current_ids.shot {
std::thread::spawn(|| {
let _ = handle_screenshot();
});
}
}
#[cfg(target_os = "windows")]

View file

@ -22,6 +22,13 @@ fn default_accent_color() -> String { "blue".to_string() }
fn default_stream_service() -> String { "twitch".to_string() }
fn default_stream_url() -> String { "rtmp://live.twitch.tv/app/".to_string() }
fn default_screenshot_hotkey() -> String { "Ctrl+Shift+S".to_string() }
fn default_screenshot_format() -> String { "png".to_string() }
fn default_capture_source() -> String { "focused".to_string() }
fn default_video_quality_mode() -> String { "cbr".to_string() }
fn default_color_range() -> String { "limited".to_string() }
fn default_audio_codec() -> String { "opus".to_string() }
fn default_replay_storage() -> String { "ram".to_string() }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ScytheConfig {
@ -83,6 +90,30 @@ pub struct ScytheConfig {
pub stream_url: String,
#[serde(default)]
pub stream_key: String,
#[serde(default = "default_screenshot_hotkey")]
pub screenshot_hotkey: String,
#[serde(default = "ScytheConfig::default_screenshot_directory")]
pub screenshot_directory: String,
#[serde(default = "default_screenshot_format")]
pub screenshot_format: String,
#[serde(default)]
pub screenshot_cursor: bool,
#[serde(default = "default_capture_source")]
pub capture_source: String,
#[serde(default = "default_video_quality_mode")]
pub video_quality_mode: String,
#[serde(default = "default_color_range")]
pub color_range: String,
#[serde(default = "default_audio_codec")]
pub audio_codec: String,
#[serde(default = "default_replay_storage")]
pub replay_storage: String,
#[serde(default)]
pub record_low_power: bool,
#[serde(default = "default_true")]
pub show_replay_notifications: bool,
}
pub type VrecConfig = ScytheConfig;
@ -120,6 +151,19 @@ impl Default for ScytheConfig {
stream_service: "twitch".to_string(),
stream_url: "rtmp://live.twitch.tv/app/".to_string(),
stream_key: String::new(),
screenshot_hotkey: default_screenshot_hotkey(),
screenshot_directory: Self::default_screenshot_directory(),
screenshot_format: default_screenshot_format(),
screenshot_cursor: false,
capture_source: default_capture_source(),
video_quality_mode: default_video_quality_mode(),
color_range: default_color_range(),
audio_codec: default_audio_codec(),
replay_storage: default_replay_storage(),
record_low_power: false,
show_replay_notifications: true,
}
}
}
@ -135,6 +179,16 @@ impl ScytheConfig {
"~/Videos".to_string()
}
pub fn default_screenshot_directory() -> String {
if let Some(pic) = dirs::picture_dir() {
return pic.join("Screenshots").to_string_lossy().to_string();
}
if let Some(home) = dirs::home_dir() {
return home.join("Pictures").join("Screenshots").to_string_lossy().to_string();
}
Self::default_output_directory()
}
pub fn config_path() -> PathBuf {
let base = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
base.join("scythe").join("config.json")

View file

@ -183,6 +183,7 @@ pub fn register_hyprland_binds(config: &ScytheConfig) {
(&config.save_hotkey, "scythe-ui --save"),
(&config.record_hotkey, "scythe-ui --record"),
(&config.cursor_hotkey, "scythe-ui --cursor"),
(&config.screenshot_hotkey, "scythe-ui --screenshot"),
];
for (hotkey, cmd) in binds {

View file

@ -14,9 +14,24 @@ pub enum SettingsTab {
Replay,
Record,
Livestream,
Screenshot,
General,
}
fn draw_window_shadow(painter: &egui::Painter, rect: egui::Rect, spread: f32) {
let shadow_steps = [
(2.0, 60),
(5.0, 38),
(10.0, 22),
(16.0, 12),
(24.0, 5),
];
for (expand, alpha) in shadow_steps {
let s_rect = rect.expand(expand * spread).translate(Vec2::new(0.0, expand * 0.4 * spread));
painter.rect_filled(s_rect, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(0, 0, 0, alpha));
}
}
#[derive(Debug, Clone)]
pub struct InOverlayToast {
pub title: String,
@ -1112,6 +1127,15 @@ pub struct ScytheOverlayApp {
hud_notification: Option<HudNotification>,
pub active_settings_tab: SettingsTab,
pub in_overlay_toast: Option<InOverlayToast>,
pub screenshot_dir: String,
pub screenshot_format: String,
pub screenshot_cursor: bool,
pub capture_source: String,
pub video_quality_mode: String,
pub color_range: String,
pub replay_storage: String,
pub record_low_power: bool,
pub show_replay_notifications: bool,
}
#[derive(Clone, Debug)]
@ -1181,6 +1205,16 @@ impl ScytheOverlayApp {
let auto_check_updates = config.auto_check_updates;
let autostart_replay = config.autostart_replay;
let autostart_overlay = config.autostart_overlay;
let screenshot_dir = config.screenshot_directory.clone();
let screenshot_format = config.screenshot_format.clone();
let screenshot_cursor = config.screenshot_cursor;
let capture_source = config.capture_source.clone();
let video_quality_mode = config.video_quality_mode.clone();
let color_range = config.color_range.clone();
let replay_storage = config.replay_storage.clone();
let record_low_power = config.record_low_power;
let show_replay_notifications = config.show_replay_notifications;
let update_status = Arc::new(std::sync::Mutex::new(crate::updater::UpdateStatus::Idle));
if auto_check_updates {
crate::updater::spawn_update_check(update_status.clone());
@ -1238,6 +1272,15 @@ impl ScytheOverlayApp {
hud_notification: None,
active_settings_tab: SettingsTab::Replay,
in_overlay_toast: None,
screenshot_dir,
screenshot_format,
screenshot_cursor,
capture_source,
video_quality_mode,
color_range,
replay_storage,
record_low_power,
show_replay_notifications,
}
}
@ -1268,6 +1311,15 @@ impl ScytheOverlayApp {
self.config.autostart_replay = self.autostart_replay;
self.config.autostart_overlay = self.autostart_overlay;
self.config.autostart = self.autostart_replay;
self.config.screenshot_directory = self.screenshot_dir.clone();
self.config.screenshot_format = self.screenshot_format.clone();
self.config.screenshot_cursor = self.screenshot_cursor;
self.config.capture_source = self.capture_source.clone();
self.config.video_quality_mode = self.video_quality_mode.clone();
self.config.color_range = self.color_range.clone();
self.config.replay_storage = self.replay_storage.clone();
self.config.record_low_power = self.record_low_power;
self.config.show_replay_notifications = self.show_replay_notifications;
let _ = self.config.save();
crate::hyprland_binds::register_hyprland_binds(&self.config);
crate::config::ScytheConfig::notify_daemon_reload();
@ -1290,9 +1342,61 @@ impl ScytheOverlayApp {
self.stream_service_idx = 0;
self.stream_url = "rtmp://live.twitch.tv/app/".to_string();
self.stream_key.clear();
self.screenshot_dir = def.screenshot_directory;
self.screenshot_format = def.screenshot_format;
self.screenshot_cursor = def.screenshot_cursor;
self.capture_source = def.capture_source;
self.video_quality_mode = def.video_quality_mode;
self.color_range = def.color_range;
self.replay_storage = def.replay_storage;
self.record_low_power = def.record_low_power;
self.show_replay_notifications = def.show_replay_notifications;
self.show_hud_notification("DEFAULTS", "Settings restored to defaults", crate::overlay::ToastIcon::Info);
}
pub fn take_screenshot(&mut self) {
let dir = ScytheConfig::expand_tilde(&self.screenshot_dir);
let _ = std::fs::create_dir_all(&dir);
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
let ext = if self.screenshot_format.to_lowercase() == "jpg" || self.screenshot_format.to_lowercase() == "jpeg" { "jpg" } else { "png" };
let fname = format!("Screenshot_{}.{}", now, ext);
let out_path = dir.join(&fname);
#[cfg(target_os = "linux")]
{
let mut grim = std::process::Command::new("grim");
if self.screenshot_cursor {
grim.arg("-c");
}
if ext == "jpg" {
grim.args(["-t", "jpeg", "-q", "90"]);
}
grim.arg(&out_path);
let res = grim.status();
if res.is_err() || !out_path.exists() {
let _ = std::process::Command::new("import").args(["-window", "root"]).arg(&out_path).status();
}
}
#[cfg(target_os = "windows")]
{
let ps_cmd = format!(
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing; \
$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; \
$bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height; \
$g = [System.Drawing.Graphics]::FromImage($bmp); \
$g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size); \
$bmp.Save('{}'); $g.Dispose(); $bmp.Dispose()",
out_path.to_string_lossy().replace('/', "\\")
);
let _ = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &ps_cmd])
.status();
}
self.show_hud_notification("SCREENSHOT", &format!("Saved to {}", fname), crate::overlay::ToastIcon::Screenshot);
}
pub fn show_hud_notification(&mut self, title: &str, subtitle: &str, icon: crate::overlay::ToastIcon) {
self.in_overlay_toast = Some(InOverlayToast {
title: title.to_string(),
@ -1812,7 +1916,7 @@ impl ScytheOverlayApp {
let is_compact_h = screen_h < 850.0;
let modal_w = if is_compact_w { 700.0_f32 } else { 780.0_f32 };
let modal_h = if is_compact_h { 460.0_f32 } else { 520.0_f32 };
let modal_h = if is_compact_h { 480.0_f32 } else { 540.0_f32 };
let modal_x = ((screen_w - modal_w) * 0.5).floor();
let modal_y = (48.0 + (screen_h - 48.0 - modal_h) * 0.5).floor().max(52.0);
@ -1826,57 +1930,51 @@ impl ScytheOverlayApp {
let painter = ui.painter();
// 1. Smooth Rounded Modal Container (Obsidian Glass `#101418` + CornerRadius 12 + soft anti-aliased border)
// 1. Sharp Window with Deep Realistic Drop Shadow (Zero sloppy roundness, NO top accent embed bar)
draw_window_shadow(painter, modal_rect, 1.0);
painter.rect_filled(
modal_rect,
CornerRadius::same(12),
Color32::from_rgb(16, 20, 24),
CornerRadius::ZERO,
Color32::from_rgb(21, 25, 30),
);
painter.rect_stroke(
modal_rect,
CornerRadius::same(12),
Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 26)),
CornerRadius::ZERO,
Stroke::new(1.0_f32, Color32::from_rgb(40, 48, 60)),
egui::StrokeKind::Inside,
);
// Smooth top accent curve
let top_accent = egui::Rect::from_min_size(modal_rect.min, egui::vec2(modal_w, 3.0));
painter.rect_filled(
top_accent,
CornerRadius { nw: 11, ne: 11, sw: 0, se: 0 },
accent,
);
let sidebar_w = if is_compact_w { 190.0_f32 } else { 210.0_f32 };
let sidebar_w = if is_compact_w { 180.0_f32 } else { 200.0_f32 };
let sidebar_rect = egui::Rect::from_min_size(
egui::pos2(modal_rect.left(), modal_rect.top() + 3.0),
egui::vec2(sidebar_w, modal_h - 3.0),
modal_rect.min,
egui::vec2(sidebar_w, modal_h),
);
// Left sidebar background matching smooth left corners
// Sidebar background
painter.rect_filled(
sidebar_rect,
CornerRadius { nw: 11, ne: 0, sw: 11, se: 0 },
Color32::from_rgb(12, 15, 18),
CornerRadius::ZERO,
Color32::from_rgb(17, 20, 24),
);
// Vertical divider
painter.line_segment(
[egui::pos2(sidebar_rect.right(), sidebar_rect.top()), egui::pos2(sidebar_rect.right(), sidebar_rect.bottom())],
Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 16)),
Stroke::new(1.0_f32, Color32::from_rgb(34, 40, 50)),
);
// 2. Left Sidebar: The 4 Dedicated Menus + Bottom Pinned Footer
let sidebar_inner = sidebar_rect.shrink2(egui::vec2(10.0, 14.0));
// 2. Left Sidebar: 5 Category Tabs + Pinned Bottom End
let sidebar_inner = sidebar_rect.shrink2(egui::vec2(8.0, 12.0));
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(sidebar_inner), |ui| {
ui.set_width(sidebar_inner.width());
// Bottom-pinned footer: Version and Reset to Defaults pinned at the complete end
// Pinned at the very bottom end
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.add_space(4.0);
ui.label(
egui::RichText::new("Scythe v0.2.17")
.size(10.0)
.color(Color32::from_rgb(95, 100, 110)),
.color(Color32::from_rgb(100, 108, 120)),
);
ui.add_space(6.0);
let reset_resp = ui.add(
@ -1885,29 +1983,30 @@ impl ScytheOverlayApp {
.size(11.5)
.color(Color32::from_rgb(180, 185, 190)),
)
.fill(Color32::from_rgb(20, 24, 28))
.stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 20)))
.corner_radius(CornerRadius::same(5))
.min_size(egui::vec2(sidebar_inner.width() - 8.0, 28.0)),
.fill(Color32::from_rgb(24, 28, 35))
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(42, 50, 62)))
.corner_radius(CornerRadius::ZERO)
.min_size(egui::vec2(sidebar_inner.width() - 4.0, 28.0)),
);
if reset_resp.clicked() {
self.reset_settings_to_defaults();
}
ui.add_space(10.0);
ui.add_space(8.0);
ui.separator();
ui.add_space(10.0);
ui.add_space(8.0);
// Top: The 4 distinct menus
// Top tabs: The 5 distinct menus
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
let tabs = [
(SettingsTab::Replay, "Instant Replay"),
(SettingsTab::Record, "Normal Recording"),
(SettingsTab::Livestream, "Broadcast LIVE"),
(SettingsTab::Screenshot, "Screenshot"),
(SettingsTab::General, "General Settings"),
];
let tab_btn_w = sidebar_inner.width();
let tab_btn_h = 38.0_f32;
let tab_btn_h = 36.0_f32;
for (tab, label) in tabs {
let is_active = self.active_settings_tab == tab;
@ -1915,9 +2014,9 @@ impl ScytheOverlayApp {
let is_hovered = btn_resp.hovered();
let bg = if is_active {
Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 36)
Color32::from_rgb(28, 34, 42)
} else if is_hovered {
Color32::from_rgba_unmultiplied(255, 255, 255, 14)
Color32::from_rgb(22, 26, 32)
} else {
Color32::TRANSPARENT
};
@ -1925,23 +2024,23 @@ impl ScytheOverlayApp {
let text_col = if is_active {
Color32::WHITE
} else if is_hovered {
Color32::from_rgb(225, 230, 235)
Color32::from_rgb(220, 225, 230)
} else {
Color32::from_rgb(150, 155, 165)
Color32::from_rgb(150, 158, 170)
};
ui.painter().rect_filled(btn_rect, CornerRadius::same(6), bg);
ui.painter().rect_filled(btn_rect, CornerRadius::ZERO, bg);
if is_active {
let pill_rect = egui::Rect::from_min_size(
egui::pos2(btn_rect.left(), btn_rect.top() + 7.0),
egui::vec2(3.0, tab_btn_h - 14.0),
egui::pos2(btn_rect.left(), btn_rect.top() + 4.0),
egui::vec2(3.0, tab_btn_h - 8.0),
);
ui.painter().rect_filled(pill_rect, CornerRadius::same(1), accent);
ui.painter().rect_filled(pill_rect, CornerRadius::ZERO, accent);
}
ui.painter().text(
egui::pos2(btn_rect.left() + 16.0, btn_rect.center().y),
egui::pos2(btn_rect.left() + 14.0, btn_rect.center().y),
egui::Align2::LEFT_CENTER,
label,
FontId::proportional(if is_active { 12.5 } else { 12.0 }),
@ -1953,7 +2052,7 @@ impl ScytheOverlayApp {
self.current_view = ShadowPlayView::Settings(tab);
}
ui.add_space(4.0);
ui.add_space(3.0);
}
});
});
@ -1963,11 +2062,11 @@ impl ScytheOverlayApp {
let content_x = modal_rect.left() + sidebar_w;
let content_w = modal_w - sidebar_w;
let content_rect = egui::Rect::from_min_size(
egui::pos2(content_x, modal_rect.top() + 3.0),
egui::vec2(content_w, modal_h - 3.0),
egui::pos2(content_x, modal_rect.top()),
egui::vec2(content_w, modal_h),
);
let content_inner = content_rect.shrink2(egui::vec2(16.0, 14.0));
let content_inner = content_rect.shrink2(egui::vec2(16.0, 12.0));
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(content_inner), |ui| {
ui.set_width(content_inner.width());
@ -1977,6 +2076,7 @@ impl ScytheOverlayApp {
SettingsTab::Replay => "INSTANT REPLAY SETTINGS",
SettingsTab::Record => "NORMAL RECORDING SETTINGS",
SettingsTab::Livestream => "BROADCAST LIVE SETTINGS",
SettingsTab::Screenshot => "SCREENSHOT SETTINGS",
SettingsTab::General => "GENERAL SETTINGS",
};
@ -2003,7 +2103,7 @@ impl ScytheOverlayApp {
ui.add_space(8.0);
// Body: scrollable panel for active tab
let scroll_h = content_inner.height() - header_h - 24.0;
let scroll_h = content_inner.height() - header_h - 20.0;
egui::ScrollArea::vertical()
.max_height(scroll_h)
.auto_shrink([false, false])
@ -2013,6 +2113,7 @@ impl ScytheOverlayApp {
SettingsTab::Replay => self.render_tab_replay(ui, accent),
SettingsTab::Record => self.render_tab_record(ui, accent),
SettingsTab::Livestream => self.render_tab_livestream(ui, accent),
SettingsTab::Screenshot => self.render_tab_screenshot(ui, accent),
SettingsTab::General => self.render_tab_general(ui, accent),
}
});
@ -2020,36 +2121,109 @@ impl ScytheOverlayApp {
}
fn render_tab_replay(&mut self, ui: &mut egui::Ui, accent: Color32) {
render_section_card(ui, "Buffer Duration & RAM Footprint", accent, |ui| {
ui.label(egui::RichText::new("Replay buffer duration (5 - 1800 seconds):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
render_section_card(ui, "File info", accent, |ui| {
ui.label(egui::RichText::new("Directory to save replays:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
numeric_input_box(ui, &mut self.replay_sec_input_str, &mut self.replay_sec, 5, 1800, 10, "sec", accent);
ui.horizontal(|ui| {
let edit_w = (ui.available_width() - 80.0 - 60.0 - ui.spacing().item_spacing.x * 2.0).max(60.0);
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(edit_w));
if squared_button_sized(ui, "Browse", false, accent, Some(egui::vec2(76.0, 26.0))) {
pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone());
}
if squared_button_sized(ui, "Open", false, accent, Some(egui::vec2(56.0, 26.0))) {
open_folder(&ScytheConfig::expand_tilde(&self.output_dir));
}
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new("Container:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
ui.horizontal(|ui| {
let _ = squared_button(ui, "mp4", true, accent);
let _ = squared_button(ui, "mkv", false, accent);
});
});
ui.add_space(16.0);
ui.vertical(|ui| {
ui.label(egui::RichText::new("Replay duration in seconds:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
numeric_input_box(ui, &mut self.replay_sec_input_str, &mut self.replay_sec, 5, 1800, 10, "sec", accent);
});
});
let mins = self.replay_sec / 60;
let secs = self.replay_sec % 60;
let est_mb = ((self.replay_sec as f64) * (self.bitrate_mbps as f64 * 1000.0)) / 8192.0;
ui.add_space(4.0);
ui.add_space(8.0);
ui.label(
egui::RichText::new(format!("Duration: {}m {:02}s | Estimated RAM footprint: {:.1} MB", mins, secs, est_mb))
egui::RichText::new(format!("Estimated video max file size in RAM: {:.2}MB. (Duration: {:02}h:{:02}m:{:02}s)", est_mb, mins / 60, mins % 60, secs))
.size(11.0)
.color(Color32::from_rgb(150, 155, 165)),
);
ui.label(
egui::RichText::new("Change video bitrate or replay duration to change file size.")
.size(10.0)
.color(Color32::from_rgb(115, 120, 130)),
);
});
ui.add_space(10.0);
render_section_card(ui, "Replay Video Parameters", accent, |ui| {
ui.label(egui::RichText::new("Target framerate (15 - 360 FPS):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
render_section_card(ui, "General", accent, |ui| {
ui.label(egui::RichText::new("Where should temporary replay data be stored?").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
numeric_input_box(ui, &mut self.fps_input_str, &mut self.target_fps, 15, 360, 5, "FPS", accent);
ui.horizontal(|ui| {
if squared_button(ui, "RAM", self.replay_storage == "ram", accent) {
self.replay_storage = "ram".to_string();
}
if squared_button(ui, "Disk (Not recommended on SSDs)", self.replay_storage == "disk", accent) {
self.replay_storage = "disk".to_string();
}
});
ui.add_space(8.0);
ui.label(egui::RichText::new("Replay bitrate (5 - 150 Mbps):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Save video in a folder based on the games name").size(11.5).color(Color32::WHITE));
});
ui.add_space(4.0);
numeric_input_box(ui, &mut self.bitrate_input_str, &mut self.bitrate_mbps, 5, 150, 5, "Mbps", accent);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Name the video file after the game").size(11.5).color(Color32::WHITE));
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Restart replay on save").size(11.5).color(Color32::WHITE));
});
ui.add_space(4.0);
ui.horizontal(|ui| {
let mut lp = self.record_low_power;
if toggle_switch(ui, &mut lp, accent).changed() {
self.record_low_power = lp;
}
ui.label(egui::RichText::new("Record in low-power mode").size(11.5).color(Color32::WHITE));
});
});
ui.add_space(10.0);
render_section_card(ui, "Video parameters", accent, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new("Frame rate:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
numeric_input_box(ui, &mut self.fps_input_str, &mut self.target_fps, 15, 360, 5, "FPS", accent);
});
ui.add_space(20.0);
ui.vertical(|ui| {
ui.label(egui::RichText::new("Video bitrate (Kbps):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
numeric_input_box(ui, &mut self.bitrate_input_str, &mut self.bitrate_mbps, 5, 150, 5, "Mbps", accent);
});
});
ui.add_space(8.0);
ui.label(egui::RichText::new("Encoder Codec:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.label(egui::RichText::new("Video codec:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
ui.horizontal(|ui| {
for (k, label) in [("h264", "H.264"), ("hevc", "HEVC (H.265)"), ("av1", "AV1")] {
@ -2061,93 +2235,206 @@ impl ScytheOverlayApp {
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Record mouse cursor in replay").size(12.0).color(Color32::WHITE));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let mut cur = self.show_cursor;
if toggle_switch(ui, &mut cur, accent).changed() {
self.show_cursor = cur;
self.config.show_cursor = cur;
let _ = self.config.save();
async_send_command(Command::ToggleCursor);
self.show_hud_notification("MOUSE CURSOR", if cur { "Visible in recording" } else { "Hidden from recording" }, crate::overlay::ToastIcon::Cursor);
}
});
let mut cur = self.show_cursor;
if toggle_switch(ui, &mut cur, accent).changed() {
self.show_cursor = cur;
self.config.show_cursor = cur;
let _ = self.config.save();
async_send_command(Command::ToggleCursor);
}
ui.label(egui::RichText::new("Record cursor").size(11.5).color(Color32::WHITE));
});
});
ui.add_space(10.0);
render_section_card(ui, "Save Directory", accent, |ui| {
ui.label(egui::RichText::new("Destination folder for saved replay clips:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
render_section_card(ui, "Replay indicator", accent, |ui| {
ui.horizontal(|ui| {
let mut notif_on = self.show_replay_notifications;
if toggle_switch(ui, &mut notif_on, accent).changed() {
self.show_replay_notifications = notif_on;
}
ui.label(egui::RichText::new("Show replay notifications").size(11.5).color(Color32::WHITE));
});
ui.add_space(4.0);
ui.horizontal(|ui| {
let edit_w = (ui.available_width() - 80.0 - 60.0 - ui.spacing().item_spacing.x * 2.0).max(60.0);
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(edit_w));
if squared_button_sized(ui, "Browse", false, accent, Some(egui::vec2(76.0, 26.0))) {
pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone());
}
if squared_button_sized(ui, "Open", false, accent, Some(egui::vec2(56.0, 26.0))) {
open_folder(&ScytheConfig::expand_tilde(&self.output_dir));
}
let mut led_on = false;
let _ = toggle_switch(ui, &mut led_on, accent);
ui.label(egui::RichText::new("Show replay status with scroll lock LED").size(11.5).color(Color32::WHITE));
});
});
ui.add_space(10.0);
render_section_card(ui, "Audio Capture Mode", accent, |ui| {
render_section_card(ui, "Autostart", accent, |ui| {
ui.horizontal(|ui| {
let modes = [("system", "System Audio"), ("mic", "Microphone"), ("both", "Both"), ("muted", "Muted")];
for (idx, (_, m_label)) in modes.iter().enumerate() {
if squared_button(ui, m_label, self.audio_mode_idx == idx, accent) {
self.audio_mode_idx = idx;
}
if squared_button(ui, "Don't turn on replay automatically", !self.autostart_replay, accent) {
self.autostart_replay = false;
}
if squared_button(ui, "Turn on replay when this program starts", self.autostart_replay, accent) {
self.autostart_replay = true;
}
});
ui.add_space(6.0);
ui.horizontal(|ui| {
let mut pwr_only = false;
let _ = toggle_switch(ui, &mut pwr_only, accent);
ui.label(egui::RichText::new("Only turn on replay if a power supply is connected").size(11.5).color(Color32::WHITE));
});
});
}
fn render_tab_record(&mut self, ui: &mut egui::Ui, accent: Color32) {
render_section_card(ui, "Recording Video Parameters", accent, |ui| {
ui.label(egui::RichText::new("Target framerate (15 - 360 FPS):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
numeric_input_box(ui, &mut self.fps_input_str, &mut self.target_fps, 15, 360, 5, "FPS", accent);
ui.add_space(8.0);
ui.label(egui::RichText::new("Recording bitrate (5 - 150 Mbps):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
numeric_input_box(ui, &mut self.bitrate_input_str, &mut self.bitrate_mbps, 5, 150, 5, "Mbps", accent);
ui.add_space(8.0);
ui.label(egui::RichText::new("Encoder Codec:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
render_section_card(ui, "Capture", accent, |ui| {
ui.label(egui::RichText::new("Capture source:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(3.0);
ui.horizontal(|ui| {
for (k, label) in [("h264", "H.264"), ("hevc", "HEVC (H.265)"), ("av1", "AV1")] {
if squared_button(ui, label, self.video_codec == k, accent) {
self.video_codec = k.to_string();
}
if squared_button(ui, "Focused monitor", self.capture_source == "focused", accent) {
self.capture_source = "focused".to_string();
}
if squared_button(ui, "Monitor eDP-1 (1920x1080)", self.capture_source == "monitor", accent) {
self.capture_source = "monitor".to_string();
}
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Record mouse cursor in video").size(12.0).color(Color32::WHITE));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let mut cur = self.show_cursor;
if toggle_switch(ui, &mut cur, accent).changed() {
self.show_cursor = cur;
self.config.show_cursor = cur;
let _ = self.config.save();
async_send_command(Command::ToggleCursor);
self.show_hud_notification("MOUSE CURSOR", if cur { "Visible in recording" } else { "Hidden from recording" }, crate::overlay::ToastIcon::Cursor);
}
});
let mut chg_res = false;
let _ = toggle_switch(ui, &mut chg_res, accent);
ui.label(egui::RichText::new("Change video resolution").size(11.5).color(Color32::WHITE));
});
});
ui.add_space(10.0);
render_section_card(ui, "Save Directory", accent, |ui| {
ui.label(egui::RichText::new("Destination folder for normal recordings:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
render_section_card(ui, "Audio", accent, |ui| {
ui.horizontal(|ui| {
let _ = squared_button(ui, "Add audio track", false, accent);
});
ui.add_space(8.0);
// Inset Audio Track #1 Box
egui::Frame::NONE
.fill(Color32::from_rgb(20, 24, 30))
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 40, 50)))
.corner_radius(CornerRadius::ZERO)
.inner_margin(Margin::same(10_i8))
.show(ui, |ui| {
ui.label(egui::RichText::new("Audio track #1").size(12.0).strong().color(Color32::WHITE));
ui.add_space(6.0);
ui.horizontal(|ui| {
let _ = squared_button(ui, "Add output device", false, accent);
let _ = squared_button(ui, "Add input device", false, accent);
let _ = squared_button(ui, "Add application audio", false, accent);
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Output device: Default output").size(11.5).color(Color32::WHITE));
});
ui.add_space(8.0);
ui.horizontal(|ui| {
let mut rec_all = true;
let _ = toggle_switch(ui, &mut rec_all, accent);
ui.label(egui::RichText::new("Record audio from all applications except the selected ones").size(11.0).color(Color32::from_rgb(200, 205, 215)));
});
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Track title:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
let mut track_t = String::new();
ui.add(egui::TextEdit::singleline(&mut track_t).desired_width(180.0));
});
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Audio codec:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
if squared_button(ui, "Opus (Recommended)", self.config.audio_codec == "opus", accent) {
self.config.audio_codec = "opus".to_string();
}
if squared_button(ui, "AAC", self.config.audio_codec == "aac", accent) {
self.config.audio_codec = "aac".to_string();
}
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Sys Vol:").size(11.5).color(Color32::WHITE));
let mut sv = self.system_volume_pct;
if ui.add(egui::Slider::new(&mut sv, 0..=150).suffix("%")).changed() {
self.system_volume_pct = sv;
}
ui.add_space(10.0);
ui.label(egui::RichText::new("Mic Vol:").size(11.5).color(Color32::WHITE));
let mut mv = self.mic_volume_pct;
if ui.add(egui::Slider::new(&mut mv, 0..=150).suffix("%")).changed() {
self.mic_volume_pct = mv;
}
});
ui.add_space(3.0);
render_vu_meter(ui, self.sys_vu, ui.available_width() - 8.0, 10.0, "SYS");
});
ui.add_space(10.0);
render_section_card(ui, "Video", accent, |ui| {
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Video quality:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
if squared_button(ui, "Constant bitrate (Recommended)", self.video_quality_mode == "cbr", accent) {
self.video_quality_mode = "cbr".to_string();
}
if squared_button(ui, "Variable bitrate", self.video_quality_mode == "vbr", accent) {
self.video_quality_mode = "vbr".to_string();
}
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new("Video bitrate (Kbps):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
numeric_input_box(ui, &mut self.bitrate_input_str, &mut self.bitrate_mbps, 5, 150, 5, "Mbps", accent);
});
ui.add_space(16.0);
ui.vertical(|ui| {
ui.label(egui::RichText::new("Frame rate:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
numeric_input_box(ui, &mut self.fps_input_str, &mut self.target_fps, 15, 360, 5, "FPS", accent);
});
ui.add_space(16.0);
ui.vertical(|ui| {
ui.label(egui::RichText::new("Color range:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
ui.horizontal(|ui| {
if squared_button(ui, "Full", self.color_range == "full", accent) {
self.color_range = "full".to_string();
}
if squared_button(ui, "Limited", self.color_range == "limited", accent) {
self.color_range = "limited".to_string();
}
});
});
});
ui.add_space(8.0);
ui.horizontal(|ui| {
let mut cur = self.show_cursor;
if toggle_switch(ui, &mut cur, accent).changed() {
self.show_cursor = cur;
self.config.show_cursor = cur;
let _ = self.config.save();
async_send_command(Command::ToggleCursor);
}
ui.label(egui::RichText::new("Record cursor").size(11.5).color(Color32::WHITE));
});
});
ui.add_space(10.0);
render_section_card(ui, "File info", accent, |ui| {
ui.label(egui::RichText::new("Directory to save recordings:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
ui.horizontal(|ui| {
let edit_w = (ui.available_width() - 80.0 - 60.0 - ui.spacing().item_spacing.x * 2.0).max(60.0);
@ -2159,48 +2446,33 @@ impl ScytheOverlayApp {
open_folder(&ScytheConfig::expand_tilde(&self.output_dir));
}
});
});
ui.add_space(10.0);
render_section_card(ui, "Audio Capture & Volume Levels", accent, |ui| {
ui.label(egui::RichText::new("Audio source mode:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
ui.horizontal(|ui| {
let modes = [("system", "System Audio"), ("mic", "Microphone"), ("both", "Both"), ("muted", "Muted")];
for (idx, (_, m_label)) in modes.iter().enumerate() {
if squared_button(ui, m_label, self.audio_mode_idx == idx, accent) {
self.audio_mode_idx = idx;
}
}
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("System Audio:").size(12.0).color(Color32::WHITE));
let mut sv = self.system_volume_pct;
if ui.add(egui::Slider::new(&mut sv, 0..=150).suffix("%")).changed() {
self.system_volume_pct = sv;
}
ui.label(egui::RichText::new("Container:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
let _ = squared_button(ui, "mp4", true, accent);
let _ = squared_button(ui, "mkv", false, accent);
});
ui.add_space(2.0);
render_vu_meter(ui, self.sys_vu, ui.available_width() - 8.0, 12.0, "SYS");
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Microphone:").size(12.0).color(Color32::WHITE));
let mut mv = self.mic_volume_pct;
if ui.add(egui::Slider::new(&mut mv, 0..=150).suffix("%")).changed() {
self.mic_volume_pct = mv;
}
});
ui.add_space(2.0);
render_vu_meter(ui, self.mic_vu, ui.available_width() - 8.0, 12.0, "MIC");
});
}
fn render_tab_livestream(&mut self, ui: &mut egui::Ui, accent: Color32) {
render_section_card(ui, "Broadcast Service", accent, |ui| {
render_section_card(ui, "Capture", accent, |ui| {
ui.label(egui::RichText::new("Capture source:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(3.0);
ui.horizontal(|ui| {
let _ = squared_button(ui, "Focused monitor", true, accent);
});
ui.add_space(6.0);
ui.horizontal(|ui| {
let mut chg_res = false;
let _ = toggle_switch(ui, &mut chg_res, accent);
ui.label(egui::RichText::new("Change video resolution").size(11.5).color(Color32::WHITE));
});
});
ui.add_space(10.0);
render_section_card(ui, "Streaming info", accent, |ui| {
let services = ["Twitch", "YouTube", "Kick", "Custom RTMP"];
ui.horizontal(|ui| {
for (idx, &srv) in services.iter().enumerate() {
@ -2217,7 +2489,7 @@ impl ScytheOverlayApp {
});
ui.add_space(8.0);
ui.label(egui::RichText::new("RTMP Server URL:").size(11.5).color(Color32::WHITE));
ui.label(egui::RichText::new("Server URL:").size(11.5).color(Color32::WHITE));
ui.add_space(2.0);
ui.add(egui::TextEdit::singleline(&mut self.stream_url).desired_width(ui.available_width() - 8.0));
@ -2229,15 +2501,96 @@ impl ScytheOverlayApp {
ui.add_space(10.0);
render_section_card(ui, "Stream Quality Parameters", accent, |ui| {
ui.label(egui::RichText::new("Stream framerate (15 - 144 FPS):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
render_section_card(ui, "Video", accent, |ui| {
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Video quality:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
let _ = squared_button(ui, "Constant bitrate (Recommended)", true, accent);
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new("Video bitrate (Kbps):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
numeric_input_box(ui, &mut self.bitrate_input_str, &mut self.bitrate_mbps, 2, 25, 1, "Mbps", accent);
});
ui.add_space(16.0);
ui.vertical(|ui| {
ui.label(egui::RichText::new("Frame rate:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(2.0);
numeric_input_box(ui, &mut self.fps_input_str, &mut self.target_fps, 15, 144, 5, "FPS", accent);
});
});
});
}
fn render_tab_screenshot(&mut self, ui: &mut egui::Ui, accent: Color32) {
render_section_card(ui, "File info", accent, |ui| {
ui.label(egui::RichText::new("Directory to save screenshots:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
numeric_input_box(ui, &mut self.fps_input_str, &mut self.target_fps, 15, 144, 5, "FPS", accent);
ui.horizontal(|ui| {
let edit_w = (ui.available_width() - 80.0 - 60.0 - ui.spacing().item_spacing.x * 2.0).max(60.0);
ui.add(egui::TextEdit::singleline(&mut self.screenshot_dir).desired_width(edit_w));
if squared_button_sized(ui, "Browse", false, accent, Some(egui::vec2(76.0, 26.0))) {
pick_folder(&self.screenshot_dir, self.folder_tx.clone(), self.folder_picking_active.clone());
}
if squared_button_sized(ui, "Open", false, accent, Some(egui::vec2(56.0, 26.0))) {
open_folder(&ScytheConfig::expand_tilde(&self.screenshot_dir));
}
});
ui.add_space(8.0);
ui.label(egui::RichText::new("Stream bitrate (2 - 25 Mbps):").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.label(egui::RichText::new("Image format:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
numeric_input_box(ui, &mut self.bitrate_input_str, &mut self.bitrate_mbps, 2, 25, 1, "Mbps", accent);
ui.horizontal(|ui| {
for (fmt_id, label) in [("png", "PNG (Lossless)"), ("jpg", "JPEG (Compressed)")] {
if squared_button(ui, label, self.screenshot_format.to_lowercase() == fmt_id, accent) {
self.screenshot_format = fmt_id.to_string();
}
}
});
});
ui.add_space(10.0);
render_section_card(ui, "Capture settings", accent, |ui| {
ui.horizontal(|ui| {
let mut cur = self.screenshot_cursor;
if toggle_switch(ui, &mut cur, accent).changed() {
self.screenshot_cursor = cur;
}
ui.label(egui::RichText::new("Capture mouse cursor in screenshot").size(11.5).color(Color32::WHITE));
});
ui.add_space(8.0);
ui.label(egui::RichText::new("Capture source:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
ui.horizontal(|ui| {
for (src_id, label) in [("focused", "Focused monitor"), ("full", "Full desktop")] {
if squared_button(ui, label, self.capture_source == src_id, accent) {
self.capture_source = src_id.to_string();
}
}
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Screenshot shortcut:").size(11.5).color(Color32::WHITE));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(egui::RichText::new(&self.config.screenshot_hotkey).font(FontId::monospace(11.5)).color(accent));
});
});
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new("CLI command:").size(11.5).color(Color32::from_rgb(180, 185, 190)));
ui.add_space(4.0);
ui.label(egui::RichText::new("scythe-ui --screenshot").font(FontId::monospace(11.0)).color(Color32::from_rgb(160, 200, 255)));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if squared_button_sized(ui, "Take Screenshot", true, accent, Some(egui::vec2(130.0, 26.0))) {
self.take_screenshot();
}
});
});
});
}
@ -2298,8 +2651,8 @@ impl ScytheOverlayApp {
Color32::from_rgb(225, 225, 230)
};
ui.painter().rect_filled(rect, CornerRadius::same(5), bg);
ui.painter().rect_stroke(rect, CornerRadius::same(5), stroke, egui::StrokeKind::Inside);
ui.painter().rect_filled(rect, CornerRadius::ZERO, bg);
ui.painter().rect_stroke(rect, CornerRadius::ZERO, stroke, egui::StrokeKind::Inside);
ui.painter().text(
rect.center(),
egui::Align2::CENTER_CENTER,