host: raise_shell on Linux and macOS; tab stills before a hide (Windows)
Linux: the shell becomes a GtkOverlay layer itself (an empty box is the base) and is re-ordered to the top or bottom of the overlay; a tab placed while it is raised goes under it. macOS: the shell's WKWebView is re-ordered among its superview's subviews (addSubview:positioned:relativeTo:), keeping the first responder if it was in the shell. Both now report support, so overlays no longer hide the tabs there either (the app makes the shell webview transparent; macOS needs tauri's macos-private-api). Windows: a tab webview about to be hidden hands the shell its last frame (WebView2 CapturePreview, JPEG, `tab-still` event), for its pane to show instead of an outline while no webview is on screen there.
This commit is contained in:
parent
5954aa94ab
commit
b5bef3463b
3 changed files with 140 additions and 8 deletions
|
|
@ -285,9 +285,11 @@ pub fn attach_shell<R: Runtime>(shell: &Webview<R>) {
|
|||
/// only: there `Webview::position` is not the placement (always 0,0 outside X11 child windows).
|
||||
/// Puts the shell webview above every tab webview (`on_top`), so shell menus, dialogs and drag
|
||||
/// zones draw over the panes while the panes stay on screen (the shell page must then be
|
||||
/// transparent where the panes are), or back below them. Returns whether this platform can:
|
||||
/// Windows only (WebView2 hosts each webview in its own child window, re-ordered with
|
||||
/// `SetWindowPos`; no activation or focus change). Elsewhere the caller hides the tabs instead.
|
||||
/// transparent where the panes are), or back below them. Returns whether this platform can.
|
||||
/// Windows: WebView2 hosts each webview in its own child window, re-ordered with `SetWindowPos`
|
||||
/// (no activation or focus change). Linux: the shell's place among the GtkOverlay's layers
|
||||
/// ([`attach_shell`]). macOS: the shell's WKWebView re-ordered among its superview's subviews
|
||||
/// (first responder kept). Elsewhere the caller hides the tabs instead.
|
||||
pub fn raise_shell<R: Runtime>(shell: &Webview<R>, on_top: bool) -> bool {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
|
|
@ -316,13 +318,52 @@ pub fn raise_shell<R: Runtime>(shell: &Webview<R>, on_top: bool) -> bool {
|
|||
});
|
||||
true
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
linux::raise_shell(shell, on_top);
|
||||
true
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
macos::raise_shell(shell, on_top);
|
||||
true
|
||||
}
|
||||
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
let _ = (shell, on_top);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use objc2_app_kit::{NSResponder, NSView, NSWindowOrderingMode};
|
||||
use tauri::{Runtime, Webview};
|
||||
|
||||
pub fn raise_shell<R: Runtime>(shell: &Webview<R>, on_top: bool) {
|
||||
let _ = shell.with_webview(move |platform| {
|
||||
// SAFETY: `inner` is the shell's live WKWebView (an NSView) and its superview (the
|
||||
// window's content view, which owns it), on the main thread (with_webview runs the
|
||||
// closure there).
|
||||
let view: &NSView = unsafe { &*platform.inner().cast::<NSView>() };
|
||||
let Some(parent) = (unsafe { view.superview() }) else { return };
|
||||
// Re-adding a subview can resign it as first responder: typing stays where it was.
|
||||
let window = view.window();
|
||||
let focused = window.as_ref().and_then(|window| window.firstResponder()).is_some_and(|responder| {
|
||||
responder.downcast_ref::<NSView>().is_some_and(|responder| responder.isDescendantOf(view))
|
||||
});
|
||||
let mode = if on_top { NSWindowOrderingMode::Above } else { NSWindowOrderingMode::Below };
|
||||
parent.addSubview_positioned_relativeTo(view, mode, None);
|
||||
if focused {
|
||||
if let Some(window) = window {
|
||||
let responder: &NSResponder = view;
|
||||
window.makeFirstResponder(Some(responder));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn placed_rect<R: Runtime>(webview: &Webview<R>) -> Option<(f64, f64, f64, f64)> {
|
||||
linux::placed(webview.label())
|
||||
|
|
@ -332,6 +373,7 @@ pub fn placed_rect<R: Runtime>(webview: &Webview<R>) -> Option<(f64, f64, f64, f
|
|||
mod linux {
|
||||
use gtk::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use tauri::{Runtime, Webview};
|
||||
|
||||
|
|
@ -349,6 +391,20 @@ mod linux {
|
|||
rects().get(label).copied()
|
||||
}
|
||||
|
||||
/// The shell is above the tab webviews ([`raise_shell`]).
|
||||
static RAISED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// The shell is the overlay's first layer (under the tabs, which `place` adds above it) or,
|
||||
/// raised, its last. Tabs placed while it is raised go under it.
|
||||
pub fn raise_shell<R: Runtime>(shell: &Webview<R>, on_top: bool) {
|
||||
RAISED.store(on_top, Ordering::SeqCst);
|
||||
let _ = shell.with_webview(move |platform| {
|
||||
let widget = platform.inner().upcast::<gtk::Widget>();
|
||||
let Some(overlay) = widget.parent().and_then(|parent| parent.downcast::<gtk::Overlay>().ok()) else { return };
|
||||
overlay.reorder_overlay(&widget, if on_top { -1 } else { 0 });
|
||||
});
|
||||
}
|
||||
|
||||
pub fn attach_shell<R: Runtime>(shell: &Webview<R>) {
|
||||
let _ = shell.with_webview(|platform| {
|
||||
let widget = platform.inner().upcast::<gtk::Widget>();
|
||||
|
|
@ -361,9 +417,14 @@ mod linux {
|
|||
let (x, y, w, h) = unsafe { *child.data::<GtkRect>(RECT_KEY)?.as_ptr() };
|
||||
Some(gtk::gdk::Rectangle::new(x, y, w, h))
|
||||
});
|
||||
// `widget` holds a reference, so removing it from the box does not destroy it.
|
||||
// `widget` holds a reference, so removing it from the box does not destroy it. The
|
||||
// shell is an overlay layer too (an empty box is the base), so it can be re-ordered
|
||||
// above the tabs; with no placement it fills the overlay (Fill alignment).
|
||||
vbox.remove(&widget);
|
||||
overlay.add(&widget);
|
||||
widget.set_halign(gtk::Align::Fill);
|
||||
widget.set_valign(gtk::Align::Fill);
|
||||
overlay.add(>k::Box::new(gtk::Orientation::Vertical, 0));
|
||||
overlay.add_overlay(&widget);
|
||||
vbox.pack_start(&overlay, true, true, 0);
|
||||
overlay.show();
|
||||
});
|
||||
|
|
@ -387,17 +448,83 @@ mod linux {
|
|||
};
|
||||
vbox.remove(&widget);
|
||||
overlay.add_overlay(&widget);
|
||||
if RAISED.load(Ordering::SeqCst) {
|
||||
overlay.reorder_overlay(&widget, 0);
|
||||
}
|
||||
}
|
||||
widget.queue_resize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The event carrying a tab's last frame to the shell ([`capture_still`]).
|
||||
pub const EVENT_TAB_STILL: &str = "tab-still";
|
||||
|
||||
#[cfg(windows)]
|
||||
#[derive(Clone, Serialize)]
|
||||
struct TabStill {
|
||||
id: String,
|
||||
/// `data:image/jpeg;base64,…`
|
||||
image: String,
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let Some(id) = policy::tab_id(webview.label()).map(str::to_owned) else { return };
|
||||
let app = webview.app_handle().clone();
|
||||
let _ = 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;
|
||||
use windows::Win32::System::Com::{STREAM_SEEK_END, STREAM_SEEK_SET};
|
||||
use windows::Win32::UI::Shell::SHCreateMemStream;
|
||||
// SAFETY: COM calls on this webview's live controller and a memory stream it writes
|
||||
// into, on the main thread (with_webview and the completion handler run there).
|
||||
unsafe {
|
||||
let controller = platform.controller();
|
||||
let mut shown = windows::core::BOOL::default();
|
||||
if controller.IsVisible(&mut shown).is_err() || !shown.as_bool() {
|
||||
return;
|
||||
}
|
||||
let (Ok(core), Some(stream)) = (controller.CoreWebView2(), SHCreateMemStream(None)) else { return };
|
||||
let target = stream.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(());
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
let _ = webview;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
let _ = if visible { webview.show() } else { webview.hide() };
|
||||
#[cfg(windows)]
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue