fix(shell): coordinate startup cover and tab stills
This commit is contained in:
parent
01f2ec0417
commit
5a2e959763
13 changed files with 442 additions and 52 deletions
|
|
@ -461,6 +461,12 @@ mod linux {
|
|||
|
||||
/// The event carrying a tab's last frame to the shell ([`capture_still`]).
|
||||
pub const EVENT_TAB_STILL: &str = "tab-still";
|
||||
#[cfg(windows)]
|
||||
const EVENT_TAB_STILL_START: &str = "tab-still-start";
|
||||
#[cfg(windows)]
|
||||
const EVENT_TAB_STILL_CANCEL: &str = "tab-still-cancel";
|
||||
#[cfg(windows)]
|
||||
const EVENT_TAB_STILL_RESET: &str = "tab-still-reset";
|
||||
|
||||
#[cfg(windows)]
|
||||
#[derive(Clone, Serialize)]
|
||||
|
|
@ -468,18 +474,114 @@ struct TabStill {
|
|||
id: String,
|
||||
/// `data:image/jpeg;base64,…`
|
||||
image: String,
|
||||
token: u64,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[derive(Clone, Serialize)]
|
||||
struct TabStillToken { id: String, token: u64 }
|
||||
|
||||
#[cfg(windows)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum StillPhase { Pending, Hidden }
|
||||
|
||||
#[cfg(windows)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct StillEntry { token: u64, phase: StillPhase }
|
||||
|
||||
#[cfg(windows)]
|
||||
impl StillEntry {
|
||||
fn accepts(self, token: u64) -> bool { self.token == token }
|
||||
fn hide(&mut self, token: u64) -> bool {
|
||||
if !self.accepts(token) || self.phase != StillPhase::Pending { return false; }
|
||||
self.phase = StillPhase::Hidden;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, windows))]
|
||||
mod still_tests {
|
||||
use super::{cancel_pending_still, pending_stills, StillEntry, StillPhase};
|
||||
|
||||
#[test]
|
||||
fn timeout_keeps_late_frame_valid_but_stale_tokens_cannot_hide() {
|
||||
let mut entry = StillEntry { token: 7, phase: StillPhase::Pending };
|
||||
assert!(!entry.hide(6));
|
||||
assert!(entry.hide(7));
|
||||
assert!(entry.accepts(7)); // CapturePreview may finish after the bounded hide.
|
||||
assert!(!entry.hide(7)); // A late acknowledgement must not hide a shown tab again.
|
||||
entry = StillEntry { token: 8, phase: StillPhase::Pending };
|
||||
assert!(!entry.accepts(7));
|
||||
assert!(!entry.hide(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn teardown_invalidates_old_capture_before_same_label_is_reused() {
|
||||
let label = "tab-still-test-recreated";
|
||||
pending_stills().lock().unwrap().insert(label.into(), StillEntry { token: 31, phase: StillPhase::Hidden });
|
||||
assert_eq!(cancel_pending_still(label), Some(31));
|
||||
pending_stills().lock().unwrap().insert(label.into(), StillEntry { token: 32, phase: StillPhase::Pending });
|
||||
let current = pending_stills().lock().unwrap().get(label).copied().unwrap();
|
||||
assert!(!current.accepts(31));
|
||||
assert!(current.accepts(32));
|
||||
cancel_pending_still(label);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn pending_stills() -> &'static std::sync::Mutex<std::collections::HashMap<String, StillEntry>> {
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
static PENDING: OnceLock<Mutex<std::collections::HashMap<String, StillEntry>>> = OnceLock::new();
|
||||
PENDING.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn cancel_pending_still(label: &str) -> Option<u64> {
|
||||
pending_stills().lock().unwrap_or_else(|poisoned| poisoned.into_inner()).remove(label).map(|entry| entry.token)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn hide_after_still<R: Runtime>(app: &AppHandle<R>, label: &str, token: u64) {
|
||||
let app = app.clone();
|
||||
let label = label.to_owned();
|
||||
let dispatcher = app.clone();
|
||||
// Webview::show/hide both execute on Tauri's main thread. Check the token there, directly
|
||||
// before hiding, so a queued hide cannot run after a newer show canceled this capture.
|
||||
let _ = dispatcher.run_on_main_thread(move || {
|
||||
let current = {
|
||||
let mut pending = pending_stills().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
pending.get_mut(&label).is_some_and(|entry| entry.hide(token))
|
||||
};
|
||||
if current {
|
||||
if let Some(tab) = app.get_webview(&label) { let _ = tab.hide(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The shell has painted a captured frame; complete the bounded overlay handoff.
|
||||
#[cfg(windows)]
|
||||
pub fn acknowledge_still<R: Runtime>(app: &AppHandle<R>, id: &str, token: u64) {
|
||||
if policy::valid_tab_id(id) {
|
||||
hide_after_still(app, &policy::tab_label(id), token);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn acknowledge_still<R: Runtime>(_app: &AppHandle<R>, _id: &str, _token: u64) {}
|
||||
|
||||
/// Windows: a tab webview about to be hidden hands the shell its last frame (WebView2
|
||||
/// `CapturePreview`, JPEG) as [`EVENT_TAB_STILL`], which its pane shows while no webview is on
|
||||
/// screen there. Only a webview that is on screen now; requested before the hide, on the same
|
||||
/// thread, so the frame is still there. Kept in the shell's memory only. No-op elsewhere.
|
||||
fn capture_still<R: Runtime>(webview: &Webview<R>) {
|
||||
fn capture_still<R: Runtime>(webview: &Webview<R>, token: u64) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let Some(id) = policy::tab_id(webview.label()).map(str::to_owned) else { return };
|
||||
let label = webview.label().to_owned();
|
||||
let app = webview.app_handle().clone();
|
||||
let _ = webview.with_webview(move |platform| {
|
||||
let fallback = app.clone();
|
||||
let fallback_label = label.clone();
|
||||
let result = webview.with_webview(move |platform| {
|
||||
use base64::Engine;
|
||||
use webview2_com::CapturePreviewCompletedHandler;
|
||||
use webview2_com::Microsoft::Web::WebView2::Win32::COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_JPEG;
|
||||
|
|
@ -491,41 +593,82 @@ fn capture_still<R: Runtime>(webview: &Webview<R>) {
|
|||
let controller = platform.controller();
|
||||
let mut shown = windows::core::BOOL::default();
|
||||
if controller.IsVisible(&mut shown).is_err() || !shown.as_bool() {
|
||||
hide_after_still(&app, &label, token);
|
||||
return;
|
||||
}
|
||||
let (Ok(core), Some(stream)) = (controller.CoreWebView2(), SHCreateMemStream(None)) else { return };
|
||||
let (Ok(core), Some(stream)) = (controller.CoreWebView2(), SHCreateMemStream(None)) else {
|
||||
hide_after_still(&app, &label, token);
|
||||
return;
|
||||
};
|
||||
let target = stream.clone();
|
||||
let callback_app = app.clone();
|
||||
let callback_label = label.clone();
|
||||
let handler = CapturePreviewCompletedHandler::create(Box::new(move |result| {
|
||||
result?;
|
||||
let mut size = 0u64;
|
||||
target.Seek(0, STREAM_SEEK_END, Some(&mut size))?;
|
||||
if size == 0 || size > 16 << 20 {
|
||||
return Ok(());
|
||||
let image = (|| {
|
||||
result.ok()?;
|
||||
let mut size = 0u64;
|
||||
target.Seek(0, STREAM_SEEK_END, Some(&mut size)).ok()?;
|
||||
if size == 0 || size > 16 << 20 { return None; }
|
||||
target.Seek(0, STREAM_SEEK_SET, None).ok()?;
|
||||
let mut bytes = vec![0u8; size as usize];
|
||||
let mut read = 0u32;
|
||||
target.Read(bytes.as_mut_ptr().cast(), size as u32, Some(&mut read)).ok().ok()?;
|
||||
bytes.truncate(read as usize);
|
||||
Some(format!("data:image/jpeg;base64,{}", base64::engine::general_purpose::STANDARD.encode(bytes)))
|
||||
})();
|
||||
if let Some(image) = image {
|
||||
let current = pending_stills().lock().unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.get(&callback_label).is_some_and(|entry| entry.accepts(token));
|
||||
if current {
|
||||
emit_shell(&callback_app, EVENT_TAB_STILL, TabStill { id, image, token });
|
||||
}
|
||||
} else {
|
||||
hide_after_still(&callback_app, &callback_label, token);
|
||||
}
|
||||
target.Seek(0, STREAM_SEEK_SET, None)?;
|
||||
let mut bytes = vec![0u8; size as usize];
|
||||
let mut read = 0u32;
|
||||
target.Read(bytes.as_mut_ptr().cast(), size as u32, Some(&mut read)).ok()?;
|
||||
bytes.truncate(read as usize);
|
||||
let image = format!("data:image/jpeg;base64,{}", base64::engine::general_purpose::STANDARD.encode(bytes));
|
||||
emit_shell(&app, EVENT_TAB_STILL, TabStill { id, image });
|
||||
Ok(())
|
||||
}));
|
||||
let _ = core.CapturePreview(COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_JPEG, &stream, &handler);
|
||||
if core.CapturePreview(COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_JPEG, &stream, &handler).is_err() {
|
||||
hide_after_still(&app, &label, token);
|
||||
}
|
||||
}
|
||||
});
|
||||
if result.is_err() {
|
||||
hide_after_still(&fallback, &fallback_label, token);
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
let _ = webview;
|
||||
let _ = (webview, token);
|
||||
}
|
||||
|
||||
/// Shows or hides a tab webview and sets its WebView2 memory target to match
|
||||
/// ([`tabs::memory_target`]): hidden tabs run at `COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL_LOW`
|
||||
/// (ICoreWebView2_19, WebView2 Runtime 114+; older runtimes just skip it). Other platforms only
|
||||
/// show/hide.
|
||||
pub fn set_tab_visible<R: Runtime>(webview: &Webview<R>, visible: bool, parked: bool) {
|
||||
if !visible {
|
||||
capture_still(webview);
|
||||
pub fn set_tab_visible<R: Runtime>(webview: &Webview<R>, visible: bool, parked: bool, was_visible: bool) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let app = webview.app_handle();
|
||||
let id = policy::tab_id(webview.label()).unwrap_or_default().to_owned();
|
||||
if let Some(token) = cancel_pending_still(webview.label()) {
|
||||
emit_shell(app, EVENT_TAB_STILL_CANCEL, TabStillToken { id: id.clone(), token });
|
||||
}
|
||||
if !visible && !parked && was_visible && webview.window().is_visible().unwrap_or(false) {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static NEXT: AtomicU64 = AtomicU64::new(1);
|
||||
let token = NEXT.fetch_add(1, Ordering::Relaxed);
|
||||
let label = webview.label().to_owned();
|
||||
pending_stills().lock().unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.insert(label.clone(), StillEntry { token, phase: StillPhase::Pending });
|
||||
emit_shell(app, EVENT_TAB_STILL_START, TabStillToken { id, token });
|
||||
let app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(160)).await;
|
||||
hide_after_still(&app, &label, token);
|
||||
});
|
||||
capture_still(webview, token);
|
||||
set_memory_target(webview, visible, parked);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = if visible { webview.show() } else { webview.hide() };
|
||||
set_memory_target(webview, visible, parked);
|
||||
|
|
@ -577,20 +720,33 @@ fn placements() -> std::sync::MutexGuard<'static, crate::layout::Placements> {
|
|||
/// native calls for whatever did not change since its last placement: a sync that moves one pane
|
||||
/// touches one webview, and one that changes nothing costs nothing.
|
||||
pub fn place_tab<R: Runtime>(webview: &Webview<R>, rect: crate::layout::Rect, visible: bool, parked: bool) {
|
||||
let changed = placements().place(webview.label(), rect, visible);
|
||||
let (changed, was_visible) = {
|
||||
let mut placed = placements();
|
||||
let was_visible = placed.was_visible(webview.label());
|
||||
(placed.place(webview.label(), rect, visible), was_visible)
|
||||
};
|
||||
if changed.bounds {
|
||||
let (x, y, width, height) = rect;
|
||||
place_child(webview, x, y, width, height);
|
||||
}
|
||||
if changed.visible {
|
||||
set_tab_visible(webview, visible, parked);
|
||||
set_tab_visible(webview, visible, parked, was_visible);
|
||||
} else {
|
||||
set_memory_target(webview, visible, parked);
|
||||
}
|
||||
}
|
||||
|
||||
/// A tab webview was closed: a new one with the same label is placed from scratch.
|
||||
pub fn forget_tab(label: &str) {
|
||||
pub fn forget_tab<R: Runtime>(app: &AppHandle<R>, label: &str) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
cancel_pending_still(label);
|
||||
if let Some(id) = policy::tab_id(label) {
|
||||
emit_shell(app, EVENT_TAB_STILL_RESET, id.to_owned());
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
let _ = app;
|
||||
placements().forget(label);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue