use super::*;
mod dcomp;
mod dragdrop;
mod pointer;
mod surface_window;
use dcomp::DcompTree;
pub use dcomp::{CompositionSurfacePixels, IslandVideoFrame, IslandVisualSpec};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
type IslandPointerFilter = Arc<dyn Fn(&str, IslandPointerPhase, f32, f32) -> bool + Send + Sync>;
static ISLAND_VISUALS: OnceLock<Mutex<HashMap<String, Vec<IslandVisualSpec>>>> = OnceLock::new();
static SURFACE_WEBTAGS: OnceLock<Mutex<HashMap<isize, String>>> = OnceLock::new();
static ISLAND_POINTER_FILTER: OnceLock<Mutex<Option<IslandPointerFilter>>> = OnceLock::new();
pub const SYNTHETIC_MOUSE_WPARAM_MARKER: usize = 0x4c58_0000;
fn island_visuals() -> &'static Mutex<HashMap<String, Vec<IslandVisualSpec>>> {
ISLAND_VISUALS.get_or_init(|| Mutex::new(HashMap::new()))
}
fn surface_webtags() -> &'static Mutex<HashMap<isize, String>> {
SURFACE_WEBTAGS.get_or_init(|| Mutex::new(HashMap::new()))
}
fn island_pointer_filter() -> &'static Mutex<Option<IslandPointerFilter>> {
ISLAND_POINTER_FILTER.get_or_init(|| Mutex::new(None))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IslandPointerPhase {
Down,
Move,
Up,
Cancel,
}
pub fn register_surface_webtag(hwnd: isize, webtag: &str) {
if webtag.is_empty() {
return;
}
if let Ok(mut tags) = surface_webtags().lock() {
tags.insert(hwnd, webtag.to_string());
}
}
pub fn unregister_surface_webtag(hwnd: isize) {
if let Ok(mut tags) = surface_webtags().lock() {
tags.remove(&hwnd);
}
}
pub fn set_island_pointer_filter(
filter: impl Fn(&str, IslandPointerPhase, f32, f32) -> bool + Send + Sync + 'static,
) {
if let Ok(mut slot) = island_pointer_filter().lock() {
*slot = Some(Arc::new(filter));
}
}
pub fn consume_island_pointer(
hwnd: windows::Win32::Foundation::HWND,
phase: IslandPointerPhase,
x: f32,
y: f32,
) -> bool {
let tag = surface_webtags()
.lock()
.ok()
.and_then(|tags| tags.get(&(hwnd.0 as isize)).cloned());
let Some(tag) = tag else {
return false;
};
let filter = island_pointer_filter()
.lock()
.ok()
.and_then(|slot| slot.clone());
filter.is_some_and(|filter| filter(&tag, phase, x, y))
}
pub fn queue_island_visuals(webtag_key: &str, visuals: Vec<IslandVisualSpec>) {
if let Ok(mut queued) = island_visuals().lock() {
queued.insert(webtag_key.to_string(), visuals);
}
}
pub fn queued_island_visuals(webtag_key: &str) -> Vec<IslandVisualSpec> {
island_visuals()
.lock()
.ok()
.and_then(|queued| queued.get(webtag_key).cloned())
.unwrap_or_default()
}
pub(crate) enum HostingMode {
Windowed,
Composition(Box<CompositionSurface>),
}
pub(crate) struct CompositionSurface {
pub(crate) hwnd: HWND,
env3: ICoreWebView2Environment3,
controller: ICoreWebView2CompositionController,
dcomp: DcompTree,
input_tokens: surface_window::InputSubscriptions,
parent: HWND,
radii: [i32; 4],
corner_color: u32,
pub(crate) bounds: RECT,
visible: bool,
webtag_key: String,
}
static COMPOSITION_HOSTING: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
pub fn set_webview_composition_hosting(enabled: bool) {
COMPOSITION_HOSTING.store(enabled, std::sync::atomic::Ordering::Relaxed);
}
pub fn webview_composition_hosting_enabled() -> bool {
composition_hosting_enabled()
}
fn composition_hosting_enabled() -> bool {
let configured = || COMPOSITION_HOSTING.load(std::sync::atomic::Ordering::Relaxed);
match std::env::var("LINGXIA_WEBVIEW_COMPOSITION") {
Ok(value) => match value.trim().to_ascii_lowercase().as_str() {
"0" | "false" | "off" => false,
"1" | "true" | "on" => true,
_ => configured(),
},
Err(_) => configured(),
}
}
pub(crate) fn create_hosting_controller(
env: &ICoreWebView2Environment,
parent: HWND,
) -> StdResult<(ICoreWebView2Controller, HostingMode)> {
if composition_hosting_enabled() {
match create_composition_surface(env, parent) {
Ok((controller, surface)) => {
return Ok((controller, HostingMode::Composition(surface)));
}
Err(err) => {
log::warn!("composition hosting unavailable; using windowed WebView2: {err}");
}
}
}
Ok((create_controller(env, parent)?, HostingMode::Windowed))
}
fn create_composition_surface(
env: &ICoreWebView2Environment,
parent: HWND,
) -> StdResult<(ICoreWebView2Controller, Box<CompositionSurface>)> {
let env3: ICoreWebView2Environment3 = env.cast().map_err(|err| {
WebViewError::WebView(format!("WebView2 runtime lacks composition hosting: {err}"))
})?;
let mut bounds = RECT::default();
unsafe {
WindowsAndMessaging::GetClientRect(parent, &mut bounds)
.map_err(|err| WebViewError::WebView(format!("GetClientRect failed: {err}")))?;
}
let hwnd = surface_window::create_surface_window(parent, bounds)?;
let assembled = (|| {
let dcomp = DcompTree::new(hwnd)?;
let controller = create_composition_controller(&env3, hwnd)?;
let input_tokens = attach_surface(hwnd, &dcomp, &env3, &controller)?;
let base: ICoreWebView2Controller = controller.cast().map_err(|err| {
WebViewError::WebView(format!("composition controller cast failed: {err}"))
})?;
Ok((
base,
Box::new(CompositionSurface {
hwnd,
env3,
controller,
dcomp,
input_tokens,
parent,
radii: [0; 4],
corner_color: 0,
bounds,
visible: false,
webtag_key: String::new(),
}),
))
})();
if assembled.is_err() {
unsafe {
let _ = WindowsAndMessaging::DestroyWindow(hwnd);
}
}
assembled
}
fn attach_surface(
hwnd: HWND,
dcomp: &DcompTree,
env3: &ICoreWebView2Environment3,
controller: &ICoreWebView2CompositionController,
) -> StdResult<surface_window::InputSubscriptions> {
unsafe {
controller
.SetRootVisualTarget(dcomp.webview_visual())
.map_err(|err| WebViewError::WebView(format!("SetRootVisualTarget failed: {err}")))?;
}
let base: ICoreWebView2Controller = controller.cast().map_err(|err| {
WebViewError::WebView(format!("composition controller cast failed: {err}"))
})?;
let tokens = surface_window::attach_input(hwnd, env3, controller, &base);
dragdrop::register_drop_target(hwnd, controller);
Ok(tokens)
}
fn create_composition_controller(
env3: &ICoreWebView2Environment3,
hwnd: HWND,
) -> StdResult<ICoreWebView2CompositionController> {
let env3 = env3.clone();
let (tx, rx) = mpsc::channel();
CreateCoreWebView2CompositionControllerCompletedHandler::wait_for_async_operation(
Box::new(move |handler| unsafe {
env3.CreateCoreWebView2CompositionController(hwnd, &handler)
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |result, controller| {
result?;
tx.send(controller.ok_or_else(|| windows::core::Error::from(E_POINTER)))
.map_err(|_| windows::core::Error::from(E_POINTER))?;
Ok(())
}),
)
.map_err(map_webview2_error)?;
rx.recv()
.map_err(|_| {
WebViewError::WebView("Composition controller callback channel failed".to_string())
})?
.map_err(|err| {
WebViewError::WebView(format!("Composition controller creation failed: {err}"))
})
}
impl CompositionSurface {
fn ensure_alive(&mut self, parent: HWND, base: &ICoreWebView2Controller) -> StdResult<bool> {
if unsafe { WindowsAndMessaging::IsWindow(Some(self.hwnd)).as_bool() } {
return Ok(false);
}
log::info!("composition surface window died with its former host; recreating");
surface_window::detach_input(&self.controller, base, self.input_tokens);
self.input_tokens = surface_window::InputSubscriptions::default();
unsafe {
self.controller
.SetRootVisualTarget(None::<&windows::core::IUnknown>)
.map_err(|err| {
WebViewError::WebView(format!(
"disconnecting stale RootVisualTarget failed: {err}"
))
})?;
}
let hwnd = surface_window::create_surface_window(parent, self.bounds)?;
let rebuilt = (|| {
let dcomp = DcompTree::new(hwnd)?;
let tokens = attach_surface(hwnd, &dcomp, &self.env3, &self.controller)?;
Ok((dcomp, tokens))
})();
let (dcomp, tokens) = match rebuilt {
Ok(parts) => parts,
Err(err) => {
unsafe {
let _ = WindowsAndMessaging::DestroyWindow(hwnd);
}
return Err(err);
}
};
self.hwnd = hwnd;
self.dcomp = dcomp;
self.input_tokens = tokens;
self.parent = parent;
let bounds = self.bounds;
self.set_geometry(base, bounds, None, &self.webtag_key.clone())?;
if self.visible {
self.set_visible(base, true)?;
}
Ok(true)
}
pub(crate) fn set_geometry(
&mut self,
base: &ICoreWebView2Controller,
bounds: RECT,
corners: Option<([i32; 4], u32)>,
webtag_key: &str,
) -> StdResult<()> {
if !unsafe { WindowsAndMessaging::IsWindow(Some(self.hwnd)).as_bool() } {
self.bounds = bounds;
if let Some((radii, corner_color)) = corners {
self.radii = radii;
self.corner_color = corner_color;
}
let parent = self.parent;
if !webtag_key.is_empty() {
self.webtag_key = webtag_key.to_string();
}
return self.ensure_alive(parent, base).map(|_| ());
}
if !webtag_key.is_empty() {
self.webtag_key = webtag_key.to_string();
}
register_surface_webtag(self.hwnd.0 as isize, &self.webtag_key);
let (radii, corner_color) = corners.unwrap_or((self.radii, self.corner_color));
let width = (bounds.right - bounds.left).max(0);
let height = (bounds.bottom - bounds.top).max(0);
unsafe {
WindowsAndMessaging::SetWindowPos(
self.hwnd,
None,
bounds.left,
bounds.top,
width,
height,
WindowsAndMessaging::SWP_NOZORDER | WindowsAndMessaging::SWP_NOACTIVATE,
)
.map_err(|err| WebViewError::WebView(format!("SetWindowPos failed: {err}")))?;
base.SetBounds(RECT {
left: 0,
top: 0,
right: width,
bottom: height,
})
.map_err(|err| WebViewError::WebView(format!("SetBounds failed: {err}")))?;
}
self.bounds = bounds;
self.radii = radii;
self.corner_color = corner_color;
let island = queued_island_visuals(webtag_key);
self.dcomp
.apply_geometry(width, height, radii, corner_color, &island)
}
pub(crate) fn present_island_video_frame(
&mut self,
frame: &dcomp::IslandVideoFrame,
) -> StdResult<()> {
self.dcomp.present_island_video_frame(frame)
}
pub(crate) fn set_visible(
&mut self,
base: &ICoreWebView2Controller,
visible: bool,
) -> StdResult<()> {
self.visible = visible;
unsafe {
if visible {
surface_window::cancel_hide_suspend(self.hwnd);
let result = base
.SetIsVisible(true)
.map_err(|err| WebViewError::WebView(format!("SetIsVisible failed: {err}")));
let _ = WindowsAndMessaging::ShowWindow(self.hwnd, WindowsAndMessaging::SW_SHOWNA);
result
} else {
let _ = WindowsAndMessaging::ShowWindow(self.hwnd, WindowsAndMessaging::SW_HIDE);
surface_window::schedule_hide_suspend(self.hwnd);
Ok(())
}
}
}
pub(crate) fn bring_to_front(&mut self, base: &ICoreWebView2Controller) -> StdResult<()> {
let parent = self.parent;
self.ensure_alive(parent, base)?;
unsafe {
WindowsAndMessaging::SetWindowPos(
self.hwnd,
Some(WindowsAndMessaging::HWND_TOP),
0,
0,
0,
0,
WindowsAndMessaging::SWP_NOMOVE
| WindowsAndMessaging::SWP_NOSIZE
| WindowsAndMessaging::SWP_NOACTIVATE
| WindowsAndMessaging::SWP_SHOWWINDOW,
)
.map_err(|err| WebViewError::WebView(format!("SetWindowPos failed: {err}")))
}
}
pub(crate) fn set_parent(
&mut self,
base: &ICoreWebView2Controller,
parent: HWND,
) -> StdResult<()> {
unsafe {
let rebuilt = self.ensure_alive(parent, base)?;
if !rebuilt && self.parent != parent {
WindowsAndMessaging::SetParent(self.hwnd, Some(parent))
.map_err(|err| WebViewError::WebView(format!("SetParent failed: {err}")))?;
}
self.parent = parent;
let _ = WindowsAndMessaging::SetWindowPos(
self.hwnd,
Some(WindowsAndMessaging::HWND_BOTTOM),
0,
0,
0,
0,
WindowsAndMessaging::SWP_NOMOVE
| WindowsAndMessaging::SWP_NOSIZE
| WindowsAndMessaging::SWP_NOACTIVATE,
);
}
Ok(())
}
pub(crate) fn destroy(&self) {
unregister_surface_webtag(self.hwnd.0 as isize);
unsafe {
let _ = WindowsAndMessaging::DestroyWindow(self.hwnd);
}
}
}
pub fn find_composition_surface_hwnd(parent: isize) -> Option<isize> {
unsafe {
let found = WindowsAndMessaging::FindWindowExW(
Some(HWND(parent as *mut _)),
None,
windows::core::w!("LingXiaWebViewSurface"),
None,
)
.ok()?;
if found.0.is_null() {
None
} else {
Some(found.0 as isize)
}
}
}
pub fn capture_composition_surface_bgra(hwnd: isize) -> StdResult<CompositionSurfacePixels> {
use windows::Win32::Graphics::Gdi::{
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, CreateCompatibleDC,
DIB_RGB_COLORS, DeleteDC, DeleteObject, GetDC, GetDIBits, ReleaseDC, SelectObject,
};
use windows::Win32::Storage::Xps::{PRINT_WINDOW_FLAGS, PrintWindow};
use windows::Win32::UI::WindowsAndMessaging::PW_RENDERFULLCONTENT;
unsafe {
let hwnd = HWND(hwnd as *mut _);
let mut rect = RECT::default();
WindowsAndMessaging::GetClientRect(hwnd, &mut rect).map_err(|err| {
WebViewError::WebView(format!("GetClientRect composition surface failed: {err}"))
})?;
let width = (rect.right - rect.left).max(0);
let height = (rect.bottom - rect.top).max(0);
if width == 0 || height == 0 {
return Err(WebViewError::WebView(
"composition surface has zero size".to_string(),
));
}
let screen = GetDC(None);
let memdc = CreateCompatibleDC(Some(screen));
let bmp = CreateCompatibleBitmap(screen, width, height);
let old = SelectObject(memdc, bmp.into());
let printed = PrintWindow(hwnd, memdc, PRINT_WINDOW_FLAGS(PW_RENDERFULLCONTENT)).as_bool();
let mut info = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: width,
biHeight: -height,
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
..Default::default()
},
..Default::default()
};
let mut bgra = vec![0u8; (width * height * 4) as usize];
let copied = if printed {
GetDIBits(
memdc,
bmp,
0,
height as u32,
Some(bgra.as_mut_ptr() as *mut _),
&mut info,
DIB_RGB_COLORS,
)
} else {
0
};
SelectObject(memdc, old);
let _ = DeleteObject(bmp.into());
let _ = DeleteDC(memdc);
ReleaseDC(None, screen);
if copied == 0 {
return Err(WebViewError::WebView(
"PrintWindow/GetDIBits of LingXiaWebViewSurface failed".to_string(),
));
}
Ok(CompositionSurfacePixels {
width: width as u32,
height: height as u32,
bgra,
})
}
}
#[cfg(test)]
mod tests {
use super::{IslandVisualSpec, queue_island_visuals, queued_island_visuals};
#[test]
fn queued_island_visuals_are_visible_to_the_geometry_commit() {
queue_island_visuals(
"test-island-queue",
vec![IslandVisualSpec {
clip: None,
id: "lx-video-1".into(),
kind: "video".into(),
offset_x: 8.0,
offset_y: 40.0,
width: 8,
height: 8,
dest_width: 8.0,
dest_height: 8.0,
color: 0xff10_1010,
text: None,
hwnd: None,
pixels: None,
}],
);
let queued = queued_island_visuals("test-island-queue");
assert_eq!(queued.len(), 1);
assert_eq!(queued[0].id, "lx-video-1");
assert_eq!(queued[0].kind, "video");
assert!(queued[0].hwnd.is_none());
}
}