feat(ui): complete overhaul matching GPU Screen Recorder UI screenshots with screenshot feature
This commit is contained in:
parent
8831c501a1
commit
f9c7ab772e
4 changed files with 652 additions and 180 deletions
|
|
@ -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, ¤t_config, &mut registered_hotkeys);
|
||||
let mut current_ids = register_all(&manager, ¤t_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, ¤t_config, &mut registered_hotkeys);
|
||||
id_menu = im;
|
||||
id_save = is;
|
||||
id_rec = ir;
|
||||
id_cur = ic;
|
||||
current_ids = register_all(&manager, ¤t_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")]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue