use super::*;
pub(crate) fn create_controller(
env: &ICoreWebView2Environment,
hwnd: HWND,
) -> StdResult<ICoreWebView2Controller> {
let env = env.clone();
let (tx, rx) = mpsc::channel();
CreateCoreWebView2ControllerCompletedHandler::wait_for_async_operation(
Box::new(move |handler| unsafe {
env.CreateCoreWebView2Controller(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("Controller callback channel failed".to_string()))?
.map_err(|err| WebViewError::WebView(format!("Controller creation failed: {err}")))
}
pub(crate) fn configure_controller(controller: &ICoreWebView2Controller) -> StdResult<()> {
use windows::core::Interface as _;
unsafe {
if let Ok(controller2) = controller.cast::<ICoreWebView2Controller2>() {
let _ = controller2.SetDefaultBackgroundColor(COREWEBVIEW2_COLOR {
A: 255,
R: 255,
G: 255,
B: 255,
});
}
if let Ok(controller3) = controller.cast::<ICoreWebView2Controller3>() {
let _ = controller3.SetShouldDetectMonitorScaleChanges(false);
let _ = controller3.SetBoundsMode(COREWEBVIEW2_BOUNDS_MODE_USE_RAW_PIXELS);
let _ = controller3.SetRasterizationScale(1.0);
}
controller
.SetBounds(RECT {
left: 0,
top: 0,
right: 1024,
bottom: 768,
})
.map_err(|err| WebViewError::WebView(format!("SetBounds failed: {err}")))?;
controller
.SetIsVisible(false)
.map_err(|err| WebViewError::WebView(format!("SetIsVisible failed: {err}")))?;
}
Ok(())
}
pub(crate) fn configure_settings(
webview: &ICoreWebView2,
effective_options: &EffectiveWebViewCreateOptions,
) -> StdResult<()> {
let relaxed_profile = effective_options.profile == SecurityProfile::BrowserRelaxed;
unsafe {
let settings = webview
.Settings()
.map_err(|err| WebViewError::WebView(format!("Settings failed: {err}")))?;
settings
.SetIsScriptEnabled(true)
.map_err(|err| WebViewError::WebView(format!("SetIsScriptEnabled failed: {err}")))?;
settings
.SetAreDefaultScriptDialogsEnabled(relaxed_profile)
.map_err(|err| {
WebViewError::WebView(format!("SetAreDefaultScriptDialogsEnabled failed: {err}"))
})?;
settings
.SetAreDefaultContextMenusEnabled(true)
.map_err(|err| {
WebViewError::WebView(format!("SetAreDefaultContextMenusEnabled failed: {err}"))
})?;
settings.SetIsWebMessageEnabled(true).map_err(|err| {
WebViewError::WebView(format!("SetIsWebMessageEnabled failed: {err}"))
})?;
settings
.SetIsStatusBarEnabled(false)
.map_err(|err| WebViewError::WebView(format!("SetIsStatusBarEnabled failed: {err}")))?;
settings
.SetAreDevToolsEnabled(webview_devtools_enabled())
.map_err(|err| WebViewError::WebView(format!("SetAreDevToolsEnabled failed: {err}")))?;
}
Ok(())
}
const EDITING_ITEM_NAMES: &[&str] = &[
"cut",
"copy",
"paste",
"pasteAsPlainText",
"selectAll",
"undo",
"redo",
];
type RefreshMenuLabelFn = Arc<dyn Fn(&str, &str) -> Option<String> + Send + Sync>;
type RefreshMenuTriggerFn = Arc<dyn Fn(&str, &str) + Send + Sync>;
static REFRESH_MENU_PROVIDER: Mutex<Option<(RefreshMenuLabelFn, RefreshMenuTriggerFn)>> =
Mutex::new(None);
pub fn set_windows_context_menu_refresh_provider(
label: RefreshMenuLabelFn,
trigger: RefreshMenuTriggerFn,
) {
if let Ok(mut slot) = REFRESH_MENU_PROVIDER.lock() {
*slot = Some((label, trigger));
}
}
fn refresh_menu_label(appid: &str, path: &str) -> Option<String> {
let provider = REFRESH_MENU_PROVIDER
.lock()
.ok()
.and_then(|slot| slot.clone())?;
(provider.0)(appid, path)
}
fn trigger_refresh_menu(appid: &str, path: &str) {
let provider = REFRESH_MENU_PROVIDER
.lock()
.ok()
.and_then(|slot| slot.clone());
if let Some((_, trigger)) = provider {
trigger(appid, path);
}
}
pub(crate) fn configure_context_menu(
webview: &ICoreWebView2,
env: &ICoreWebView2Environment,
appid: &str,
path: &str,
effective_options: &EffectiveWebViewCreateOptions,
) -> StdResult<()> {
if effective_options.profile == SecurityProfile::BrowserRelaxed {
return Ok(());
}
let webview11 = match webview.cast::<ICoreWebView2_11>() {
Ok(webview11) => webview11,
Err(err) => {
log::warn!("ContextMenuRequested unavailable; lxapp keeps default menu: {err}");
return Ok(());
}
};
let env = env.clone();
let appid = appid.to_string();
let path = path.to_string();
let handler = ContextMenuRequestedEventHandler::create(Box::new(move |_sender, args| {
let Some(args) = args else {
return Ok(());
};
unsafe {
let items = args.MenuItems()?;
trim_to_editing_items(&items)?;
insert_refresh_item(&env, &items, &appid, &path)?;
}
Ok(())
}));
let mut token = 0;
unsafe {
webview11
.add_ContextMenuRequested(&handler, &mut token)
.map_err(|err| {
WebViewError::WebView(format!("add_ContextMenuRequested failed: {err}"))
})?;
}
Ok(())
}
fn insert_refresh_item(
env: &ICoreWebView2Environment,
items: &ICoreWebView2ContextMenuItemCollection,
appid: &str,
path: &str,
) -> WinResult<()> {
let Some(label) = refresh_menu_label(appid, path) else {
return Ok(());
};
let Ok(env9) = env.cast::<ICoreWebView2Environment9>() else {
return Ok(());
};
let no_icon: Option<&IStream> = None;
let label_w: Vec<u16> = label.encode_utf16().chain(std::iter::once(0)).collect();
unsafe {
let item = env9.CreateContextMenuItem(
PCWSTR(label_w.as_ptr()),
no_icon,
COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND,
)?;
let appid = appid.to_string();
let path = path.to_string();
let selected = CustomItemSelectedEventHandler::create(Box::new(move |_item, _args| {
trigger_refresh_menu(&appid, &path);
Ok(())
}));
let mut token = 0i64;
item.add_CustomItemSelected(&selected, &mut token)?;
items.InsertValueAtIndex(0, &item)?;
let mut count = 0u32;
items.Count(&mut count)?;
if count > 1 {
let separator = env9.CreateContextMenuItem(
PCWSTR::null(),
no_icon,
COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_SEPARATOR,
)?;
items.InsertValueAtIndex(1, &separator)?;
}
}
Ok(())
}
fn trim_to_editing_items(items: &ICoreWebView2ContextMenuItemCollection) -> WinResult<()> {
unsafe {
let mut count = 0u32;
items.Count(&mut count)?;
for index in (0..count).rev() {
let item = items.GetValueAtIndex(index)?;
let mut kind = COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_SEPARATOR;
item.Kind(&mut kind)?;
if kind == COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_SEPARATOR {
continue;
}
let mut name = PWSTR::null();
item.Name(&mut name)?;
let name = CoTaskMemPWSTR::from(name).to_string();
if !EDITING_ITEM_NAMES.contains(&name.as_str()) {
items.RemoveValueAtIndex(index)?;
}
}
tidy_separators(items)
}
}
fn tidy_separators(items: &ICoreWebView2ContextMenuItemCollection) -> WinResult<()> {
unsafe {
loop {
let mut count = 0u32;
items.Count(&mut count)?;
let mut removed_at: Option<u32> = None;
let mut prev_separator = true;
for index in 0..count {
let item = items.GetValueAtIndex(index)?;
let mut kind = COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_SEPARATOR;
item.Kind(&mut kind)?;
let is_separator = kind == COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_SEPARATOR;
if is_separator && prev_separator {
removed_at = Some(index);
break;
}
prev_separator = is_separator;
}
if removed_at.is_none() && prev_separator && count > 0 {
removed_at = Some(count - 1);
}
match removed_at {
Some(index) => items.RemoveValueAtIndex(index)?,
None => break,
}
}
Ok(())
}
}