webview-sys 0.5.0

Rust native ffi bindings for webview
Documentation
#include "webview.h"

#include <atomic>
#include <cstring>
#include <functional>
#include <future>
#include <map>
#include <string>
#include <vector>

#define WIN32_LEAN_AND_MEAN
#include <objbase.h>
#include <windows.h>
#include <wingdi.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.UI.Interop.h>

#pragma comment(lib, "windowsapp.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "ole32.lib")

// Free result with SysFreeString.
static inline BSTR webview_to_bstr(const char *s) {
  DWORD size = MultiByteToWideChar(CP_UTF8, 0, s, -1, 0, 0);
  BSTR bs = SysAllocStringLen(0, size);
  if (bs == NULL) {
    return NULL;
  }
  MultiByteToWideChar(CP_UTF8, 0, s, -1, bs, size);
  return bs;
}

namespace webview {
using dispatch_fn_t = std::function<void()>;
using msg_cb_t = std::function<void(const char* msg)>;

inline std::string url_decode(const char *s)
{
    std::string decoded;
    size_t length = strlen(s);
    for (unsigned int i = 0; i < length; i++) {
        if (s[i] == '%') {
            decoded.push_back(hex2char(s + i + 1));
            i = i + 2;
        } else if (s[i] == '+') {
            decoded.push_back(' ');
        } else {
            decoded.push_back(s[i]);
        }
    }
    return decoded;
}

inline std::string html_from_uri(const char *s)
{
    const char *const prefix = "data:text/html,";
    const size_t prefix_length = strlen(prefix);
    if (!strncmp(s, prefix, prefix_length)) {
      return url_decode(s + prefix_length);
    }
    return "";
}

LRESULT CALLBACK WebviewWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
class browser_window {
private:
    bool EnableDpiAwareness() {
        auto lib_user32 = GetModuleHandleW(L"user32.dll");
        if(lib_user32) {
            auto fn_set_thread_dpi_awareness_context =
                reinterpret_cast<decltype(&SetThreadDpiAwarenessContext)>(
                GetProcAddress(lib_user32, "SetThreadDpiAwarenessContext")
            );
            if (
                fn_set_thread_dpi_awareness_context
                && fn_set_thread_dpi_awareness_context(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
            ) {
                return true;
            }
        }
        // Don't worry when SetThreadDpiAwarenessContext is not available. If
        // it's not available, we are not on a recent windows 10, and we don't
        // have edge...
        return false;
    }
    int MyGetDpiForWindow(HWND hWnd) {
        auto lib_user32 = GetModuleHandleW(L"user32.dll");
        if (lib_user32) {
            auto fn_get_dpi_for_window = reinterpret_cast<decltype(&GetDpiForWindow)>(
                GetProcAddress(lib_user32, "GetDpiForWindow")
            );
            if (fn_get_dpi_for_window) {
                return fn_get_dpi_for_window(hWnd);
            }
        }
        // Again, don't worry when GetDpiForWindow is not available. If we are
        // not on Windows 10, we don't have edge...
        return 96;
    }
public:
    browser_window(msg_cb_t cb, const char* title, int width, int height, bool resizable, bool frameless)
        : m_cb(cb)
    {
        HINSTANCE hInstance = GetModuleHandle(nullptr);

        WNDCLASSEX wc;
        ZeroMemory(&wc, sizeof(WNDCLASSEX));
        wc.cbSize = sizeof(WNDCLASSEX);
        wc.hInstance = hInstance;
        wc.lpfnWndProc = WebviewWndProc;
        wc.lpszClassName = L"webview";
        RegisterClassEx(&wc);

        EnableDpiAwareness();

        DWORD style = WS_OVERLAPPEDWINDOW;
        if (!resizable) {
            style &= ~(WS_SIZEBOX);
        }

        if (frameless) {
            style &= ~(WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
        }

        // Create window first, because we need the window to get DPI for the window.
        BSTR window_title = webview_to_bstr(title);
        m_window = CreateWindowEx(0, L"webview", window_title, style, 0, 0, 0, 0,
                     HWND_DESKTOP, NULL, hInstance, (void *)this);
        SysFreeString(window_title);

        // Have to call this before SetWindowPos or it will crash!
        SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this);
        if (frameless)
        {
            SetWindowLongPtr(m_window, GWL_STYLE, style);
        }
        this->saved_style = style;

        UINT dpi = MyGetDpiForWindow(m_window);
        if (dpi == 0) {
            dpi = 96;
        }

        RECT clientRect;
        RECT rect;
        rect.left = 0;
        rect.top = 0;
        rect.right = MulDiv(width, dpi, 96);
        rect.bottom = MulDiv(height, dpi, 96);
        AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, 0);
        GetClientRect(GetDesktopWindow(), &clientRect);
        int left = (clientRect.right / 2) - ((rect.right - rect.left) / 2);
        int top = (clientRect.bottom / 2) - ((rect.bottom - rect.top) / 2);
        rect.right = rect.right - rect.left + left;
        rect.left = left;
        rect.bottom = rect.bottom - rect.top + top;
        rect.top = top;

        // Set position, size and show window *atomically*.
        SetWindowPos(m_window, HWND_TOP, rect.left, rect.top,
            rect.right - rect.left, rect.bottom - rect.top, SWP_SHOWWINDOW);

        UpdateWindow(m_window);
        SetFocus(m_window);
    }

    void run()
    {
        while (this->loop(true) == 0) {

        }
    }

    int loop(int blocking)
    {
        MSG msg;

        if (blocking) {
            if (GetMessage(&msg, nullptr, 0, 0) < 0) return 0;
        } else {
            if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE) == 0) return 0;
        }

        if (msg.hwnd) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
            return 0;
        }
        if (msg.message == WM_APP) {
            auto f = (dispatch_fn_t*)(msg.lParam);
            (*f)();
            delete f;
        } else if (msg.message == WM_QUIT) {
            return -1;
        }

        return 0;
    }
    void exit() { PostQuitMessage(0); }
    void dispatch(dispatch_fn_t f)
    {

        PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f));
    }

    void set_title(const char* title)
    {

        BSTR window_title = webview_to_bstr(title);
        SetWindowText(m_window, window_title);
        SysFreeString(window_title);
    }

    void set_size(int width, int height)
    {

        RECT r;
        r.left = 50;
        r.top = 50;
        r.right = width;
        r.bottom = height;
        AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0);
        SetWindowPos(m_window, NULL, r.left, r.top, r.right - r.left, r.bottom - r.top,
            SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
    }

    void set_fullscreen(bool fullscreen)
    {

        if (this->is_fullscreen == fullscreen) {
            return;
        }
        if (!this->is_fullscreen) {
            this->saved_style = GetWindowLong(this->m_window, GWL_STYLE);
            this->saved_ex_style = GetWindowLong(this->m_window, GWL_EXSTYLE);
            GetWindowRect(this->m_window, &this->saved_rect);
        }
        this->is_fullscreen = !!fullscreen;
        if (fullscreen) {
            MONITORINFO monitor_info;
            SetWindowLong(this->m_window, GWL_STYLE,
                        this->saved_style & ~(WS_CAPTION | WS_THICKFRAME));
            SetWindowLong(this->m_window, GWL_EXSTYLE,
                        this->saved_ex_style &
                            ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE |
                                WS_EX_CLIENTEDGE | WS_EX_STATICEDGE));
            monitor_info.cbSize = sizeof(monitor_info);
            GetMonitorInfo(MonitorFromWindow(this->m_window, MONITOR_DEFAULTTONEAREST),
                        &monitor_info);
            RECT r;
            r.left = monitor_info.rcMonitor.left;
            r.top = monitor_info.rcMonitor.top;
            r.right = monitor_info.rcMonitor.right;
            r.bottom = monitor_info.rcMonitor.bottom;
            SetWindowPos(this->m_window, NULL, r.left, r.top, r.right - r.left,
                        r.bottom - r.top,
                        SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
        } else {
            SetWindowLong(this->m_window, GWL_STYLE, this->saved_style);
            SetWindowLong(this->m_window, GWL_EXSTYLE, this->saved_ex_style);
            SetWindowPos(this->m_window, NULL, this->saved_rect.left,
                        this->saved_rect.top,
                        this->saved_rect.right - this->saved_rect.left,
                        this->saved_rect.bottom - this->saved_rect.top,
                        SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
        }
    }

    void set_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
    {

        HBRUSH brush = CreateSolidBrush(RGB(r, g, b));
        SetClassLongPtr(this->m_window, GCLP_HBRBACKGROUND, (LONG_PTR)brush);
    }

    // protected:
    virtual void resize() {}
    HWND m_window;
    DWORD m_main_thread = GetCurrentThreadId();
    msg_cb_t m_cb;

    bool is_fullscreen = false;
    DWORD saved_style = 0;
    DWORD saved_ex_style = 0;
    RECT saved_rect;
};

LRESULT CALLBACK WebviewWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    auto w = (browser_window*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
    switch (msg) {
    case WM_SIZE:
        w->resize();
        break;
    case WM_DPICHANGED: {
        auto rect = reinterpret_cast<LPRECT>(lp);
        auto x = rect->left;
        auto y = rect->top;
        auto w = rect->right - x;
        auto h = rect->bottom - y;
        SetWindowPos(hwnd, nullptr, x, y, w, h, SWP_NOZORDER | SWP_NOACTIVATE);
        break;
    }
    case WM_CLOSE:
        DestroyWindow(hwnd);
        break;
    case WM_DESTROY:
        w->exit();
        break;
    default:
        return DefWindowProc(hwnd, msg, wp, lp);
    }
    return 0;
}

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::UI;
using namespace Windows::Web::UI::Interop;

class webview : public browser_window {
public:
    webview(webview_external_invoke_cb_t invoke_cb, const char* title, int width, int height, bool resizable, bool debug, bool frameless)
        : browser_window(std::bind(&webview::on_message, this, std::placeholders::_1), title, width, height, resizable, frameless)
        , invoke_cb(invoke_cb)
    {
        init_apartment(winrt::apartment_type::single_threaded);
        WebViewControlProcessOptions options;
        options.PrivateNetworkClientServerCapability(WebViewControlProcessCapabilityState::Enabled);
        m_process = WebViewControlProcess(options);
        auto op = m_process.CreateWebViewControlAsync(
            reinterpret_cast<int64_t>(m_window), Rect());
        if (op.Status() != AsyncStatus::Completed) {
            handle h(CreateEvent(nullptr, false, false, nullptr));
            op.Completed([h = h.get()](auto, auto) { SetEvent(h); });
            HANDLE hs[] = { h.get() };
            DWORD i;
            CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES
                    | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE,
                INFINITE, 1, hs, &i);
        }
        m_webview = op.GetResults();
        m_webview.Settings().IsScriptNotifyAllowed(true);
        m_webview.IsVisible(true);
        m_webview.ScriptNotify([=](auto const& sender, auto const& args) {
            std::string s = winrt::to_string(args.Value());
            m_cb(s.c_str());
        });
        m_webview.NavigationStarting([=](auto const& sender, auto const& args) {
            m_webview.AddInitializeScript(winrt::to_hstring(init_js));
        });
        init("window.external.invoke = s => window.external.notify(s)");
        resize();
    }

    void navigate(const char* url)
    {

        std::string html = html_from_uri(url);
        if (html != "") {
            m_webview.NavigateToString(winrt::to_hstring(html.c_str()));
        } else {
            Uri uri(winrt::to_hstring(url));
            m_webview.Navigate(uri);
        }
    }
    void init(const char* js)
    {

      init_js.append("(function(){")
             .append(js)
             .append("})();");
    }
    void eval(const char* js)
    {

        m_webview.InvokeScriptAsync(
            L"eval", single_threaded_vector<hstring>({ winrt::to_hstring(js) }));
    }

    void exit() {
        m_webview.Close();
    }

    void* window() { return (void*)m_window; }

    void* get_user_data() { return this->user_data; }
    void set_user_data(void* user_data) { this->user_data = user_data; }
private:
    void on_message(const char* msg)
    {

        this->invoke_cb(this, msg);
    }

    void resize()
    {
        RECT r;
        GetClientRect(m_window, &r);
        Rect bounds(r.left, r.top, r.right - r.left, r.bottom - r.top);
        m_webview.Bounds(bounds);
    }
    WebViewControlProcess m_process;
    WebViewControl m_webview = nullptr;
    std::string init_js = "";

    void* user_data = nullptr;
    webview_external_invoke_cb_t invoke_cb = nullptr;
};

} // namespace webview

// Free result with GlobalFree.
static inline char *webview_from_utf16(WCHAR *ws) {
  int n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL);
  char *s = (char *)GlobalAlloc(GMEM_FIXED, n);
  if (s == NULL) {
    return NULL;
  }
  WideCharToMultiByte(CP_UTF8, 0, ws, -1, s, n, NULL, NULL);
  return s;
}

WEBVIEW_API void webview_run(webview_t w)
{
    static_cast<webview::webview*>(w)->run();
}

WEBVIEW_API int webview_loop(webview_t w, int blocking)
{
    return static_cast<webview::webview*>(w)->loop(blocking);
}

WEBVIEW_API int webview_eval(webview_t w, const char *js)
{
    static_cast<webview::webview*>(w)->eval(js);
    return 0;
}

WEBVIEW_API void webview_set_title(webview_t w, const char *title)
{
    static_cast<webview::webview*>(w)->set_title(title);
}

WEBVIEW_API void webview_set_fullscreen(webview_t w, int fullscreen)
{
    static_cast<webview::webview*>(w)->set_fullscreen(fullscreen);
}

WEBVIEW_API void webview_set_color(webview_t w, uint8_t r, uint8_t g,
                                   uint8_t b, uint8_t a)
{
    static_cast<webview::webview*>(w)->set_color(r, g, b, a);
}

WEBVIEW_API void webview_dispatch(webview_t w, webview_dispatch_fn fn,
                                  void *arg)
{
    static_cast<webview::webview*>(w)->dispatch([=]() { fn(w, arg); });
}

WEBVIEW_API void webview_exit(webview_t w)
{
    webview::webview* wv = static_cast<webview::webview*>(w);
    DestroyWindow(wv->m_window);
}

WEBVIEW_API void webview_debug(const char *format, ...)
{
    // TODO
}

WEBVIEW_API void webview_print_log(const char *s)
{
    // TODO
}

WEBVIEW_API void* webview_get_user_data(webview_t w)
{
    return static_cast<webview::webview*>(w)->get_user_data();
}

WEBVIEW_API webview_t webview_new(
    const char* title, const char* url, int width, int height, int resizable, int debug,
    int frameless, webview_external_invoke_cb_t external_invoke_cb, void* userdata)
{
    auto w = new webview::webview(external_invoke_cb, title, width, height, resizable, debug, frameless);
    w->set_user_data(userdata);
    w->navigate(url);
	return w;
}

WEBVIEW_API void webview_free(webview_t w)
{
    delete static_cast<webview::webview*>(w);
}

WEBVIEW_API void webview_destroy(webview_t w)
{
    delete static_cast<webview::webview*>(w);
}