diff --git a/.gitignore b/.gitignore index d32efda..359e550 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vs/ +.vscode/ bin/ obj/ *.user diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json deleted file mode 100644 index fe605c4..0000000 --- a/.vscode/c_cpp_properties.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "configurations": [ - { - "name": "Mac", - "includePath": [ - "${workspaceFolder}/**", - "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/**" - ], - "defines": [ - "OS_MAC" - ], - "macFrameworkPath": [ - "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks" - ], - "compilerPath": "/usr/bin/gcc", - "cStandard": "c11", - "cppStandard": "c++17", - "intelliSenseMode": "gcc-x64", - "compilerArgs": [ - "-shared -lstdc++ -DOS_MAC Exports.cpp WebWindow.Mac.cpp" - ] - } - ], - "version": 4 -} \ No newline at end of file diff --git a/src/WebWindow.Blazor/ComponentsDesktop.cs b/src/WebWindow.Blazor/ComponentsDesktop.cs index e9f9a52..c7ceee6 100644 --- a/src/WebWindow.Blazor/ComponentsDesktop.cs +++ b/src/WebWindow.Blazor/ComponentsDesktop.cs @@ -20,8 +20,7 @@ public static class ComponentsDesktop internal static string BaseUriAbsolute { get; private set; } internal static DesktopJSRuntime DesktopJSRuntime { get; private set; } internal static DesktopRenderer DesktopRenderer { get; private set; } - - internal static WebWindow webWindow; + internal static WebWindow WebWindow { get; private set; } public static void Run(string windowTitle, string hostHtmlPath) { @@ -30,7 +29,7 @@ public static void Run(string windowTitle, string hostHtmlPath) UnhandledException(exception); }; - webWindow = new WebWindow(windowTitle, options => + WebWindow = new WebWindow(windowTitle, options => { var contentRootAbsolute = Path.GetDirectoryName(Path.GetFullPath(hostHtmlPath)); @@ -57,11 +56,11 @@ public static void Run(string windowTitle, string hostHtmlPath) }); CancellationTokenSource appLifetimeCts = new CancellationTokenSource(); - Task.Factory.StartNew(async() => + Task.Factory.StartNew(async () => { try { - var ipc = new IPC(webWindow); + var ipc = new IPC(WebWindow); await RunAsync(ipc, appLifetimeCts.Token); } catch (Exception ex) @@ -73,8 +72,8 @@ public static void Run(string windowTitle, string hostHtmlPath) try { - webWindow.NavigateToUrl(BlazorAppScheme + "://app/"); - webWindow.WaitForExit(); + WebWindow.NavigateToUrl(BlazorAppScheme + "://app/"); + WebWindow.WaitForExit(); } finally { @@ -110,7 +109,7 @@ private static string BlazorAppScheme private static void UnhandledException(Exception ex) { - webWindow.ShowMessage("Error", $"{ex.Message}\n{ex.StackTrace}"); + WebWindow.ShowMessage("Error", $"{ex.Message}\n{ex.StackTrace}"); } private static async Task RunAsync(IPC ipc, CancellationToken appLifetime) @@ -124,6 +123,7 @@ private static async Task RunAsync(IPC ipc, CancellationToken appLifet serviceCollection.AddSingleton(DesktopNavigationManager.Instance); serviceCollection.AddSingleton(DesktopJSRuntime); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(WebWindow); var startup = new ConventionBasedStartup(Activator.CreateInstance(typeof(TStartup))); startup.ConfigureServices(serviceCollection); diff --git a/src/WebWindow.Blazor/IPC.cs b/src/WebWindow.Blazor/IPC.cs index fa7b18d..6c3863e 100644 --- a/src/WebWindow.Blazor/IPC.cs +++ b/src/WebWindow.Blazor/IPC.cs @@ -19,7 +19,7 @@ public IPC(WebWindow webWindow) _webWindow.OnWebMessageReceived += HandleScriptNotify; } - public async Task Send(string eventName, params object[] args) + public void Send(string eventName, params object[] args) { try { diff --git a/src/WebWindow.Native/Exports.cpp b/src/WebWindow.Native/Exports.cpp index 9d2a2f4..1b62fbd 100644 --- a/src/WebWindow.Native/Exports.cpp +++ b/src/WebWindow.Native/Exports.cpp @@ -1,7 +1,7 @@ #include "WebWindow.h" #ifdef _WIN32 -# define EXPORTED __declspec( dllexport ) +# define EXPORTED __declspec(dllexport) #else # define EXPORTED #endif @@ -25,12 +25,17 @@ extern "C" } #endif - EXPORTED WebWindow* WebWindow_ctor(UTF8String title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback) + EXPORTED WebWindow* WebWindow_ctor(AutoString title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback) { return new WebWindow(title, parent, webMessageReceivedCallback); } - EXPORTED void WebWindow_SetTitle(WebWindow* instance, UTF8String title) + EXPORTED void WebWindow_dtor(WebWindow* instance) + { + delete instance; + } + + EXPORTED void WebWindow_SetTitle(WebWindow* instance, AutoString title) { instance->SetTitle(title); } @@ -45,7 +50,7 @@ extern "C" instance->WaitForExit(); } - EXPORTED void WebWindow_ShowMessage(WebWindow* instance, UTF8String title, UTF8String body, unsigned int type) + EXPORTED void WebWindow_ShowMessage(WebWindow* instance, AutoString title, AutoString body, unsigned int type) { instance->ShowMessage(title, body, type); } @@ -55,23 +60,78 @@ extern "C" instance->Invoke(callback); } - EXPORTED void WebWindow_NavigateToString(WebWindow* instance, UTF8String content) + EXPORTED void WebWindow_NavigateToString(WebWindow* instance, AutoString content) { instance->NavigateToString(content); } - EXPORTED void WebWindow_NavigateToUrl(WebWindow* instance, UTF8String url) + EXPORTED void WebWindow_NavigateToUrl(WebWindow* instance, AutoString url) { instance->NavigateToUrl(url); } - EXPORTED void WebWindow_SendMessage(WebWindow* instance, UTF8String message) + EXPORTED void WebWindow_SendMessage(WebWindow* instance, AutoString message) { instance->SendMessage(message); } - EXPORTED void WebWindow_AddCustomScheme(WebWindow* instance, UTF8String scheme, WebResourceRequestedCallback requestHandler) + EXPORTED void WebWindow_AddCustomScheme(WebWindow* instance, AutoString scheme, WebResourceRequestedCallback requestHandler) { instance->AddCustomScheme(scheme, requestHandler); } + + EXPORTED void WebWindow_SetResizable(WebWindow* instance, int resizable) + { + instance->SetResizable(resizable); + } + + EXPORTED void WebWindow_GetSize(WebWindow* instance, int* width, int* height) + { + instance->GetSize(width, height); + } + + EXPORTED void WebWindow_SetSize(WebWindow* instance, int width, int height) + { + instance->SetSize(width, height); + } + + EXPORTED void WebWindow_SetResizedCallback(WebWindow* instance, ResizedCallback callback) + { + instance->SetResizedCallback(callback); + } + + EXPORTED void WebWindow_GetAllMonitors(WebWindow* instance, GetAllMonitorsCallback callback) + { + instance->GetAllMonitors(callback); + } + + EXPORTED unsigned int WebWindow_GetScreenDpi(WebWindow* instance) + { + return instance->GetScreenDpi(); + } + + EXPORTED void WebWindow_GetPosition(WebWindow* instance, int* x, int* y) + { + instance->GetPosition(x, y); + } + + EXPORTED void WebWindow_SetPosition(WebWindow* instance, int x, int y) + { + instance->SetPosition(x, y); + } + + EXPORTED void WebWindow_SetMovedCallback(WebWindow* instance, MovedCallback callback) + { + instance->SetMovedCallback(callback); + } + + EXPORTED void WebWindow_SetTopmost(WebWindow* instance, int topmost) + { + instance->SetTopmost(topmost); + } + + EXPORTED void WebWindow_SetIconFile(WebWindow* instance, AutoString filename) + { + instance->SetIconFile(filename); + } } diff --git a/src/WebWindow.Native/WebWindow.Linux.cpp b/src/WebWindow.Native/WebWindow.Linux.cpp index 9dd6416..8ec24c1 100644 --- a/src/WebWindow.Native/WebWindow.Linux.cpp +++ b/src/WebWindow.Native/WebWindow.Linux.cpp @@ -4,6 +4,7 @@ #include "WebWindow.h" #include #include +#include #include #include #include @@ -23,30 +24,46 @@ struct InvokeJSWaitInfo bool isCompleted; }; -WebWindow::WebWindow(UTF8String title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback) +void on_size_allocate(GtkWidget* widget, GdkRectangle* allocation, gpointer self); +gboolean on_configure_event(GtkWidget* widget, GdkEvent* event, gpointer self); + +WebWindow::WebWindow(AutoString title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback) : _webview(nullptr) { _webMessageReceivedCallback = webMessageReceivedCallback; + // It makes xlib thread safe. + // Needed for get_position. + XInitThreads(); + gtk_init(0, NULL); _window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_default_size((GtkWindow*)_window, 900, 600); + gtk_window_set_default_size(GTK_WINDOW(_window), 900, 600); SetTitle(title); if (parent == NULL) { g_signal_connect(G_OBJECT(_window), "destroy", - G_CALLBACK(+[](GtkWidget* w, gpointer arg) { - gtk_main_quit(); - }), + G_CALLBACK(+[](GtkWidget* w, gpointer arg) { gtk_main_quit(); }), + this); + g_signal_connect(G_OBJECT(_window), "size-allocate", + G_CALLBACK(on_size_allocate), + this); + g_signal_connect(G_OBJECT(_window), "configure-event", + G_CALLBACK(on_configure_event), this); } } +WebWindow::~WebWindow() +{ + gtk_widget_destroy(_window); +} + void HandleWebMessage(WebKitUserContentManager* contentManager, WebKitJavascriptResult* jsResult, gpointer arg) { JSCValue* jsValue = webkit_javascript_result_get_js_value(jsResult); if (jsc_value_is_string(jsValue)) { - UTF8String str_value = jsc_value_to_string(jsValue); + AutoString str_value = jsc_value_to_string(jsValue); WebMessageReceivedCallback callback = (WebMessageReceivedCallback)arg; callback(str_value); @@ -91,9 +108,9 @@ void WebWindow::Show() webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector)); } -void WebWindow::SetTitle(UTF8String title) +void WebWindow::SetTitle(AutoString title) { - gtk_window_set_title((GtkWindow*)_window, title); + gtk_window_set_title(GTK_WINDOW(_window), title); } void WebWindow::WaitForExit() @@ -125,9 +142,9 @@ void WebWindow::Invoke(ACTION callback) waitInfo.completionNotifier.wait(uLock, [&] { return waitInfo.isCompleted; }); } -void WebWindow::ShowMessage(UTF8String title, UTF8String body, unsigned int type) +void WebWindow::ShowMessage(AutoString title, AutoString body, unsigned int type) { - GtkWidget* dialog = gtk_message_dialog_new((GtkWindow*)_window, + GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(_window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_OTHER, GTK_BUTTONS_OK, @@ -138,12 +155,12 @@ void WebWindow::ShowMessage(UTF8String title, UTF8String body, unsigned int type gtk_widget_destroy(dialog); } -void WebWindow::NavigateToUrl(UTF8String url) +void WebWindow::NavigateToUrl(AutoString url) { webkit_web_view_load_uri(WEBKIT_WEB_VIEW(_webview), url); } -void WebWindow::NavigateToString(UTF8String content) +void WebWindow::NavigateToString(AutoString content) { webkit_web_view_load_html(WEBKIT_WEB_VIEW(_webview), content, NULL); } @@ -178,7 +195,7 @@ static void webview_eval_finished(GObject* object, GAsyncResult* result, gpointe waitInfo->isCompleted = true; } -void WebWindow::SendMessage(UTF8String message) +void WebWindow::SendMessage(AutoString message) { std::string js; js.append("__dispatchMessageCallback(\""); @@ -199,15 +216,15 @@ void HandleCustomSchemeRequest(WebKitURISchemeRequest* request, gpointer user_da const gchar* uri = webkit_uri_scheme_request_get_uri(request); int numBytes; - UTF8String contentType; - void* dotNetResponse = webResourceRequestedCallback((UTF8String)uri, &numBytes, &contentType); + AutoString contentType; + void* dotNetResponse = webResourceRequestedCallback((AutoString)uri, &numBytes, &contentType); GInputStream* stream = g_memory_input_stream_new_from_data(dotNetResponse, numBytes, NULL); webkit_uri_scheme_request_finish(request, (GInputStream*)stream, -1, contentType); g_object_unref(stream); delete[] contentType; } -void WebWindow::AddCustomScheme(UTF8String scheme, WebResourceRequestedCallback requestHandler) +void WebWindow::AddCustomScheme(AutoString scheme, WebResourceRequestedCallback requestHandler) { WebKitWebContext* context = webkit_web_context_get_default(); webkit_web_context_register_uri_scheme(context, scheme, @@ -215,4 +232,81 @@ void WebWindow::AddCustomScheme(UTF8String scheme, WebResourceRequestedCallback (void*)requestHandler, NULL); } +void WebWindow::SetResizable(bool resizable) +{ + gtk_window_set_resizable(GTK_WINDOW(_window), resizable ? TRUE : FALSE); +} + +void WebWindow::GetSize(int* width, int* height) +{ + gtk_window_get_size(GTK_WINDOW(_window), width, height); +} + +void WebWindow::SetSize(int width, int height) +{ + gtk_window_resize(GTK_WINDOW(_window), width, height); +} + +void on_size_allocate(GtkWidget* widget, GdkRectangle* allocation, gpointer self) +{ + int width, height; + gtk_window_get_size(GTK_WINDOW(widget), &width, &height); + ((WebWindow*)self)->InvokeResized(width, height); +} + +void WebWindow::GetAllMonitors(GetAllMonitorsCallback callback) +{ + if (callback) + { + GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(_window)); + GdkDisplay* display = gdk_screen_get_display(screen); + int n = gdk_display_get_n_monitors(display); + for (int i = 0; i < n; i++) + { + GdkMonitor* monitor = gdk_display_get_monitor(display, i); + Monitor props = {}; + gdk_monitor_get_geometry(monitor, (GdkRectangle*)&props.monitor); + gdk_monitor_get_workarea(monitor, (GdkRectangle*)&props.work); + if (!callback(&props)) break; + } + } +} + +unsigned int WebWindow::GetScreenDpi() +{ + GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(_window)); + gdouble dpi = gdk_screen_get_resolution(screen); + if (dpi < 0) return 96; + else return (unsigned int)dpi; +} + +void WebWindow::GetPosition(int* x, int* y) +{ + gtk_window_get_position(GTK_WINDOW(_window), x, y); +} + +void WebWindow::SetPosition(int x, int y) +{ + gtk_window_move(GTK_WINDOW(_window), x, y); +} + +gboolean on_configure_event(GtkWidget* widget, GdkEvent* event, gpointer self) +{ + if (event->type == GDK_CONFIGURE) + { + ((WebWindow*)self)->InvokeMoved(event->configure.x, event->configure.y); + } + return FALSE; +} + +void WebWindow::SetTopmost(bool topmost) +{ + gtk_window_set_keep_above(GTK_WINDOW(_window), topmost ? TRUE : FALSE); +} + +void WebWindow::SetIconFile(AutoString filename) +{ + gtk_window_set_icon_from_file(GTK_WINDOW(_window), filename, NULL); +} + #endif diff --git a/src/WebWindow.Native/WebWindow.Mac.AppDelegate.m b/src/WebWindow.Native/WebWindow.Mac.AppDelegate.mm similarity index 100% rename from src/WebWindow.Native/WebWindow.Mac.AppDelegate.m rename to src/WebWindow.Native/WebWindow.Mac.AppDelegate.mm diff --git a/src/WebWindow.Native/WebWindow.Mac.UiDelegate.h b/src/WebWindow.Native/WebWindow.Mac.UiDelegate.h index 8a75fee..96e16e9 100644 --- a/src/WebWindow.Native/WebWindow.Mac.UiDelegate.h +++ b/src/WebWindow.Native/WebWindow.Mac.UiDelegate.h @@ -1,11 +1,13 @@ #import #import +#include "WebWindow.h" typedef void (*WebMessageReceivedCallback) (char* message); @interface MyUiDelegate : NSObject { @public NSWindow * window; + WebWindow * webWindow; WebMessageReceivedCallback webMessageReceivedCallback; } @end diff --git a/src/WebWindow.Native/WebWindow.Mac.UiDelegate.m b/src/WebWindow.Native/WebWindow.Mac.UiDelegate.mm similarity index 88% rename from src/WebWindow.Native/WebWindow.Mac.UiDelegate.m rename to src/WebWindow.Native/WebWindow.Mac.UiDelegate.mm index 9af0136..89eddd2 100644 --- a/src/WebWindow.Native/WebWindow.Mac.UiDelegate.m +++ b/src/WebWindow.Native/WebWindow.Mac.UiDelegate.mm @@ -58,4 +58,17 @@ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSSt [alert release]; }]; } + +- (void)windowDidResize:(NSNotification *)notification { + int width, height; + webWindow->GetSize(&width, &height); + webWindow->InvokeResized(width, height); +} + +- (void)windowDidMove:(NSNotification *)notification { + int x, y; + webWindow->GetPosition(&x, &y); + webWindow->InvokeMoved(x, y); +} + @end diff --git a/src/WebWindow.Native/WebWindow.Mac.mm b/src/WebWindow.Native/WebWindow.Mac.mm index cec29ba..cdee0bf 100644 --- a/src/WebWindow.Native/WebWindow.Mac.mm +++ b/src/WebWindow.Native/WebWindow.Mac.mm @@ -3,10 +3,15 @@ #import "WebWindow.Mac.AppDelegate.h" #import "WebWindow.Mac.UiDelegate.h" #import "WebWindow.Mac.UrlSchemeHandler.h" -#include +#include +#include #import #import +using namespace std; + +map nsWindowToWebWindow; + void WebWindow::Register() { [NSAutoreleasePool new]; @@ -29,7 +34,7 @@ [application setDelegate:appDelegate]; } -WebWindow::WebWindow(UTF8String title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback) +WebWindow::WebWindow(AutoString title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback) { _webMessageReceivedCallback = webMessageReceivedCallback; NSRect frame = NSMakeRect(0, 0, 900, 600); @@ -49,9 +54,20 @@ _webview = nil; } +WebWindow::~WebWindow() +{ + WKWebViewConfiguration *webViewConfiguration = (WKWebViewConfiguration*)_webviewConfiguration; + [webViewConfiguration release]; + WKWebView *webView = (WKWebView*)_webview; + [webView release]; + NSWindow* window = (NSWindow*)_window; + [window close]; +} + void WebWindow::AttachWebView() { MyUiDelegate *uiDelegate = [[[MyUiDelegate alloc] init] autorelease]; + uiDelegate->webWindow = this; NSString *initScriptSource = @"window.__receiveMessageCallbacks = [];" "window.__dispatchMessageCallback = function(message) {" @@ -83,6 +99,10 @@ uiDelegate->webMessageReceivedCallback = _webMessageReceivedCallback; [userContentController addScriptMessageHandler:uiDelegate name:@"webwindowinterop"]; + // TODO: Remove these observers when the window is closed + [[NSNotificationCenter defaultCenter] addObserver:uiDelegate selector:@selector(windowDidResize:) name:NSWindowDidResizeNotification object:window]; + [[NSNotificationCenter defaultCenter] addObserver:uiDelegate selector:@selector(windowDidMove:) name:NSWindowDidMoveNotification object:window]; + _webview = webView; } @@ -96,7 +116,7 @@ [window makeKeyAndOrderFront:nil]; } -void WebWindow::SetTitle(UTF8String title) +void WebWindow::SetTitle(AutoString title) { NSWindow* window = (NSWindow*)_window; NSString* nstitle = [[NSString stringWithUTF8String:title] autorelease]; @@ -115,7 +135,7 @@ }); } -void WebWindow::ShowMessage(UTF8String title, UTF8String body, unsigned int type) +void WebWindow::ShowMessage(AutoString title, AutoString body, unsigned int type) { NSString* nstitle = [[NSString stringWithUTF8String:title] autorelease]; NSString* nsbody= [[NSString stringWithUTF8String:body] autorelease]; @@ -125,14 +145,14 @@ [alert runModal]; } -void WebWindow::NavigateToString(UTF8String content) +void WebWindow::NavigateToString(AutoString content) { WKWebView *webView = (WKWebView *)_webview; NSString* nscontent = [[NSString stringWithUTF8String:content] autorelease]; [webView loadHTMLString:nscontent baseURL:nil]; } -void WebWindow::NavigateToUrl(UTF8String url) +void WebWindow::NavigateToUrl(AutoString url) { WKWebView *webView = (WKWebView *)_webview; NSString* nsurlstring = [[NSString stringWithUTF8String:url] autorelease]; @@ -141,7 +161,7 @@ [webView loadRequest:nsrequest]; } -void WebWindow::SendMessage(UTF8String message) +void WebWindow::SendMessage(AutoString message) { // JSON-encode the message NSString* nsmessage = [NSString stringWithUTF8String:message]; @@ -156,7 +176,7 @@ [webView evaluateJavaScript:javaScriptToEval completionHandler:nil]; } -void WebWindow::AddCustomScheme(UTF8String scheme, WebResourceRequestedCallback requestHandler) +void WebWindow::AddCustomScheme(AutoString scheme, WebResourceRequestedCallback requestHandler) { // Note that this can only be done *before* the WKWebView is instantiated, so we only let this // get called from the options callback in the constructor @@ -168,4 +188,94 @@ [webviewConfiguration setURLSchemeHandler:schemeHandler forURLScheme:nsscheme]; } +void WebWindow::SetResizable(bool resizable) +{ + NSWindow* window = (NSWindow*)_window; + if (resizable) window.styleMask |= NSWindowStyleMaskResizable; + else window.styleMask &= ~NSWindowStyleMaskResizable; +} + +void WebWindow::GetSize(int* width, int* height) +{ + NSWindow* window = (NSWindow*)_window; + NSSize size = [window frame].size; + if (width) *width = (int)roundf(size.width); + if (height) *height = (int)roundf(size.height); +} + +void WebWindow::SetSize(int width, int height) +{ + CGFloat fw = (CGFloat)width; + CGFloat fh = (CGFloat)height; + NSWindow* window = (NSWindow*)_window; + NSRect frame = [window frame]; + CGFloat oldHeight = frame.size.height; + CGFloat heightDelta = fh - oldHeight; + frame.size = CGSizeMake(fw, fh); + frame.origin.y -= heightDelta; + [window setFrame: frame display: YES]; +} + +void WebWindow::GetAllMonitors(GetAllMonitorsCallback callback) +{ + if (callback) + { + for (NSScreen* screen in [NSScreen screens]) + { + Monitor props = {}; + NSRect frame = [screen frame]; + props.monitor.x = (int)roundf(frame.origin.x); + props.monitor.y = (int)roundf(frame.origin.y); + props.monitor.width = (int)roundf(frame.size.width); + props.monitor.height = (int)roundf(frame.size.height); + NSRect vframe = [screen visibleFrame]; + props.work.x = (int)roundf(vframe.origin.x); + props.work.y = (int)roundf(vframe.origin.y); + props.work.width = (int)roundf(vframe.size.width); + props.work.height = (int)roundf(vframe.size.height); + callback(&props); + } + } +} + +unsigned int WebWindow::GetScreenDpi() +{ + return 72; +} + +void WebWindow::GetPosition(int* x, int* y) +{ + NSWindow* window = (NSWindow*)_window; + NSRect frame = [window frame]; + if (x) *x = (int)roundf(frame.origin.x); + if (y) *y = (int)roundf(-frame.size.height - frame.origin.y); // It will be negative, because macOS measures from bottom-left. For x-plat consistency, we want increasing this value to mean moving down. +} + +void WebWindow::SetPosition(int x, int y) +{ + NSWindow* window = (NSWindow*)_window; + NSRect frame = [window frame]; + frame.origin.x = (CGFloat)x; + frame.origin.y = -frame.size.height - (CGFloat)y; + [window setFrame: frame display: YES]; +} + +void WebWindow::SetTopmost(bool topmost) +{ + NSWindow* window = (NSWindow*)_window; + if (topmost) [window setLevel:NSFloatingWindowLevel]; + else [window setLevel:NSNormalWindowLevel]; +} + +void WebWindow::SetIconFile(AutoString filename) +{ + NSString* path = [[NSString stringWithUTF8String:filename] autorelease]; + NSImage* icon = [[NSImage alloc] initWithContentsOfFile:path]; + if (icon != nil) + { + NSWindow* window = (NSWindow*)_window; + [[window standardWindowButton:NSWindowDocumentIconButton] setImage:icon]; + } +} + #endif diff --git a/src/WebWindow.Native/WebWindow.Windows.cpp b/src/WebWindow.Native/WebWindow.Windows.cpp index ba8d802..f56db8c 100644 --- a/src/WebWindow.Native/WebWindow.Windows.cpp +++ b/src/WebWindow.Native/WebWindow.Windows.cpp @@ -6,13 +6,13 @@ #include #include #include + #define WM_USER_SHOWMESSAGE (WM_USER + 0x0001) #define WM_USER_INVOKE (WM_USER + 0x0002) using namespace Microsoft::WRL; LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); -LPWSTR Utf8ToLPWSTR(UTF8String str); LPCWSTR CLASS_NAME = L"WebWindow"; std::mutex invokeLockMutex; HINSTANCE WebWindow::_hInstance; @@ -27,8 +27,8 @@ struct InvokeWaitInfo struct ShowMessageParams { - LPCWSTR title; - LPCWSTR body; + std::wstring title; + std::wstring body; UINT type; }; @@ -41,21 +41,20 @@ void WebWindow::Register(HINSTANCE hInstance) wc.lpfnWndProc = WindowProc; wc.hInstance = hInstance; wc.lpszClassName = CLASS_NAME; - RegisterClassW(&wc); + RegisterClass(&wc); SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); } -WebWindow::WebWindow(UTF8String title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback) +WebWindow::WebWindow(AutoString title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback) { // Create the window _webMessageReceivedCallback = webMessageReceivedCallback; _parent = parent; - LPCWSTR wtitle = Utf8ToLPWSTR(title); - _hWnd = CreateWindowExW( + _hWnd = CreateWindowEx( 0, // Optional window styles. CLASS_NAME, // Window class - wtitle, // Window text + title, // Window text WS_OVERLAPPEDWINDOW, // Window style // Size and position @@ -66,10 +65,12 @@ WebWindow::WebWindow(UTF8String title, WebWindow* parent, WebMessageReceivedCall _hInstance, // Instance handle this // Additional application data ); - delete[] wtitle; hwndToWebWindow[_hWnd] = this; } +// Needn't to release the handles. +WebWindow::~WebWindow() {} + HWND WebWindow::getHwnd() { return _hWnd; @@ -93,9 +94,7 @@ LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) case WM_USER_SHOWMESSAGE: { ShowMessageParams* params = (ShowMessageParams*)wParam; - MessageBoxW(hwnd, params->body, params->title, params->type); - delete params->title; - delete params->body; + MessageBox(hwnd, params->body.c_str(), params->title.c_str(), params->type); delete params; return 0; } @@ -118,10 +117,24 @@ LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) if (webWindow) { webWindow->RefitContent(); + int width, height; + webWindow->GetSize(&width, &height); + webWindow->InvokeResized(width, height); } return 0; } - break; + case WM_MOVE: + { + WebWindow* webWindow = hwndToWebWindow[hwnd]; + if (webWindow) + { + int x, y; + webWindow->GetPosition(&x, &y); + webWindow->InvokeMoved(x, y); + } + return 0; + } + break; } return DefWindowProc(hwnd, uMsg, wParam, lParam); @@ -137,11 +150,9 @@ void WebWindow::RefitContent() } } -void WebWindow::SetTitle(UTF8String title) +void WebWindow::SetTitle(AutoString title) { - LPCWSTR wtitle = Utf8ToLPWSTR(title); - SetWindowTextW(_hWnd, wtitle); - delete[] wtitle; + SetWindowText(_hWnd, title); } void WebWindow::Show() @@ -170,19 +181,19 @@ void WebWindow::WaitForExit() } } -void WebWindow::ShowMessage(UTF8String title, UTF8String body, UINT type) +void WebWindow::ShowMessage(AutoString title, AutoString body, UINT type) { - ShowMessageParams *params = new ShowMessageParams; - params->title = Utf8ToLPWSTR(title); - params->body = Utf8ToLPWSTR(body); + ShowMessageParams* params = new ShowMessageParams; + params->title = title; + params->body = body; params->type = type; - PostMessageW(_hWnd, WM_USER_SHOWMESSAGE, (WPARAM)params, 0); + PostMessage(_hWnd, WM_USER_SHOWMESSAGE, (WPARAM)params, 0); } void WebWindow::Invoke(ACTION callback) { - InvokeWaitInfo waitInfo = {}; - PostMessageW(_hWnd, WM_USER_INVOKE, (WPARAM)callback, (LPARAM)&waitInfo); + InvokeWaitInfo waitInfo = {}; + PostMessage(_hWnd, WM_USER_INVOKE, (WPARAM)callback, (LPARAM)&waitInfo); // Block until the callback is actually executed and completed // TODO: Add return values, exception handling, etc. @@ -190,22 +201,6 @@ void WebWindow::Invoke(ACTION callback) waitInfo.completionNotifier.wait(uLock, [&] { return waitInfo.isCompleted; }); } -LPWSTR Utf8ToLPWSTR(UTF8String str) -{ - int wchars_num = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); - wchar_t* wstr = new wchar_t[wchars_num]; - MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, wchars_num); - return wstr; -} - -UTF8String LPWSTRToUtf8(LPWSTR str) -{ - int utf8chars_num = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL); - char* utf8 = new char[utf8chars_num]; - WideCharToMultiByte(CP_UTF8, 0, str, -1, utf8, utf8chars_num, NULL, NULL); - return utf8; -} - void WebWindow::AttachWebView() { std::atomic_flag flag = ATOMIC_FLAG_INIT; @@ -236,12 +231,9 @@ void WebWindow::AttachWebView() _webviewWindow->AddScriptToExecuteOnDocumentCreated(L"window.external = { sendMessage: function(message) { window.chrome.webview.postMessage(message); }, receiveMessage: function(callback) { window.chrome.webview.addEventListener(\'message\', function(e) { callback(e.data); }); } };", nullptr); _webviewWindow->add_WebMessageReceived(Callback( [this](IWebView2WebView* webview, IWebView2WebMessageReceivedEventArgs* args) -> HRESULT { - PWSTR message; + wil::unique_cotaskmem_string message; args->get_WebMessageAsString(&message); - UTF8String messageUtf8 = LPWSTRToUtf8(message); - _webMessageReceivedCallback(messageUtf8); - delete[] messageUtf8; - CoTaskMemFree(message); + _webMessageReceivedCallback(message.get()); return S_OK; }).Get(), &webMessageToken); @@ -252,42 +244,34 @@ void WebWindow::AttachWebView() IWebView2WebResourceRequest* req; args->get_Request(&req); - LPWSTR uri; + wil::unique_cotaskmem_string uri; req->get_Uri(&uri); - UTF8String uriUtf8 = LPWSTRToUtf8(uri); - - std::string uriString(uriUtf8); - size_t colonPos = uriString.find(':', 0); + std::wstring uriString = uri.get(); + size_t colonPos = uriString.find(L':', 0); if (colonPos > 0) { - std::string scheme = uriString.substr(0, colonPos); + std::wstring scheme = uriString.substr(0, colonPos); WebResourceRequestedCallback handler = _schemeToRequestHandler[scheme]; if (handler != NULL) { int numBytes; - UTF8String contentType; - void* dotNetResponse = handler(uriUtf8, &numBytes, &contentType); + AutoString contentType; + wil::unique_cotaskmem dotNetResponse(handler(uriString.c_str(), &numBytes, &contentType)); if (dotNetResponse != nullptr && contentType != nullptr) { - LPWSTR contentTypeW = Utf8ToLPWSTR(contentType); - std::wstring contentTypeWS(contentTypeW); + std::wstring contentTypeWS = contentType; - IStream* dataStream = SHCreateMemStream((byte*)dotNetResponse, numBytes); + IStream* dataStream = SHCreateMemStream((BYTE*)dotNetResponse.get(), numBytes); wil::com_ptr response; _webviewEnvironment->CreateWebResourceResponse( dataStream, 200, L"OK", (L"Content-Type: " + contentTypeWS).c_str(), &response); args->put_Response(response.get()); - CoTaskMemFree(dotNetResponse); - delete[] contentTypeW; } } } - delete[] uriUtf8; - CoTaskMemFree(uri); - return S_OK; } ).Get(), &webResourceRequestedToken); @@ -319,28 +303,101 @@ void WebWindow::AttachWebView() } } -void WebWindow::NavigateToUrl(UTF8String url) +void WebWindow::NavigateToUrl(AutoString url) { - LPCWSTR urlW = Utf8ToLPWSTR(url); - _webviewWindow->Navigate(urlW); - delete[] urlW; + _webviewWindow->Navigate(url); } -void WebWindow::NavigateToString(UTF8String content) +void WebWindow::NavigateToString(AutoString content) { - LPCWSTR contentW = Utf8ToLPWSTR(content); - _webviewWindow->NavigateToString(contentW); - delete[] contentW; + _webviewWindow->NavigateToString(content); } -void WebWindow::SendMessage(UTF8String message) +void WebWindow::SendMessage(AutoString message) { - LPCWSTR messageW = Utf8ToLPWSTR(message); - _webviewWindow->PostWebMessageAsString(messageW); - delete[] messageW; + _webviewWindow->PostWebMessageAsString(message); } -void WebWindow::AddCustomScheme(UTF8String scheme, WebResourceRequestedCallback requestHandler) +void WebWindow::AddCustomScheme(AutoString scheme, WebResourceRequestedCallback requestHandler) { _schemeToRequestHandler[scheme] = requestHandler; } + +void WebWindow::SetResizable(bool resizable) +{ + LONG_PTR style = GetWindowLongPtr(_hWnd, GWL_STYLE); + if (resizable) style |= WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; + else style &= (~WS_THICKFRAME) & (~WS_MINIMIZEBOX) & (~WS_MAXIMIZEBOX); + SetWindowLongPtr(_hWnd, GWL_STYLE, style); +} + +void WebWindow::GetSize(int* width, int* height) +{ + RECT rect = {}; + GetWindowRect(_hWnd, &rect); + if (width) *width = rect.right - rect.left; + if (height) *height = rect.bottom - rect.top; +} + +void WebWindow::SetSize(int width, int height) +{ + SetWindowPos(_hWnd, HWND_TOP, 0, 0, width, height, SWP_NOMOVE | SWP_NOZORDER); +} + +BOOL MonitorEnum(HMONITOR monitor, HDC, LPRECT, LPARAM arg) +{ + auto callback = (GetAllMonitorsCallback)arg; + MONITORINFO info = {}; + info.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(monitor, &info); + Monitor props = {}; + props.monitor.x = info.rcMonitor.left; + props.monitor.y = info.rcMonitor.top; + props.monitor.width = info.rcMonitor.right - info.rcMonitor.left; + props.monitor.height = info.rcMonitor.bottom - info.rcMonitor.top; + props.work.x = info.rcWork.left; + props.work.y = info.rcWork.top; + props.work.width = info.rcWork.right - info.rcWork.left; + props.work.height = info.rcWork.bottom - info.rcWork.top; + return callback(&props) ? TRUE : FALSE; +} + +void WebWindow::GetAllMonitors(GetAllMonitorsCallback callback) +{ + if (callback) + { + EnumDisplayMonitors(NULL, NULL, MonitorEnum, (LPARAM)callback); + } +} + +unsigned int WebWindow::GetScreenDpi() +{ + return GetDpiForWindow(_hWnd); +} + +void WebWindow::GetPosition(int* x, int* y) +{ + RECT rect = {}; + GetWindowRect(_hWnd, &rect); + if (x) *x = rect.left; + if (y) *y = rect.top; +} + +void WebWindow::SetPosition(int x, int y) +{ + SetWindowPos(_hWnd, HWND_TOP, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER); +} + +void WebWindow::SetTopmost(bool topmost) +{ + SetWindowPos(_hWnd, topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); +} + +void WebWindow::SetIconFile(AutoString filename) +{ + HICON icon = (HICON)LoadImage(NULL, filename, IMAGE_ICON, 0, 0, LR_LOADFROMFILE); + if (icon) + { + ::SendMessage(_hWnd, WM_SETICON, ICON_SMALL, (LPARAM)icon); + } +} diff --git a/src/WebWindow.Native/WebWindow.h b/src/WebWindow.Native/WebWindow.h index e7cab14..f5cf4f7 100644 --- a/src/WebWindow.Native/WebWindow.h +++ b/src/WebWindow.Native/WebWindow.h @@ -1,36 +1,50 @@ -#pragma once -typedef char* UTF8String; +#ifndef WEBWINDOW_H +#define WEBWINDOW_H #ifdef _WIN32 #include -#include +#include #include #include #include -#include "WebView2.h" -typedef void(__stdcall* ACTION)(); -typedef void(__stdcall* WebMessageReceivedCallback)(UTF8String message); -typedef void* (__stdcall *WebResourceRequestedCallback) (UTF8String url, int* outNumBytes, UTF8String *outContentType); +#include +typedef const wchar_t* AutoString; #else - #ifdef OS_LINUX - #include - #endif -typedef void (*ACTION) (); -typedef void (*WebMessageReceivedCallback) (UTF8String message); -typedef void* (*WebResourceRequestedCallback) (UTF8String url, int* outNumBytes, UTF8String* outContentType); +#ifdef OS_LINUX +#include #endif +typedef char* AutoString; +#endif + +struct Monitor +{ + struct MonitorRect + { + int x, y; + int width, height; + } monitor, work; +}; + +typedef void (*ACTION)(); +typedef void (*WebMessageReceivedCallback)(AutoString message); +typedef void* (*WebResourceRequestedCallback)(AutoString url, int* outNumBytes, AutoString* outContentType); +typedef int (*GetAllMonitorsCallback)(const Monitor* monitor); +typedef void (*ResizedCallback)(int width, int height); +typedef void (*MovedCallback)(int x, int y); class WebWindow { private: WebMessageReceivedCallback _webMessageReceivedCallback; + MovedCallback _movedCallback; + ResizedCallback _resizedCallback; #ifdef _WIN32 static HINSTANCE _hInstance; HWND _hWnd; WebWindow* _parent; wil::com_ptr _webviewEnvironment; wil::com_ptr _webviewWindow; - std::map _schemeToRequestHandler; + std::map _schemeToRequestHandler; void AttachWebView(); #elif OS_LINUX GtkWidget* _window; @@ -51,14 +65,30 @@ class WebWindow static void Register(); #endif - WebWindow(UTF8String title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback); - void SetTitle(UTF8String title); + WebWindow(AutoString title, WebWindow* parent, WebMessageReceivedCallback webMessageReceivedCallback); + ~WebWindow(); + void SetTitle(AutoString title); void Show(); void WaitForExit(); - void ShowMessage(UTF8String title, UTF8String body, unsigned int type); + void ShowMessage(AutoString title, AutoString body, unsigned int type); void Invoke(ACTION callback); - void NavigateToUrl(UTF8String url); - void NavigateToString(UTF8String content); - void SendMessage(UTF8String message); - void AddCustomScheme(UTF8String scheme, WebResourceRequestedCallback requestHandler); + void NavigateToUrl(AutoString url); + void NavigateToString(AutoString content); + void SendMessage(AutoString message); + void AddCustomScheme(AutoString scheme, WebResourceRequestedCallback requestHandler); + void SetResizable(bool resizable); + void GetSize(int* width, int* height); + void SetSize(int width, int height); + void SetResizedCallback(ResizedCallback callback) { _resizedCallback = callback; } + void InvokeResized(int width, int height) { if (_resizedCallback) _resizedCallback(width, height); } + void GetAllMonitors(GetAllMonitorsCallback callback); + unsigned int GetScreenDpi(); + void GetPosition(int* x, int* y); + void SetPosition(int x, int y); + void SetMovedCallback(MovedCallback callback) { _movedCallback = callback; } + void InvokeMoved(int x, int y) { if (_movedCallback) _movedCallback(x, y); } + void SetTopmost(bool topmost); + void SetIconFile(AutoString filename); }; + +#endif // !WEBWINDOW_H diff --git a/src/WebWindow/WebWindow.cs b/src/WebWindow/WebWindow.cs index b73597d..49e3b5a 100644 --- a/src/WebWindow/WebWindow.cs +++ b/src/WebWindow/WebWindow.cs @@ -1,33 +1,92 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Threading; namespace WebWindows { + [StructLayout(LayoutKind.Sequential)] + struct NativeRect + { + public int x, y; + public int width, height; + } + + [StructLayout(LayoutKind.Sequential)] + struct NativeMonitor + { + public NativeRect monitor; + public NativeRect work; + } + + public readonly struct Monitor + { + public readonly Rectangle MonitorArea; + public readonly Rectangle WorkArea; + + public Monitor(Rectangle monitor, Rectangle work) + { + MonitorArea = monitor; + WorkArea = work; + } + + internal Monitor(NativeRect monitor, NativeRect work) + : this(new Rectangle(monitor.x, monitor.y, monitor.width, monitor.height), new Rectangle(work.x, work.y, work.width, work.height)) + { } + + internal Monitor(NativeMonitor nativeMonitor) + : this(nativeMonitor.monitor, nativeMonitor.work) + { } + } + public class WebWindow { - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void OnWebMessageReceivedCallback([MarshalAs(UnmanagedType.LPUTF8Str)] string message); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate IntPtr OnWebResourceRequestedCallback([MarshalAs(UnmanagedType.LPUTF8Str)] string url, out int numBytes, [MarshalAs(UnmanagedType.LPUTF8Str)] out string contentType); + // Here we use auto charset instead of forcing UTF-8. + // Thus the native code for Windows will be much more simple. + // Auto charset is UTF-16 on Windows and UTF-8 on Unix(.NET Core 3.0 and later and Mono). + // As we target .NET Standard 2.1, we assume it runs on .NET Core 3.0 and later. + // We should specify using auto charset because the default value is ANSI. + + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto)] delegate void OnWebMessageReceivedCallback(string message); + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto)] delegate IntPtr OnWebResourceRequestedCallback(string url, out int numBytes, out string contentType); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void InvokeCallback(); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate int GetAllMonitorsCallback(in NativeMonitor monitor); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void ResizedCallback(int width, int height); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void MovedCallback(int x, int y); const string DllName = "WebWindow.Native"; - [DllImport(DllName)] static extern IntPtr WebWindow_register_win32(IntPtr hInstance); - [DllImport(DllName)] static extern IntPtr WebWindow_register_mac(); - [DllImport(DllName)] static extern IntPtr WebWindow_ctor([MarshalAs(UnmanagedType.LPUTF8Str)] string title, IntPtr parentWebWindow, IntPtr webMessageReceivedCallback); - [DllImport(DllName)] static extern IntPtr WebWindow_getHwnd_win32(IntPtr instance); - [DllImport(DllName)] static extern void WebWindow_SetTitle(IntPtr instance, [MarshalAs(UnmanagedType.LPUTF8Str)] string title); - [DllImport(DllName)] static extern void WebWindow_Show(IntPtr instance); - [DllImport(DllName)] static extern void WebWindow_WaitForExit(IntPtr instance); - [DllImport(DllName)] static extern void WebWindow_Invoke(IntPtr instance, IntPtr callback); - [DllImport(DllName)] static extern void WebWindow_NavigateToString(IntPtr instance, [MarshalAs(UnmanagedType.LPUTF8Str)] string content); - [DllImport(DllName)] static extern void WebWindow_NavigateToUrl(IntPtr instance, [MarshalAs(UnmanagedType.LPUTF8Str)] string url); - [DllImport(DllName)] static extern void WebWindow_ShowMessage(IntPtr instance, [MarshalAs(UnmanagedType.LPUTF8Str)] string title, [MarshalAs(UnmanagedType.LPUTF8Str)] string body, uint type); - [DllImport(DllName)] static extern void WebWindow_SendMessage(IntPtr instance, [MarshalAs(UnmanagedType.LPUTF8Str)] string message); - [DllImport(DllName)] static extern void WebWindow_AddCustomScheme(IntPtr instance, [MarshalAs(UnmanagedType.LPUTF8Str)] string scheme, IntPtr requestHandler); - - private List _gcHandlesToFree = new List(); - private IntPtr _nativeWebWindow; + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern IntPtr WebWindow_register_win32(IntPtr hInstance); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern IntPtr WebWindow_register_mac(); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] static extern IntPtr WebWindow_ctor(string title, IntPtr parentWebWindow, OnWebMessageReceivedCallback webMessageReceivedCallback); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_dtor(IntPtr instance); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern IntPtr WebWindow_getHwnd_win32(IntPtr instance); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] static extern void WebWindow_SetTitle(IntPtr instance, string title); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_Show(IntPtr instance); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_WaitForExit(IntPtr instance); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_Invoke(IntPtr instance, InvokeCallback callback); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] static extern void WebWindow_NavigateToString(IntPtr instance, string content); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] static extern void WebWindow_NavigateToUrl(IntPtr instance, string url); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] static extern void WebWindow_ShowMessage(IntPtr instance, string title, string body, uint type); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] static extern void WebWindow_SendMessage(IntPtr instance, string message); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] static extern void WebWindow_AddCustomScheme(IntPtr instance, string scheme, OnWebResourceRequestedCallback requestHandler); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_SetResizable(IntPtr instance, int resizable); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_GetSize(IntPtr instance, out int width, out int height); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_SetSize(IntPtr instance, int width, int height); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_SetResizedCallback(IntPtr instance, ResizedCallback callback); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_GetAllMonitors(IntPtr instance, GetAllMonitorsCallback callback); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern uint WebWindow_GetScreenDpi(IntPtr instance); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_GetPosition(IntPtr instance, out int x, out int y); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_SetPosition(IntPtr instance, int x, int y); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_SetMovedCallback(IntPtr instance, MovedCallback callback); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] static extern void WebWindow_SetTopmost(IntPtr instance, int topmost); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Auto)] static extern void WebWindow_SetIconFile(IntPtr instance, string filename); + + private readonly List _gcHandlesToFree = new List(); + private readonly List _hGlobalToFree = new List(); + private readonly IntPtr _nativeWebWindow; + private readonly int _ownerThreadId; private string _title; static WebWindow() @@ -55,6 +114,8 @@ public WebWindow(string title) : this(title, _ => { }) public WebWindow(string title, Action configure) { + _ownerThreadId = Thread.CurrentThread.ManagedThreadId; + if (configure is null) { throw new ArgumentNullException(nameof(configure)); @@ -67,16 +128,23 @@ public WebWindow(string title, Action configure) var onWebMessageReceivedDelegate = (OnWebMessageReceivedCallback)ReceiveWebMessage; _gcHandlesToFree.Add(GCHandle.Alloc(onWebMessageReceivedDelegate)); - var onWebMessageReceivedPtr = Marshal.GetFunctionPointerForDelegate(onWebMessageReceivedDelegate); var parentPtr = options.Parent?._nativeWebWindow ?? default; - _nativeWebWindow = WebWindow_ctor(_title, parentPtr, onWebMessageReceivedPtr); + _nativeWebWindow = WebWindow_ctor(_title, parentPtr, onWebMessageReceivedDelegate); foreach (var (schemeName, handler) in options.SchemeHandlers) { AddCustomScheme(schemeName, handler); } + var onResizedDelegate = (ResizedCallback)OnResized; + _gcHandlesToFree.Add(GCHandle.Alloc(onResizedDelegate)); + WebWindow_SetResizedCallback(_nativeWebWindow, onResizedDelegate); + + var onMovedDelegate = (MovedCallback)OnMoved; + _gcHandlesToFree.Add(GCHandle.Alloc(onMovedDelegate)); + WebWindow_SetMovedCallback(_nativeWebWindow, onMovedDelegate); + // Auto-show to simplify the API, but more importantly because you can't // do things like navigate until it has been shown Show(); @@ -85,11 +153,19 @@ public WebWindow(string title, Action configure) ~WebWindow() { // TODO: IDisposable + WebWindow_SetResizedCallback(_nativeWebWindow, null); + WebWindow_SetMovedCallback(_nativeWebWindow, null); foreach (var gcHandle in _gcHandlesToFree) { gcHandle.Free(); } _gcHandlesToFree.Clear(); + foreach (var handle in _hGlobalToFree) + { + Marshal.FreeHGlobal(handle); + } + _hGlobalToFree.Clear(); + WebWindow_dtor(_nativeWebWindow); } public void Show() => WebWindow_Show(_nativeWebWindow); @@ -112,9 +188,15 @@ public void ShowMessage(string title, string body) public void Invoke(Action workItem) { - var fnPtr = Marshal.GetFunctionPointerForDelegate(workItem); - WebWindow_Invoke(_nativeWebWindow, fnPtr); - GC.KeepAlive(fnPtr); + // If we're already on the UI thread, no need to dispatch + if (Thread.CurrentThread.ManagedThreadId == _ownerThreadId) + { + workItem(); + } + else + { + WebWindow_Invoke(_nativeWebWindow, workItem.Invoke); + } } public IntPtr Hwnd @@ -172,7 +254,7 @@ private void WriteTitleField(string value) _title = value; } - private void ReceiveWebMessage([MarshalAs(UnmanagedType.LPUTF8Str)] string message) + private void ReceiveWebMessage(string message) { OnWebMessageReceived?.Invoke(this, message); } @@ -203,14 +285,190 @@ private void AddCustomScheme(string scheme, ResolveWebResourceDelegate requestHa numBytes = (int)ms.Position; var buffer = Marshal.AllocHGlobal(numBytes); Marshal.Copy(ms.GetBuffer(), 0, buffer, numBytes); + _hGlobalToFree.Add(buffer); return buffer; } }; _gcHandlesToFree.Add(GCHandle.Alloc(callback)); + WebWindow_AddCustomScheme(_nativeWebWindow, scheme, callback); + } + + private bool _resizable = true; + public bool Resizable + { + get => _resizable; + set + { + if (_resizable != value) + { + _resizable = value; + Invoke(() => WebWindow_SetResizable(_nativeWebWindow, _resizable ? 1 : 0)); + } + } + } + + private int _width; + private int _height; + + private void GetSize() => WebWindow_GetSize(_nativeWebWindow, out _width, out _height); + + private void SetSize() => Invoke(() => WebWindow_SetSize(_nativeWebWindow, _width, _height)); + + public int Width + { + get + { + GetSize(); + return _width; + } + set + { + GetSize(); + if (_width != value) + { + _width = value; + SetSize(); + } + } + } + + public int Height + { + get + { + GetSize(); + return _height; + } + set + { + GetSize(); + if (_height != value) + { + _height = value; + SetSize(); + } + } + } + + public Size Size + { + get + { + GetSize(); + return new Size(_width, _height); + } + set + { + if (_width != value.Width || _height != value.Height) + { + _width = value.Width; + _height = value.Height; + SetSize(); + } + } + } + + private void OnResized(int width, int height) => SizeChanged?.Invoke(this, new Size(width, height)); + + public event EventHandler SizeChanged; + + private int _x; + private int _y; + + private void GetPosition() => WebWindow_GetPosition(_nativeWebWindow, out _x, out _y); + + private void SetPosition() => Invoke(() => WebWindow_SetPosition(_nativeWebWindow, _x, _y)); + + public int Left + { + get + { + GetPosition(); + return _x; + } + set + { + GetPosition(); + if (_x != value) + { + _x = value; + SetPosition(); + } + } + } + + public int Top + { + get + { + GetPosition(); + return _y; + } + set + { + GetPosition(); + if (_y != value) + { + _y = value; + SetPosition(); + } + } + } - var callbackPtr = Marshal.GetFunctionPointerForDelegate(callback); - WebWindow_AddCustomScheme(_nativeWebWindow, scheme, callbackPtr); + public Point Location + { + get + { + GetPosition(); + return new Point(_x, _y); + } + set + { + if (_x != value.X || _y != value.Y) + { + _x = value.X; + _y = value.Y; + SetPosition(); + } + } } + + private void OnMoved(int x, int y) => LocationChanged?.Invoke(this, new Point(x, y)); + + public event EventHandler LocationChanged; + + public IReadOnlyList Monitors + { + get + { + List monitors = new List(); + int callback(in NativeMonitor monitor) + { + monitors.Add(new Monitor(monitor)); + return 1; + } + WebWindow_GetAllMonitors(_nativeWebWindow, callback); + return monitors; + } + } + + public uint ScreenDpi => WebWindow_GetScreenDpi(_nativeWebWindow); + + private bool _topmost = false; + public bool Topmost + { + get => _topmost; + set + { + if (_topmost != value) + { + _topmost = value; + Invoke(() => WebWindow_SetTopmost(_nativeWebWindow, _topmost ? 1 : 0)); + } + } + } + + public void SetIconFile(string filename) => WebWindow_SetIconFile(_nativeWebWindow, Path.GetFullPath(filename)); } } diff --git a/src/WebWindow/WebWindow.csproj b/src/WebWindow/WebWindow.csproj index d58a22f..f2a56da 100644 --- a/src/WebWindow/WebWindow.csproj +++ b/src/WebWindow/WebWindow.csproj @@ -20,7 +20,7 @@ + Command="gcc -shared -lstdc++ -DOS_MAC -framework Cocoa -framework WebKit WebWindow.Mac.mm Exports.cpp WebWindow.Mac.AppDelegate.mm WebWindow.Mac.UiDelegate.mm WebWindow.Mac.UrlSchemeHandler.m -o x64/$(Configuration)/WebWindow.Native.dylib" /> diff --git a/testassets/MyBlazorApp/Pages/WindowProp.razor b/testassets/MyBlazorApp/Pages/WindowProp.razor new file mode 100644 index 0000000..2fab8a2 --- /dev/null +++ b/testassets/MyBlazorApp/Pages/WindowProp.razor @@ -0,0 +1,71 @@ +@page "/window" +@inject WebWindow Window + +

Window properties

+

Screen size

+

DPI: @Window.ScreenDpi

+@foreach (var (m, i) in Window.Monitors.Select((monitor, index) => (monitor, index))) +{ +

Monitor @i: Width: @m.MonitorArea.Width, Height: @m.MonitorArea.Height

+} +

Window size

+
+
+ + +
+
+ + +
+
+

Window location

+
+
+ + +
+
+ + +
+
+

Window properties

+
+
+ + +
+
+ + +
+
+

Icon

+
+
+ + +
+
+ +
+
+ +@code { + protected override void OnInitialized() + { + Window.SizeChanged += async (sender, e) => await Task.Run(StateHasChanged); + Window.LocationChanged += async (sender, e) => await Task.Run(StateHasChanged); + } + + string iconFilename; + + void ChangeIconFile() + { + if (!string.IsNullOrEmpty(iconFilename)) + { + Window.SetIconFile(iconFilename); + } + } +} diff --git a/testassets/MyBlazorApp/Shared/NavMenu.razor b/testassets/MyBlazorApp/Shared/NavMenu.razor index fbf2526..ae83c2d 100644 --- a/testassets/MyBlazorApp/Shared/NavMenu.razor +++ b/testassets/MyBlazorApp/Shared/NavMenu.razor @@ -22,6 +22,11 @@ Fetch data + diff --git a/testassets/MyBlazorApp/_Imports.razor b/testassets/MyBlazorApp/_Imports.razor index 0661f47..2d99771 100644 --- a/testassets/MyBlazorApp/_Imports.razor +++ b/testassets/MyBlazorApp/_Imports.razor @@ -6,3 +6,4 @@ @using Microsoft.JSInterop @using MyBlazorApp @using MyBlazorApp.Shared +@using WebWindows;