fix(windows): resolve DWM transparency, eliminate white flash, fix capture staging, and terminate locked processes in installer
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions
This commit is contained in:
parent
fbfe6f2360
commit
5b4797ceff
7 changed files with 131 additions and 34 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3700,7 +3700,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scythe"
|
name = "scythe"
|
||||||
version = "0.2.2"
|
version = "0.2.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ashpd",
|
"ashpd",
|
||||||
"cpal",
|
"cpal",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "scythe"
|
name = "scythe"
|
||||||
version = "0.2.2"
|
version = "0.2.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
@ -45,9 +45,11 @@ windows = { version = "0.58.0", features = [
|
||||||
"Win32_Graphics_Direct3D11",
|
"Win32_Graphics_Direct3D11",
|
||||||
"Win32_Graphics_Dxgi",
|
"Win32_Graphics_Dxgi",
|
||||||
"Win32_Graphics_Dxgi_Common",
|
"Win32_Graphics_Dxgi_Common",
|
||||||
|
"Win32_Graphics_Dwm",
|
||||||
"Win32_Graphics_Gdi",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
"Win32_UI_Controls",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
] }
|
] }
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ RequestExecutionLevel user
|
||||||
!insertmacro MUI_LANGUAGE "English"
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
Section "Scythe" SecMain
|
Section "Scythe" SecMain
|
||||||
|
; Terminate any running instances so locked executables and DLLs can be updated cleanly
|
||||||
|
nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-ui.exe /T >nul 2>&1'
|
||||||
|
nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-daemon.exe /T >nul 2>&1'
|
||||||
|
Sleep 500
|
||||||
|
|
||||||
|
SetOverwrite on
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
File /r "..\..\dist\bundle\*.*"
|
File /r "..\..\dist\bundle\*.*"
|
||||||
|
|
||||||
|
|
@ -43,6 +49,10 @@ Section "Scythe" SecMain
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section "Uninstall"
|
Section "Uninstall"
|
||||||
|
nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-ui.exe /T >nul 2>&1'
|
||||||
|
nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-daemon.exe /T >nul 2>&1'
|
||||||
|
Sleep 500
|
||||||
|
|
||||||
Delete "$DESKTOP\scythe.lnk"
|
Delete "$DESKTOP\scythe.lnk"
|
||||||
RMDir /r "$SMPROGRAMS\scythe"
|
RMDir /r "$SMPROGRAMS\scythe"
|
||||||
RMDir /r "$INSTDIR"
|
RMDir /r "$INSTDIR"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ SilentInstall silent
|
||||||
RequestExecutionLevel user
|
RequestExecutionLevel user
|
||||||
|
|
||||||
Section
|
Section
|
||||||
|
; Terminate any running instances so locked executables and DLLs can be updated cleanly
|
||||||
|
nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-ui.exe /T >nul 2>&1'
|
||||||
|
nsExec::Exec 'cmd.exe /C taskkill /F /IM scythe-daemon.exe /T >nul 2>&1'
|
||||||
|
Sleep 500
|
||||||
|
|
||||||
|
SetOverwrite on
|
||||||
; Extract self-contained binaries and all runtime DLLs directly to user app data
|
; Extract self-contained binaries and all runtime DLLs directly to user app data
|
||||||
SetOutPath "$LOCALAPPDATA\scythe"
|
SetOutPath "$LOCALAPPDATA\scythe"
|
||||||
File /r "..\..\dist\bundle\*.*"
|
File /r "..\..\dist\bundle\*.*"
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,10 @@ pub struct WindowsCapture {
|
||||||
device: ID3D11Device,
|
device: ID3D11Device,
|
||||||
context: ID3D11DeviceContext,
|
context: ID3D11DeviceContext,
|
||||||
duplication: IDXGIOutputDuplication,
|
duplication: IDXGIOutputDuplication,
|
||||||
gpu_texture: ID3D11Texture2D,
|
staging_texture: ID3D11Texture2D,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
|
last_frame: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
@ -85,33 +86,34 @@ impl WindowsCapture {
|
||||||
// Initialize Desktop Duplication
|
// Initialize Desktop Duplication
|
||||||
let duplication = output1.DuplicateOutput(&device)?;
|
let duplication = output1.DuplicateOutput(&device)?;
|
||||||
|
|
||||||
// Allocate a dedicated VRAM-resident GPU texture for 100% zero-copy capture
|
// Allocate a CPU-accessible staging texture for hardware frame reading
|
||||||
let gpu_desc = D3D11_TEXTURE2D_DESC {
|
let staging_desc = D3D11_TEXTURE2D_DESC {
|
||||||
Width: width,
|
Width: width,
|
||||||
Height: height,
|
Height: height,
|
||||||
MipLevels: 1,
|
MipLevels: 1,
|
||||||
ArraySize: 1,
|
ArraySize: 1,
|
||||||
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||||
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
|
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
|
||||||
Usage: D3D11_USAGE_DEFAULT,
|
Usage: D3D11_USAGE_STAGING,
|
||||||
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
BindFlags: 0,
|
||||||
CPUAccessFlags: 0,
|
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
|
||||||
MiscFlags: 0,
|
MiscFlags: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut gpu_texture: Option<ID3D11Texture2D> = None;
|
let mut staging_texture: Option<ID3D11Texture2D> = None;
|
||||||
device.CreateTexture2D(&gpu_desc, None, Some(&mut gpu_texture))?;
|
device.CreateTexture2D(&staging_desc, None, Some(&mut staging_texture))?;
|
||||||
let gpu_texture = gpu_texture.ok_or("Failed to create GPU VRAM texture")?;
|
let staging_texture = staging_texture.ok_or("Failed to create D3D11 staging texture")?;
|
||||||
|
|
||||||
println!("Windows DXGI Hardware Capture active (Pure GPU Zero-Copy): {}x{}", width, height);
|
println!("Windows DXGI Hardware Desktop Duplication active: {}x{}", width, height);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
device,
|
device,
|
||||||
context,
|
context,
|
||||||
duplication,
|
duplication,
|
||||||
gpu_texture,
|
staging_texture,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
last_frame: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,32 +145,51 @@ impl FrameSource for WindowsCapture {
|
||||||
let mut desktop_resource: Option<IDXGIResource> = None;
|
let mut desktop_resource: Option<IDXGIResource> = None;
|
||||||
|
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
match self.duplication.AcquireNextFrame(150, &mut frame_info, &mut desktop_resource) {
|
match self.duplication.AcquireNextFrame(100, &mut frame_info, &mut desktop_resource) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Some(resource) = desktop_resource {
|
if let Some(resource) = desktop_resource {
|
||||||
let texture: ID3D11Texture2D = resource.cast()?;
|
let texture: ID3D11Texture2D = resource.cast()?;
|
||||||
|
|
||||||
// Copy directly on the GPU from desktop buffer to persistent VRAM texture
|
// Copy GPU desktop texture to staging texture accessible by CPU
|
||||||
self.context.CopyResource(&self.gpu_texture, &texture);
|
self.context.CopyResource(&self.staging_texture, &texture);
|
||||||
|
|
||||||
// Immediately release the desktop frame back to the DWM compositor
|
// Immediately release the desktop frame back to the DWM compositor
|
||||||
let _ = self.duplication.ReleaseFrame();
|
let _ = self.duplication.ReleaseFrame();
|
||||||
|
|
||||||
return Ok(Frame::D3D11Texture {
|
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||||
handle: self.gpu_texture.as_raw() as usize,
|
self.context.Map(&self.staging_texture, 0, D3D11_MAP_READ, 0, Some(&mut mapped))?;
|
||||||
|
|
||||||
|
let stride = mapped.RowPitch as usize;
|
||||||
|
let total_bytes = stride * self.height as usize;
|
||||||
|
if self.last_frame.len() != total_bytes {
|
||||||
|
self.last_frame.resize(total_bytes, 0);
|
||||||
|
}
|
||||||
|
std::ptr::copy_nonoverlapping(
|
||||||
|
mapped.pData as *const u8,
|
||||||
|
self.last_frame.as_mut_ptr(),
|
||||||
|
total_bytes,
|
||||||
|
);
|
||||||
|
self.context.Unmap(&self.staging_texture, 0);
|
||||||
|
|
||||||
|
return Ok(Frame::Raw {
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
|
stride: stride as u32,
|
||||||
|
data: self.last_frame.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let _ = self.duplication.ReleaseFrame();
|
let _ = self.duplication.ReleaseFrame();
|
||||||
}
|
}
|
||||||
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
||||||
|
if !self.last_frame.is_empty() {
|
||||||
|
return Ok(Frame::Raw {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
stride: self.width * 4,
|
||||||
|
data: self.last_frame.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(8));
|
std::thread::sleep(std::time::Duration::from_millis(8));
|
||||||
return Ok(Frame::D3D11Texture {
|
|
||||||
handle: self.gpu_texture.as_raw() as usize,
|
|
||||||
width: self.width,
|
|
||||||
height: self.height,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
|
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
|
||||||
println!("DXGI access lost (display mode or fullscreen switch), reacquiring...");
|
println!("DXGI access lost (display mode or fullscreen switch), reacquiring...");
|
||||||
|
|
@ -182,11 +203,21 @@ impl FrameSource for WindowsCapture {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Frame::D3D11Texture {
|
if !self.last_frame.is_empty() {
|
||||||
handle: self.gpu_texture.as_raw() as usize,
|
Ok(Frame::Raw {
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
})
|
stride: self.width * 4,
|
||||||
|
data: self.last_frame.clone(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(Frame::Raw {
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
stride: self.width * 4,
|
||||||
|
data: vec![0u8; (self.width * self.height * 4) as usize],
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,8 @@ pub fn parse_hotkey(s: &str) -> Option<HotKey> {
|
||||||
"delete" | "del" => Some(Code::Delete),
|
"delete" | "del" => Some(Code::Delete),
|
||||||
"home" => Some(Code::Home),
|
"home" => Some(Code::Home),
|
||||||
"end" => Some(Code::End),
|
"end" => Some(Code::End),
|
||||||
"pageup" | "pgup" => Some(Code::PageUp),
|
"pageup" | "pgup" | "page_up" => Some(Code::PageUp),
|
||||||
"pagedown" | "pgdn" => Some(Code::PageDown),
|
"pagedown" | "pgdn" | "page_down" => Some(Code::PageDown),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -912,7 +912,7 @@ impl ScytheOverlayApp {
|
||||||
self.output_dir = new_dir.clone();
|
self.output_dir = new_dir.clone();
|
||||||
self.config.output_directory = new_dir;
|
self.config.output_directory = new_dir;
|
||||||
let _ = self.config.save();
|
let _ = self.config.save();
|
||||||
let _ = ipc::send_command(Command::ReloadConfig);
|
async_send_command(Command::ReloadConfig);
|
||||||
self.refresh_clips();
|
self.refresh_clips();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2130,18 +2130,21 @@ impl eframe::App for ScytheOverlayApp {
|
||||||
if self.listening_keybind.is_some() || self.hud_notification.is_some() {
|
if self.listening_keybind.is_some() || self.hud_notification.is_some() {
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
} else {
|
} else {
|
||||||
ctx.request_repaint_after(Duration::from_millis(33));
|
ctx.request_repaint_after(Duration::from_millis(16));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-position and size to monitor on launch
|
// Auto-position, DWM transparency, and size to monitor on launch
|
||||||
if !self.initial_pos_set {
|
if !self.initial_pos_set {
|
||||||
if let Some(monitor_size) = ctx.input(|i| i.viewport().monitor_size) {
|
if let Some(monitor_size) = ctx.input(|i| i.viewport().monitor_size) {
|
||||||
if monitor_size.x > 100.0 && monitor_size.y > 100.0 {
|
if monitor_size.x > 100.0 && monitor_size.y > 100.0 {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0)));
|
ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0)));
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(monitor_size));
|
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(monitor_size));
|
||||||
self.initial_pos_set = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
apply_windows_transparency("Scythe");
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
|
||||||
|
self.initial_pos_set = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle interactive keybind recording mode
|
// Handle interactive keybind recording mode
|
||||||
|
|
@ -2155,6 +2158,10 @@ impl eframe::App for ScytheOverlayApp {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Some(key_name) = format_egui_key(*key) {
|
if let Some(key_name) = format_egui_key(*key) {
|
||||||
|
let k_lower = key_name.to_lowercase();
|
||||||
|
if ["alt", "ctrl", "control", "shift", "super", "meta", "command"].contains(&k_lower.as_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
if modifiers.ctrl {
|
if modifiers.ctrl {
|
||||||
parts.push("Ctrl");
|
parts.push("Ctrl");
|
||||||
|
|
@ -2259,6 +2266,27 @@ impl eframe::App for ScytheOverlayApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn apply_windows_transparency(title: &str) {
|
||||||
|
unsafe {
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::FindWindowW;
|
||||||
|
use windows::Win32::Graphics::Dwm::DwmExtendFrameIntoClientArea;
|
||||||
|
use windows::Win32::UI::Controls::MARGINS;
|
||||||
|
use windows::core::HSTRING;
|
||||||
|
|
||||||
|
let htitle = HSTRING::from(title);
|
||||||
|
if let Ok(hwnd) = FindWindowW(None, &htitle) {
|
||||||
|
let margins = MARGINS {
|
||||||
|
cxLeftWidth: -1,
|
||||||
|
cxRightWidth: -1,
|
||||||
|
cyTopHeight: -1,
|
||||||
|
cyBottomHeight: -1,
|
||||||
|
};
|
||||||
|
let _ = DwmExtendFrameIntoClientArea(hwnd, &margins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run_egui_overlay() {
|
pub fn run_egui_overlay() {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let (screen_w, screen_h): (f32, f32) = unsafe {
|
let (screen_w, screen_h): (f32, f32) = unsafe {
|
||||||
|
|
@ -2280,6 +2308,7 @@ pub fn run_egui_overlay() {
|
||||||
.with_resizable(false)
|
.with_resizable(false)
|
||||||
.with_decorations(false)
|
.with_decorations(false)
|
||||||
.with_transparent(true)
|
.with_transparent(true)
|
||||||
|
.with_visible(false)
|
||||||
.with_always_on_top(),
|
.with_always_on_top(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
@ -2300,6 +2329,7 @@ pub struct ShadowPlayToastApp {
|
||||||
accent: Color32,
|
accent: Color32,
|
||||||
created_at: Instant,
|
created_at: Instant,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
|
initial_setup: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for ShadowPlayToastApp {
|
impl eframe::App for ShadowPlayToastApp {
|
||||||
|
|
@ -2308,6 +2338,13 @@ impl eframe::App for ShadowPlayToastApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
if !self.initial_setup {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
apply_windows_transparency("Scythe Notification");
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
|
||||||
|
self.initial_setup = true;
|
||||||
|
}
|
||||||
|
|
||||||
let elapsed = self.created_at.elapsed().as_secs_f32();
|
let elapsed = self.created_at.elapsed().as_secs_f32();
|
||||||
let total_dur = self.duration.as_secs_f32();
|
let total_dur = self.duration.as_secs_f32();
|
||||||
if elapsed >= total_dur {
|
if elapsed >= total_dur {
|
||||||
|
|
@ -2427,6 +2464,15 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc
|
||||||
let toast_w: f32 = 340.0;
|
let toast_w: f32 = 340.0;
|
||||||
let toast_h: f32 = 64.0;
|
let toast_h: f32 = 64.0;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
unsafe {
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::{FindWindowW, PostMessageW, WM_CLOSE};
|
||||||
|
if let Ok(prev) = FindWindowW(None, windows::core::w!("Scythe Notification")) {
|
||||||
|
let _ = PostMessageW(prev, WM_CLOSE, windows::Win32::Foundation::WPARAM(0), windows::Win32::Foundation::LPARAM(0));
|
||||||
|
std::thread::sleep(Duration::from_millis(30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let screen_w: f32 = unsafe {
|
let screen_w: f32 = unsafe {
|
||||||
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN};
|
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN};
|
||||||
|
|
@ -2447,6 +2493,7 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc
|
||||||
.with_inner_size([toast_w, toast_h])
|
.with_inner_size([toast_w, toast_h])
|
||||||
.with_decorations(false)
|
.with_decorations(false)
|
||||||
.with_transparent(true)
|
.with_transparent(true)
|
||||||
|
.with_visible(false)
|
||||||
.with_always_on_top()
|
.with_always_on_top()
|
||||||
.with_resizable(false),
|
.with_resizable(false),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -2459,6 +2506,7 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc
|
||||||
accent,
|
accent,
|
||||||
created_at: Instant::now(),
|
created_at: Instant::now(),
|
||||||
duration: Duration::from_millis(2800),
|
duration: Duration::from_millis(2800),
|
||||||
|
initial_setup: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watchdog thread to guarantee exit after duration
|
// Watchdog thread to guarantee exit after duration
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue