#include "webview.h" #include #include #include #include "util/composition.desktop.interop.h" #include "util/string_converter.h" #include "webview_host.h" using namespace Microsoft::WRL; namespace { inline void ConvertColor(COREWEBVIEW2_COLOR& webview_color, int32_t color) { webview_color.B = color & 0xFF; webview_color.G = (color >> 8) & 0xFF; webview_color.R = (color >> 16) & 0xFF; webview_color.A = (color >> 24) & 0xFF; } inline WebviewPermissionKind CW2PermissionKindToPermissionKind( COREWEBVIEW2_PERMISSION_KIND kind) { using k = COREWEBVIEW2_PERMISSION_KIND; switch (kind) { case k::COREWEBVIEW2_PERMISSION_KIND_MICROPHONE: return WebviewPermissionKind::Microphone; case k::COREWEBVIEW2_PERMISSION_KIND_CAMERA: return WebviewPermissionKind::Camera; case k::COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION: return WebviewPermissionKind::GeoLocation; case k::COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS: return WebviewPermissionKind::Notifications; case k::COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS: return WebviewPermissionKind::OtherSensors; case k::COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ: return WebviewPermissionKind::ClipboardRead; default: return WebviewPermissionKind::Unknown; } } inline COREWEBVIEW2_PERMISSION_STATE WebViewPermissionStateToCW2PermissionState( WebviewPermissionState state) { using s = COREWEBVIEW2_PERMISSION_STATE; switch (state) { case WebviewPermissionState::Allow: return s::COREWEBVIEW2_PERMISSION_STATE_ALLOW; case WebviewPermissionState::Deny: return s::COREWEBVIEW2_PERMISSION_STATE_DENY; default: return s::COREWEBVIEW2_PERMISSION_STATE_DEFAULT; } } } // namespace Webview::Webview( wil::com_ptr composition_controller, WebviewHost* host, HWND hwnd, bool owns_window, bool offscreen_only) : composition_controller_(std::move(composition_controller)), host_(host), hwnd_(hwnd), owns_window_(owns_window) { webview_controller_ = composition_controller_.try_query(); if (!webview_controller_ || FAILED(webview_controller_->get_CoreWebView2(webview_.put()))) { return; } webview_controller_->put_BoundsMode(COREWEBVIEW2_BOUNDS_MODE_USE_RAW_PIXELS); webview_controller_->put_ShouldDetectMonitorScaleChanges(FALSE); webview_controller_->put_RasterizationScale(1.0); wil::com_ptr settings; if (SUCCEEDED(webview_->get_Settings(settings.put()))) { settings2_ = settings.try_query(); auto settings3 = settings.try_query(); settings->put_IsStatusBarEnabled(FALSE); settings->put_AreDevToolsEnabled(FALSE); settings->put_AreDefaultContextMenusEnabled(FALSE); if (settings3) { settings3->put_AreBrowserAcceleratorKeysEnabled(FALSE); } } EnableSecurityUpdates(); RegisterEventHandlers(); is_valid_ = CreateSurface(host->compositor(), hwnd, offscreen_only); } Webview::~Webview() { if (owns_window_) { DestroyWindow(hwnd_); } } bool Webview::CreateSurface( winrt::com_ptr compositor, HWND hwnd, bool offscreen_only) { winrt::com_ptr root; if (FAILED(compositor->CreateContainerVisual(root.put()))) { return false; } surface_ = root.try_as(); assert(surface_); // initial size. doesn't matter as we resize the surface anyway. surface_->put_Size({1280, 720}); surface_->put_IsVisible(true); // Create on-screen window for debugging purposes if (!offscreen_only) { window_target_ = util::TryCreateDesktopWindowTarget(compositor, hwnd); auto composition_target = window_target_ .try_as(); if (composition_target) { composition_target->put_Root(surface_.get()); } } winrt::com_ptr webview_visual; compositor->CreateContainerVisual( reinterpret_cast( webview_visual.put())); auto webview_visual2 = webview_visual.try_as(); if (webview_visual2) { webview_visual2->put_RelativeSizeAdjustment({1.0f, 1.0f}); } winrt::com_ptr children; root->get_Children(children.put()); children->InsertAtTop(webview_visual.get()); composition_controller_->put_RootVisualTarget(webview_visual2.get()); webview_controller_->put_IsVisible(true); return true; } void Webview::EnableSecurityUpdates() { if (SUCCEEDED(webview_->CallDevToolsProtocolMethod(L"Security.enable", L"{}", nullptr)) && SUCCEEDED(webview_->GetDevToolsProtocolEventReceiver( L"Security.securityStateChanged", &devtools_protocol_event_receiver_))) { devtools_protocol_event_receiver_->add_DevToolsProtocolEventReceived( Callback( [this](ICoreWebView2* sender, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT { if (devtools_protocol_event_callback_) { wil::unique_cotaskmem_string json_args; if (args->get_ParameterObjectAsJson(&json_args) == S_OK) { std::string json = util::Utf8FromUtf16(json_args.get()); devtools_protocol_event_callback_(json.c_str()); } } return S_OK; }) .Get(), &event_registrations_.devtools_protocol_event_token_); } } void Webview::RegisterEventHandlers() { if (!webview_) { return; } webview_->add_ContentLoading( Callback( [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { if (loading_state_changed_callback_) { loading_state_changed_callback_(WebviewLoadingState::Loading); } return S_OK; }) .Get(), &event_registrations_.content_loading_token_); webview_->add_NavigationCompleted( Callback( [this](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT { BOOL is_success; args->get_IsSuccess(&is_success); if (!is_success && on_load_error_callback_) { COREWEBVIEW2_WEB_ERROR_STATUS web_error_status; args->get_WebErrorStatus(&web_error_status); on_load_error_callback_(web_error_status); } if (loading_state_changed_callback_) { loading_state_changed_callback_( WebviewLoadingState::NavigationCompleted); } return S_OK; }) .Get(), &event_registrations_.navigation_completed_token_); webview_->add_HistoryChanged( Callback( [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { if (history_changed_callback_) { BOOL can_go_back; BOOL can_go_forward; sender->get_CanGoBack(&can_go_back); sender->get_CanGoForward(&can_go_forward); history_changed_callback_({can_go_back, can_go_forward}); } return S_OK; }) .Get(), &event_registrations_.history_changed_token_); webview_->add_SourceChanged( Callback( [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { LPWSTR wurl; if (url_changed_callback_ && webview_->get_Source(&wurl) == S_OK) { std::string url = util::Utf8FromUtf16(wurl); url_changed_callback_(url); } return S_OK; }) .Get(), &event_registrations_.source_changed_token_); webview_->add_DocumentTitleChanged( Callback( [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { LPWSTR wtitle; if (document_title_changed_callback_ && webview_->get_DocumentTitle(&wtitle) == S_OK) { std::string title = util::Utf8FromUtf16(wtitle); document_title_changed_callback_(title); } return S_OK; }) .Get(), &event_registrations_.document_title_changed_token_); composition_controller_->add_CursorChanged( Callback( [this](ICoreWebView2CompositionController* sender, IUnknown* args) -> HRESULT { HCURSOR cursor; if (cursor_changed_callback_ && sender->get_Cursor(&cursor) == S_OK) { cursor_changed_callback_(cursor); } return S_OK; }) .Get(), &event_registrations_.cursor_changed_token_); webview_controller_->add_GotFocus( Callback( [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT { if (focus_changed_callback_) { focus_changed_callback_(true); } return S_OK; }) .Get(), &event_registrations_.got_focus_token_); webview_controller_->add_LostFocus( Callback( [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT { if (focus_changed_callback_) { focus_changed_callback_(false); } return S_OK; }) .Get(), &event_registrations_.lost_focus_token_); webview_->add_WebMessageReceived( Callback( [this](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT { wil::unique_cotaskmem_string wmessage; if (web_message_received_callback_ && args->get_WebMessageAsJson(&wmessage) == S_OK) { const std::string message = util::Utf8FromUtf16(wmessage.get()); web_message_received_callback_(message); } return S_OK; }) .Get(), &event_registrations_.web_message_received_token_); webview_->add_PermissionRequested( Callback( [this](ICoreWebView2* sender, ICoreWebView2PermissionRequestedEventArgs* args) -> HRESULT { if (!permission_requested_callback_) { return S_OK; } wil::unique_cotaskmem_string wuri; COREWEBVIEW2_PERMISSION_KIND kind = COREWEBVIEW2_PERMISSION_KIND_UNKNOWN_PERMISSION; BOOL is_user_initiated = false; if (args->get_Uri(&wuri) == S_OK && args->get_PermissionKind(&kind) == S_OK && args->get_IsUserInitiated(&is_user_initiated) == S_OK) { wil::com_ptr deferral; args->GetDeferral(deferral.put()); const std::string uri = util::Utf8FromUtf16(wuri.get()); permission_requested_callback_( uri, CW2PermissionKindToPermissionKind(kind), is_user_initiated == TRUE, [deferral = std::move(deferral), args = std::move(args)](WebviewPermissionState state) { args->put_State( WebViewPermissionStateToCW2PermissionState(state)); deferral->Complete(); }); } return S_OK; }) .Get(), &event_registrations_.permission_requested_token_); webview_->add_NewWindowRequested( Callback( [this](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT { switch (popup_window_policy_) { case WebviewPopupWindowPolicy::Deny: args->put_Handled(TRUE); break; case WebviewPopupWindowPolicy::ShowInSameWindow: args->put_NewWindow(webview_.get()); args->put_Handled(TRUE); break; } return S_OK; }) .Get(), &event_registrations_.new_windows_requested_token_); webview_->add_ContainsFullScreenElementChanged( Callback( [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT { BOOL flag = FALSE; if (contains_fullscreen_element_changed_callback_ && SUCCEEDED(sender->get_ContainsFullScreenElement(&flag))) { contains_fullscreen_element_changed_callback_(flag); } return S_OK; }) .Get(), &event_registrations_.contains_fullscreen_element_changed_token_); } void Webview::SetSurfaceSize(size_t width, size_t height, float scale_factor) { if (!IsValid()) { return; } if (surface_ && width > 0 && height > 0) { scale_factor_ = scale_factor; auto scaled_width = width * scale_factor; auto scaled_height = height * scale_factor; RECT bounds; bounds.left = 0; bounds.top = 0; bounds.right = static_cast(scaled_width); bounds.bottom = static_cast(scaled_height); surface_->put_Size({scaled_width, scaled_height}); webview_controller_->put_RasterizationScale(scale_factor); if (webview_controller_->put_Bounds(bounds) != S_OK) { std::cerr << "Setting webview bounds failed." << std::endl; } if (surface_size_changed_callback_) { surface_size_changed_callback_(width, height); } } } bool Webview::OpenDevTools() { if (!IsValid()) { return false; } webview_->OpenDevToolsWindow(); return true; } bool Webview::ClearCookies() { if (!IsValid()) { return false; } return webview_->CallDevToolsProtocolMethod(L"Network.clearBrowserCookies", L"{}", nullptr) == S_OK; } bool Webview::ClearCache() { if (!IsValid()) { return false; } return webview_->CallDevToolsProtocolMethod(L"Network.clearBrowserCache", L"{}", nullptr) == S_OK; } bool Webview::SetCacheDisabled(bool disabled) { if (!IsValid()) { return false; } std::string json = std::format("{{\"disableCache\":{}}}", disabled); return webview_->CallDevToolsProtocolMethod(L"Network.setCacheDisabled", util::Utf16FromUtf8(json).c_str(), nullptr) == S_OK; } void Webview::SetPopupWindowPolicy(WebviewPopupWindowPolicy policy) { popup_window_policy_ = policy; } bool Webview::SetUserAgent(const std::string& user_agent) { if (settings2_) { return settings2_->put_UserAgent(util::Utf16FromUtf8(user_agent).c_str()) == S_OK; } return false; } bool Webview::SetBackgroundColor(int32_t color) { if (!IsValid()) { return false; } COREWEBVIEW2_COLOR webview_color; ConvertColor(webview_color, color); // Semi-transparent backgrounds are not supported. // Valid alpha values are 0 or 255. if (webview_color.A > 0) { webview_color.A = 0xFF; } return webview_controller_->put_DefaultBackgroundColor(webview_color) == S_OK; } bool Webview::SetZoomFactor(double factor) { if (!IsValid()) { return false; } return webview_controller_->put_ZoomFactor(factor) == S_OK; } void Webview::SetCursorPos(double x, double y) { if (!IsValid()) { return; } POINT point; point.x = static_cast(x * scale_factor_); point.y = static_cast(y * scale_factor_); last_cursor_pos_ = point; // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2?view=webview2-1.0.774.44 composition_controller_->SendMouseInput( COREWEBVIEW2_MOUSE_EVENT_KIND::COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE, virtual_keys_.state(), 0, point); } void Webview::SetPointerUpdate(int32_t pointer, WebviewPointerEventKind eventKind, double x, double y, double size, double pressure) { if (!IsValid()) { return; } COREWEBVIEW2_POINTER_EVENT_KIND event = COREWEBVIEW2_POINTER_EVENT_KIND_UPDATE; UINT32 pointerFlags = POINTER_FLAG_NONE; switch (eventKind) { case WebviewPointerEventKind::Activate: event = COREWEBVIEW2_POINTER_EVENT_KIND_ACTIVATE; break; case WebviewPointerEventKind::Down: event = COREWEBVIEW2_POINTER_EVENT_KIND_DOWN; pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; break; case WebviewPointerEventKind::Enter: event = COREWEBVIEW2_POINTER_EVENT_KIND_ENTER; break; case WebviewPointerEventKind::Leave: event = COREWEBVIEW2_POINTER_EVENT_KIND_LEAVE; break; case WebviewPointerEventKind::Up: event = COREWEBVIEW2_POINTER_EVENT_KIND_UP; pointerFlags = POINTER_FLAG_UP; break; case WebviewPointerEventKind::Update: event = COREWEBVIEW2_POINTER_EVENT_KIND_UPDATE; pointerFlags = POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; break; } POINT point; point.x = static_cast(x * scale_factor_); point.y = static_cast(y * scale_factor_); RECT rect; rect.left = point.x - 2; rect.right = point.x + 2; rect.top = point.y - 2; rect.bottom = point.y + 2; host_->CreateWebViewPointerInfo( [this, pointer, event, pointerFlags, point, rect, pressure]( wil::com_ptr pointerInfo, std::unique_ptr error) { if (pointerInfo) { ICoreWebView2PointerInfo* pInfo = pointerInfo.get(); pInfo->put_PointerId(pointer); pInfo->put_PointerKind(PT_TOUCH); pInfo->put_PointerFlags(pointerFlags); pInfo->put_TouchFlags(TOUCH_FLAG_NONE); pInfo->put_TouchMask(TOUCH_MASK_CONTACTAREA | TOUCH_MASK_PRESSURE); pInfo->put_TouchPressure( std::clamp((UINT32)(pressure == 0.0 ? 1024 : 1024 * pressure), (UINT32)0, (UINT32)1024)); pInfo->put_PixelLocationRaw(point); pInfo->put_TouchContactRaw(rect); composition_controller_->SendPointerInput(event, pInfo); } }); } void Webview::SetPointerButtonState(WebviewPointerButton button, bool is_down) { if (!IsValid()) { return; } COREWEBVIEW2_MOUSE_EVENT_KIND kind; switch (button) { case WebviewPointerButton::Primary: virtual_keys_.set_isLeftButtonDown(is_down); kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN : COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP; break; case WebviewPointerButton::Secondary: virtual_keys_.set_isRightButtonDown(is_down); kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOWN : COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_UP; break; case WebviewPointerButton::Tertiary: virtual_keys_.set_isMiddleButtonDown(is_down); kind = is_down ? COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOWN : COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_UP; break; default: kind = static_cast(0); } composition_controller_->SendMouseInput(kind, virtual_keys_.state(), 0, last_cursor_pos_); } void Webview::SendScroll(double delta, bool horizontal) { // delta * 6 gives me a multiple of WHEEL_DELTA (120) constexpr auto kScrollMultiplier = 6; auto offset = static_cast(delta * kScrollMultiplier); POINT point; point.x = 0; point.y = 0; if (horizontal) { composition_controller_->SendMouseInput( COREWEBVIEW2_MOUSE_EVENT_KIND_HORIZONTAL_WHEEL, virtual_keys_.state(), offset, point); } else { composition_controller_->SendMouseInput(COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL, virtual_keys_.state(), offset, point); } } void Webview::SetScrollDelta(double delta_x, double delta_y) { if (!IsValid()) { return; } if (delta_x != 0.0) { SendScroll(delta_x, true); } if (delta_y != 0.0) { SendScroll(delta_y, false); } } void Webview::LoadUrl(const std::string& url) { if (IsValid()) { webview_->Navigate(util::Utf16FromUtf8(url).c_str()); } } void Webview::LoadStringContent(const std::string& content) { if (IsValid()) { webview_->NavigateToString(util::Utf16FromUtf8(content).c_str()); } } bool Webview::Stop() { if (!IsValid()) { return false; } return SUCCEEDED(webview_->CallDevToolsProtocolMethod(L"Page.stopLoading", L"{}", nullptr)); } bool Webview::Reload() { if (!IsValid()) { return false; } return SUCCEEDED(webview_->Reload()); } bool Webview::GoBack() { if (!IsValid()) { return false; } return SUCCEEDED(webview_->GoBack()); } bool Webview::GoForward() { if (!IsValid()) { return false; } return SUCCEEDED(webview_->GoForward()); } void Webview::AddScriptToExecuteOnDocumentCreated( const std::string& script, AddScriptToExecuteOnDocumentCreatedCallback callback) { if (IsValid()) { if (SUCCEEDED(webview_->AddScriptToExecuteOnDocumentCreated( util::Utf16FromUtf8(script).c_str(), Callback< ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>( [callback](HRESULT result, LPCWSTR wsid) -> HRESULT { std::string sid = util::Utf8FromUtf16(wsid); callback(SUCCEEDED(result), sid); return S_OK; }) .Get()))) { return; } } callback(false, std::string()); } void Webview::RemoveScriptToExecuteOnDocumentCreated( const std::string& script_id) { if (IsValid()) { webview_->RemoveScriptToExecuteOnDocumentCreated( util::Utf16FromUtf8(script_id).c_str()); } } void Webview::ExecuteScript(const std::string& script, ScriptExecutedCallback callback) { if (IsValid()) { if (SUCCEEDED(webview_->ExecuteScript( util::Utf16FromUtf8(script).c_str(), Callback( [callback](HRESULT result, LPCWSTR json_result_object) { callback(SUCCEEDED(result), util::Utf8FromUtf16(json_result_object)); return S_OK; }) .Get()))) { return; } } callback(false, std::string()); } bool Webview::PostWebMessage(const std::string& json) { if (!IsValid()) { return false; } return webview_->PostWebMessageAsJson(util::Utf16FromUtf8(json).c_str()) == S_OK; } bool Webview::Suspend() { if (!IsValid()) { return false; } wil::com_ptr webview; webview = webview_.query(); if (!webview) { return false; } webview_controller_->put_IsVisible(false); return webview->TrySuspend( Callback( [](HRESULT error_code, BOOL is_successful) -> HRESULT { return S_OK; }) .Get()) == S_OK; } bool Webview::Resume() { if (!IsValid()) { return false; } wil::com_ptr webview; webview = webview_.query(); if (!webview) { return false; } return webview->Resume() == S_OK && webview_controller_->put_IsVisible(true) == S_OK; } bool Webview::SetVirtualHostNameMapping( const std::string& hostName, const std::string& path, WebviewHostResourceAccessKind accessKind) { if (!IsValid()) { return false; } wil::com_ptr webview; webview = webview_.query(); if (!webview) { return false; } COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY; switch (accessKind) { case WebviewHostResourceAccessKind::Allow: accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW; break; case WebviewHostResourceAccessKind::DenyCors: accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY_CORS; break; case WebviewHostResourceAccessKind::Deny: accessKindIntValue = COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY; break; } return webview->SetVirtualHostNameToFolderMapping( util::Utf16FromUtf8(hostName).c_str(), util::Utf16FromUtf8(path).c_str(), accessKindIntValue); } bool Webview::ClearVirtualHostNameMapping(const std::string& hostName) { if (!IsValid()) { return false; } wil::com_ptr webview; webview = webview_.query(); if (!webview) { return false; } return webview->ClearVirtualHostNameToFolderMapping( util::Utf16FromUtf8(hostName).c_str()); }