From c22e3f84d8893716593742b6d14bfed1a89b18f7 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Tue, 13 Feb 2018 17:34:07 +0000 Subject: [PATCH 01/52] Update README.md Changed the link in the comment --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c1f36b..77b0dcd 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ class Program }; // Publishing makes an object available at a given URL - // The user should be directed to http://localhost:8080/button + // The user should be directed to http://localhost:8080/shared-button UI.Publish ("/shared-button", button); // Don't exit the app until someone hits return From 7267bb2687255488318ac0f3dd00cb5e92b92185 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Wed, 21 Feb 2018 15:40:12 -0800 Subject: [PATCH 02/52] Don't keep ASP.NET sessions around --- Ooui.AspNetCore/WebSocketHandler.cs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Ooui.AspNetCore/WebSocketHandler.cs b/Ooui.AspNetCore/WebSocketHandler.cs index c93d3c4..a699176 100644 --- a/Ooui.AspNetCore/WebSocketHandler.cs +++ b/Ooui.AspNetCore/WebSocketHandler.cs @@ -11,21 +11,21 @@ namespace Ooui.AspNetCore { public static string WebSocketPath { get; set; } = "/ooui.ws"; - public static TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes (5); + public static TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes (1); - static readonly ConcurrentDictionary activeSessions = - new ConcurrentDictionary (); + static readonly ConcurrentDictionary pendingSessions = + new ConcurrentDictionary (); public static string BeginSession (HttpContext context, Element element) { var id = Guid.NewGuid ().ToString ("N"); - var s = new ActiveSession { + var s = new PendingSession { Element = element, - LastConnectTimeUtc = DateTime.UtcNow, + CreateTimeUtc = DateTime.UtcNow, }; - if (!activeSessions.TryAdd (id, s)) { + if (!pendingSessions.TryAdd (id, s)) { throw new Exception ("Failed to schedule pending session"); } @@ -62,19 +62,18 @@ namespace Ooui.AspNetCore // // Clear old sessions // - var toClear = activeSessions.Where (x => (DateTime.UtcNow - x.Value.LastConnectTimeUtc) > SessionTimeout).ToList (); + var toClear = pendingSessions.Where (x => (DateTime.UtcNow - x.Value.CreateTimeUtc) > SessionTimeout).ToList (); foreach (var c in toClear) { - activeSessions.TryRemove (c.Key, out var _); + pendingSessions.TryRemove (c.Key, out var _); } // // Find the pending session // - if (!activeSessions.TryGetValue (id, out var activeSession)) { + if (!pendingSessions.TryRemove (id, out var activeSession)) { BadRequest ("Unknown `id`"); return; } - activeSession.LastConnectTimeUtc = DateTime.UtcNow; // // Set the element's dimensions @@ -102,10 +101,10 @@ namespace Ooui.AspNetCore await session.RunAsync ().ConfigureAwait (false); } - class ActiveSession + class PendingSession { public Element Element; - public DateTime LastConnectTimeUtc; + public DateTime CreateTimeUtc; } } } From 7f1a4363ef75c857c0b1cea3fc40b898668329a9 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Wed, 21 Feb 2018 15:47:46 -0800 Subject: [PATCH 03/52] Don't allow browsers to cache the ASP.NET page This allows the WS URL to be re-generated for each session. Fixes #83 --- Ooui.AspNetCore/ElementResult.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Ooui.AspNetCore/ElementResult.cs b/Ooui.AspNetCore/ElementResult.cs index e918d73..03f9975 100644 --- a/Ooui.AspNetCore/ElementResult.cs +++ b/Ooui.AspNetCore/ElementResult.cs @@ -22,6 +22,7 @@ namespace Ooui.AspNetCore var response = context.HttpContext.Response; response.StatusCode = 200; response.ContentType = "text/html; charset=utf-8"; + response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; if (element.WantsFullScreen) { element.Style.Width = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowWidth", 32, 640, 10000); From 00d14adcf1b5e01bdb5e98fabcededdb62845178 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Wed, 21 Feb 2018 16:32:12 -0800 Subject: [PATCH 04/52] Add HeadHtml, BodyHeaderHtml, and BodyFooterHtml to UI Fixes #78 --- Ooui/UI.cs | 56 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 540f74c..5445e51 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -34,24 +34,9 @@ namespace Ooui public static byte[] ClientJsBytes => clientJsBytes; public static string ClientJsEtag => clientJsEtag; - public static string Template { get; set; } = $@" - - - @Title - - - - - - -
-@InitialHtml -
- - - - -"; + public static string HeadHtml { get; set; } = @""; + public static string BodyHeaderHtml { get; set; } = @""; + public static string BodyFooterHtml { get; set; } = @""; static string host = "*"; public static string Host { @@ -392,7 +377,40 @@ namespace Ooui public static string RenderTemplate (string webSocketPath, string title = "", string initialHtml = "") { - return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title).Replace ("@InitialHtml", initialHtml); + using (var w = new System.IO.StringWriter ()) { + RenderTemplate (w, webSocketPath, title, initialHtml); + return w.ToString (); + } + } + + public static void RenderTemplate (TextWriter writer, string webSocketPath, string title, string initialHtml) + { + writer.Write (@" + + + "); + writer.Write (title.Replace ("&", "&").Replace ("<", "<")); + writer.Write (@" + + "); + writer.WriteLine (HeadHtml); + writer.WriteLine (@" + +"); + writer.WriteLine (BodyHeaderHtml); + writer.WriteLine (@"
"); + writer.WriteLine (initialHtml); + writer.Write (@"
+ + +"); + writer.WriteLine (BodyFooterHtml); + writer.WriteLine (@" +"); } class DataHandler : RequestHandler From a0e45ab61ad00d009b2dc43b6caa8acad0f90712 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 12:56:32 -0800 Subject: [PATCH 05/52] Add netstandard1.0 (PCL) support --- Ooui/EventTarget.cs | 5 +- Ooui/Node.cs | 35 ++++++- Ooui/Ooui.csproj | 8 +- Ooui/Platform.cs | 231 +++++++++++++++++++++++--------------------- Ooui/Style.cs | 20 ++++ Ooui/UI.cs | 19 ++-- 6 files changed, 195 insertions(+), 123 deletions(-) diff --git a/Ooui/EventTarget.cs b/Ooui/EventTarget.cs index a37b69a..f13a255 100644 --- a/Ooui/EventTarget.cs +++ b/Ooui/EventTarget.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace Ooui { @@ -21,7 +22,7 @@ namespace Ooui public IReadOnlyList StateMessages { get { lock (stateMessages) { - return new List (stateMessages).AsReadOnly (); + return new ReadOnlyList (stateMessages); } } } @@ -242,7 +243,7 @@ namespace Ooui public override bool CanConvert (Type objectType) { - return typeof (EventTarget).IsAssignableFrom (objectType); + return typeof (EventTarget).GetTypeInfo ().IsAssignableFrom (objectType.GetTypeInfo ()); } } diff --git a/Ooui/Node.cs b/Ooui/Node.cs index 374c016..efea672 100644 --- a/Ooui/Node.cs +++ b/Ooui/Node.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -11,7 +12,7 @@ namespace Ooui public IReadOnlyList Children { get { lock (children) { - return new List (children).AsReadOnly (); + return new ReadOnlyList (children); } } } @@ -35,7 +36,7 @@ namespace Ooui protected Node (string tagName) : base (tagName) - { + { } public override EventTarget GetElementById (string id) @@ -84,7 +85,7 @@ namespace Ooui return null; lock (children) { if (!children.Remove (child)) { - throw new ArgumentException ("Child not contained in this element", nameof(child)); + throw new ArgumentException ("Child not contained in this element", nameof (child)); } } child.MessageSent -= HandleChildMessageSent; @@ -130,8 +131,8 @@ namespace Ooui case MessageType.Call when message.Key == "removeChild" && message.Value is Array ma && ma.Length == 1: UpdateStateMessages (state => { var mchild = ma.GetValue (0); - Node nextChild = null; - for (var i = 0; i < state.Count; ) { + Node nextChild = null; + for (var i = 0; i < state.Count;) { var x = state[i]; if (x.Key == "insertBefore" && x.Value is Array xa && xa.Length == 2 && ReferenceEquals (xa.GetValue (0), mchild)) { // Remove any inserts for this node @@ -200,4 +201,28 @@ namespace Ooui public abstract void WriteOuterHtml (System.Xml.XmlWriter w); } + + class ReadOnlyList : IReadOnlyList + { + readonly List list; + + public ReadOnlyList (List items) + { + list = new List (items); + } + + T IReadOnlyList.this[int index] => list[index]; + + int IReadOnlyCollection.Count => list.Count; + + IEnumerator IEnumerable.GetEnumerator () + { + return ((IEnumerable)list).GetEnumerator (); + } + + IEnumerator IEnumerable.GetEnumerator () + { + return ((IEnumerable)list).GetEnumerator (); + } + } } diff --git a/Ooui/Ooui.csproj b/Ooui/Ooui.csproj index 98357fe..5dcf5cd 100644 --- a/Ooui/Ooui.csproj +++ b/Ooui/Ooui.csproj @@ -8,12 +8,18 @@ https://github.com/praeclarum/Ooui https://github.com/praeclarum/Ooui/blob/master/LICENSE https://github.com/praeclarum/Ooui.git - netstandard2.0 + netstandard2.0;netstandard1.0 + + + PCL + + + diff --git a/Ooui/Platform.cs b/Ooui/Platform.cs index 287bfa4..cb6c85e 100644 --- a/Ooui/Platform.cs +++ b/Ooui/Platform.cs @@ -7,131 +7,139 @@ namespace Ooui { static class Platform { - static readonly Assembly iosAssembly; - static readonly Type iosUIViewControllerType; - static readonly Type iosUIApplicationType; - static readonly Type iosUIWebViewType; - static readonly Type iosNSUrl; - static readonly Type iosNSUrlRequest; +#if PCL - static readonly Assembly androidAssembly; - static readonly Type androidActivityType; - static readonly Type androidWebViewType; + public static void OpenBrowser (string url, object presenter) + { + } - static Platform () - { - var asms = AppDomain.CurrentDomain.GetAssemblies ().ToDictionary ( - x => x.GetName ().Name); +#else - asms.TryGetValue ("Xamarin.iOS", out iosAssembly); - if (iosAssembly != null) { - iosUIViewControllerType = iosAssembly.GetType ("UIKit.UIViewController"); - iosUIApplicationType = iosAssembly.GetType ("UIKit.UIApplication"); - iosUIWebViewType = iosAssembly.GetType ("UIKit.UIWebView"); - iosNSUrl = iosAssembly.GetType ("Foundation.NSUrl"); - iosNSUrlRequest = iosAssembly.GetType ("Foundation.NSUrlRequest"); - } + static readonly Assembly iosAssembly; + static readonly Type iosUIViewControllerType; + static readonly Type iosUIApplicationType; + static readonly Type iosUIWebViewType; + static readonly Type iosNSUrl; + static readonly Type iosNSUrlRequest; - asms.TryGetValue ("Mono.Android", out androidAssembly); - if (androidAssembly != null) { - androidActivityType = androidAssembly.GetType ("Android.App.Activity"); - androidWebViewType = androidAssembly.GetType ("Android.Webkit.WebView"); - } - } + static readonly Assembly androidAssembly; + static readonly Type androidActivityType; + static readonly Type androidWebViewType; - public static void OpenBrowser (string url, object presenter) - { - if (iosAssembly != null) { - OpenBrowserOniOS (url, presenter); - } - else if (androidAssembly != null) { - OpenBrowserOnAndroid (url, presenter); - } - else { - StartBrowserProcess (url); - } - } + static Platform () + { + var asms = AppDomain.CurrentDomain.GetAssemblies ().ToDictionary ( + x => x.GetName ().Name); - static void OpenBrowserOnAndroid (string url, object presenter) - { - var presenterType = GetObjectType (presenter); + asms.TryGetValue ("Xamarin.iOS", out iosAssembly); + if (iosAssembly != null) { + iosUIViewControllerType = iosAssembly.GetType ("UIKit.UIViewController"); + iosUIApplicationType = iosAssembly.GetType ("UIKit.UIApplication"); + iosUIWebViewType = iosAssembly.GetType ("UIKit.UIWebView"); + iosNSUrl = iosAssembly.GetType ("Foundation.NSUrl"); + iosNSUrlRequest = iosAssembly.GetType ("Foundation.NSUrlRequest"); + } - object presenterWebView = null; - if (presenter != null && androidWebViewType.IsAssignableFrom (presenterType)) { - presenterWebView = presenter; - } + asms.TryGetValue ("Mono.Android", out androidAssembly); + if (androidAssembly != null) { + androidActivityType = androidAssembly.GetType ("Android.App.Activity"); + androidWebViewType = androidAssembly.GetType ("Android.Webkit.WebView"); + } + } - if (presenterWebView == null) { - throw new ArgumentException ("Presenter must be a WebView", nameof(presenter)); - } + public static void OpenBrowser (string url, object presenter) + { + if (iosAssembly != null) { + OpenBrowserOniOS (url, presenter); + } + else if (androidAssembly != null) { + OpenBrowserOnAndroid (url, presenter); + } + else { + StartBrowserProcess (url); + } + } - var m = androidWebViewType.GetMethod ("LoadUrl", BindingFlags.Public|BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof(string) }, null); - m.Invoke (presenterWebView, new object[] { url }); - } + static void OpenBrowserOnAndroid (string url, object presenter) + { + var presenterType = GetObjectType (presenter); - static void OpenBrowserOniOS (string url, object presenter) - { - var presenterType = GetObjectType (presenter); + object presenterWebView = null; + if (presenter != null && androidWebViewType.IsAssignableFrom (presenterType)) { + presenterWebView = presenter; + } - // - // Find a presenter view controller - // 1. Try the given presenter - // 2. Find the key window vc - // 3. Create a window? - // - object presenterViewController = null; - if (presenter != null && iosUIViewControllerType.IsAssignableFrom (presenterType)) { - presenterViewController = presenter; - } + if (presenterWebView == null) { + throw new ArgumentException ("Presenter must be a WebView", nameof (presenter)); + } - if (presenterViewController == null) { - var app = iosUIApplicationType.GetProperty ("SharedApplication").GetValue (null, null); - var window = iosUIApplicationType.GetProperty ("KeyWindow").GetValue (app, null); - if (window != null) { - var rvc = window.GetType ().GetProperty ("RootViewController").GetValue (window, null); - if (rvc != null) { - var pvc = rvc.GetType ().GetProperty ("PresentedViewController").GetValue (rvc, null); - presenterViewController = pvc ?? rvc; - } - } - } + var m = androidWebViewType.GetMethod ("LoadUrl", BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof (string) }, null); + m.Invoke (presenterWebView, new object[] { url }); + } - if (presenterViewController == null) { - throw new InvalidOperationException ("Cannot find a view controller from which to present"); - } + static void OpenBrowserOniOS (string url, object presenter) + { + var presenterType = GetObjectType (presenter); - // - // Create the browser - // - var browserVC = Activator.CreateInstance (iosUIViewControllerType); - var browserV = Activator.CreateInstance (iosUIWebViewType); + // + // Find a presenter view controller + // 1. Try the given presenter + // 2. Find the key window vc + // 3. Create a window? + // + object presenterViewController = null; + if (presenter != null && iosUIViewControllerType.IsAssignableFrom (presenterType)) { + presenterViewController = presenter; + } - var nsUrl = iosNSUrl.GetMethod ("FromString").Invoke (null, new object[] { url }); - var nsUrlRequest = iosNSUrlRequest.GetMethod ("FromUrl").Invoke (null, new object[] { nsUrl }); - iosUIWebViewType.GetMethod ("LoadRequest").Invoke (browserV, new object[] { nsUrlRequest }); - iosUIViewControllerType.GetProperty ("View").SetValue (browserVC, browserV, null); + if (presenterViewController == null) { + var app = iosUIApplicationType.GetProperty ("SharedApplication").GetValue (null, null); + var window = iosUIApplicationType.GetProperty ("KeyWindow").GetValue (app, null); + if (window != null) { + var rvc = window.GetType ().GetProperty ("RootViewController").GetValue (window, null); + if (rvc != null) { + var pvc = rvc.GetType ().GetProperty ("PresentedViewController").GetValue (rvc, null); + presenterViewController = pvc ?? rvc; + } + } + } - var m = iosUIViewControllerType.GetMethod ("PresentViewController"); + if (presenterViewController == null) { + throw new InvalidOperationException ("Cannot find a view controller from which to present"); + } - // Console.WriteLine (presenterViewController); - // Console.WriteLine (browserVC); - m.Invoke (presenterViewController, new object[] { browserVC, false, null }); - } + // + // Create the browser + // + var browserVC = Activator.CreateInstance (iosUIViewControllerType); + var browserV = Activator.CreateInstance (iosUIWebViewType); - static Type GetObjectType (object o) - { - var t = typeof (object); - if (o is IReflectableType rt) { - t = rt.GetTypeInfo ().AsType (); - } - else if (o != null) { - t = o.GetType (); - } - return t; - } + var nsUrl = iosNSUrl.GetMethod ("FromString").Invoke (null, new object[] { url }); + var nsUrlRequest = iosNSUrlRequest.GetMethod ("FromUrl").Invoke (null, new object[] { nsUrl }); + iosUIWebViewType.GetMethod ("LoadRequest").Invoke (browserV, new object[] { nsUrlRequest }); + iosUIViewControllerType.GetProperty ("View").SetValue (browserVC, browserV, null); - static Process StartBrowserProcess (string url) - { + var m = iosUIViewControllerType.GetMethod ("PresentViewController"); + + // Console.WriteLine (presenterViewController); + // Console.WriteLine (browserVC); + m.Invoke (presenterViewController, new object[] { browserVC, false, null }); + } + + static Type GetObjectType (object o) + { + var t = typeof (object); + if (o is IReflectableType rt) { + t = rt.GetTypeInfo ().AsType (); + } + else if (o != null) { + t = o.GetType (); + } + return t; + } + + static void StartBrowserProcess (string url) + { // var vs = Environment.GetEnvironmentVariables (); // foreach (System.Collections.DictionaryEntry kv in vs) { // System.Console.WriteLine($"K={kv.Key}, V={kv.Value}"); @@ -139,9 +147,14 @@ namespace Ooui // Console.WriteLine ($"Process.Start {cmd} {args}"); - return Environment.OSVersion.Platform == PlatformID.Unix - ? Process.Start ("open", url) - : Process.Start (new ProcessStartInfo (url) { UseShellExecute = true }); + if (Environment.OSVersion.Platform == PlatformID.Unix) { + Process.Start ("open", url); + } + else { + Process.Start (new ProcessStartInfo (url) { UseShellExecute = true }); + } } + +#endif } } diff --git a/Ooui/Style.cs b/Ooui/Style.cs index 71f3177..c35f499 100644 --- a/Ooui/Style.cs +++ b/Ooui/Style.cs @@ -412,8 +412,19 @@ namespace Ooui return null; if (val is string s) return s; + + if (val is int i) + return i + units; + if (val is double d) + return d.ToString (System.Globalization.CultureInfo.InvariantCulture) + units; + if (val is float f) + return f.ToString (System.Globalization.CultureInfo.InvariantCulture) + units; + +#if !PCL if (val is IConvertible c) return c.ToString (System.Globalization.CultureInfo.InvariantCulture) + units; +#endif + return val.ToString (); } @@ -431,8 +442,17 @@ namespace Ooui return num; } + if (v is int i) + return i; + if (v is double d) + return d; + if (v is float f) + return f; + +#if !PCL if (v is IConvertible c) return c.ToDouble (System.Globalization.CultureInfo.InvariantCulture); +#endif return 0; } diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 5445e51..5d55082 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -6,12 +6,17 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Net; + +#if !PCL using System.Net.WebSockets; +#endif namespace Ooui { public static class UI { +#if !PCL + static readonly ManualResetEvent started = new ManualResetEvent (false); [ThreadStatic] @@ -22,12 +27,6 @@ namespace Ooui static readonly Dictionary publishedPaths = new Dictionary (); - static readonly Dictionary styles = - new Dictionary (); - static readonly StyleSelectors rules = new StyleSelectors (); - - public static StyleSelectors Styles => rules; - static readonly byte[] clientJsBytes; static readonly string clientJsEtag; @@ -809,6 +808,14 @@ namespace Ooui } } +#endif + + static readonly Dictionary styles = + new Dictionary (); + static readonly StyleSelectors rules = new StyleSelectors (); + + public static StyleSelectors Styles => rules; + public class StyleSelectors { public Style this[string selector] { From 91627643e276cbcb789fded4284f5b196940a0e2 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 15:14:51 -0800 Subject: [PATCH 06/52] Add WebAssembly session --- Ooui.AspNetCore/WebSocketHandler.cs | 2 +- Ooui.Forms/Forms.cs | 2 +- Ooui/Session.cs | 103 +++++++++++ Ooui/UI.cs | 255 +++++----------------------- Ooui/WebAssemblySession.cs | 84 +++++++++ Ooui/WebSocketSession.cs | 205 ++++++++++++++++++++++ 6 files changed, 433 insertions(+), 218 deletions(-) create mode 100644 Ooui/Session.cs create mode 100644 Ooui/WebAssemblySession.cs create mode 100644 Ooui/WebSocketSession.cs diff --git a/Ooui.AspNetCore/WebSocketHandler.cs b/Ooui.AspNetCore/WebSocketHandler.cs index a699176..03a7794 100644 --- a/Ooui.AspNetCore/WebSocketHandler.cs +++ b/Ooui.AspNetCore/WebSocketHandler.cs @@ -97,7 +97,7 @@ namespace Ooui.AspNetCore // var token = CancellationToken.None; var webSocket = await context.WebSockets.AcceptWebSocketAsync ("ooui"); - var session = new Ooui.UI.Session (webSocket, activeSession.Element, w, h, token); + var session = new Ooui.WebSocketSession (webSocket, activeSession.Element, w, h, token); await session.RunAsync ().ConfigureAwait (false); } diff --git a/Ooui.Forms/Forms.cs b/Ooui.Forms/Forms.cs index a2b1f37..c648cfa 100644 --- a/Ooui.Forms/Forms.cs +++ b/Ooui.Forms/Forms.cs @@ -133,7 +133,7 @@ namespace Xamarin.Forms { if (timer != null) return; - var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.Session.MaxFps); + var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.MaxFps); timer = new Timer ((_ => { this.SendSignals (); }), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds); diff --git a/Ooui/Session.cs b/Ooui/Session.cs new file mode 100644 index 0000000..029c530 --- /dev/null +++ b/Ooui/Session.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; + +namespace Ooui +{ + public abstract class Session + { + protected readonly Element element; + protected readonly double initialWidth; + protected readonly double initialHeight; + + protected readonly HashSet createdIds; + + protected readonly List queuedMessages = new List (); + + public Session (Element element, double initialWidth, double initialHeight) + { + this.element = element; + this.initialWidth = initialWidth; + this.initialHeight = initialHeight; + + // + // Keep a list of all the elements for which we've transmitted the initial state + // + createdIds = new HashSet { + "window", + "document", + "document.body", + }; + } + + void QueueStateMessagesLocked (EventTarget target) + { + if (target == null) return; + var created = false; + foreach (var m in target.StateMessages) { + if (m.MessageType == MessageType.Create) { + createdIds.Add (m.TargetId); + created = true; + } + if (created) { + QueueMessageLocked (m); + } + } + } + + void QueueMessageLocked (Message message) + { + // + // Make sure all the referenced objects have been created + // + if (!createdIds.Contains (message.TargetId)) { + QueueStateMessagesLocked (element.GetElementById (message.TargetId)); + } + if (message.Value is EventTarget ve) { + if (!createdIds.Contains (ve.Id)) { + QueueStateMessagesLocked (ve); + } + } + else if (message.Value is Array a) { + for (var i = 0; i < a.Length; i++) { + // Console.WriteLine ($"A{i} = {a.GetValue(i)}"); + if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) { + QueueStateMessagesLocked (e); + } + } + } + + // + // Add it to the queue + // + //Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}"); + queuedMessages.Add (message); + } + + protected virtual void QueueMessage (Message message) + { + lock (queuedMessages) { + QueueMessageLocked (message); + } + } + + protected void Error (string message, Exception ex) + { +#if PCL + System.Diagnostics.Debug.WriteLine (string.Format ("{0}: {1}", message, ex)); +#else + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine ("{0}: {1}", message, ex); + Console.ResetColor (); +#endif + } + + protected void Info (string message) + { +#if PCL + System.Diagnostics.Debug.WriteLine (message); +#else + Console.WriteLine (message); +#endif + } + } +} diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 5d55082..aff0c38 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -7,16 +7,13 @@ using System.Threading; using System.Threading.Tasks; using System.Net; -#if !PCL -using System.Net.WebSockets; -#endif - namespace Ooui { public static class UI { -#if !PCL + public const int MaxFps = 30; +#if !PCL static readonly ManualResetEvent started = new ManualResetEvent (false); [ThreadStatic] @@ -538,8 +535,8 @@ namespace Ooui // // Connect the web socket // - WebSocketContext webSocketContext = null; - WebSocket webSocket = null; + System.Net.WebSockets.WebSocketContext webSocketContext = null; + System.Net.WebSockets.WebSocket webSocket = null; try { webSocketContext = await listenerContext.AcceptWebSocketAsync (subProtocol: "ooui").ConfigureAwait (false); webSocket = webSocketContext.WebSocket; @@ -577,10 +574,10 @@ namespace Ooui // Create a new session and let it handle everything from here // try { - var session = new Session (webSocket, element, w, h, serverToken); + var session = new WebSocketSession (webSocket, element, w, h, serverToken); await session.RunAsync ().ConfigureAwait (false); } - catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { + catch (System.Net.WebSockets.WebSocketException ex) when (ex.WebSocketErrorCode == System.Net.WebSockets.WebSocketError.ConnectionClosedPrematurely) { // The remote party closed the WebSocket connection without completing the close handshake. } catch (Exception ex) { @@ -598,217 +595,43 @@ namespace Ooui Console.ResetColor (); } - public class Session +#endif + + static readonly Dictionary globalElements = new Dictionary (); + static readonly Dictionary globalElementSessions = new Dictionary (); + + public static void SetGlobalElement (string globalElementId, Element element) { - readonly WebSocket webSocket; - readonly Element element; - readonly Action handleElementMessageSent; - - readonly CancellationTokenSource sessionCts = new CancellationTokenSource (); - readonly CancellationTokenSource linkedCts; - readonly CancellationToken token; - - readonly HashSet createdIds; - readonly List queuedMessages = new List (); - - public const int MaxFps = 30; - - readonly System.Timers.Timer sendThrottle; - DateTime lastTransmitTime = DateTime.MinValue; - readonly TimeSpan throttleInterval = TimeSpan.FromSeconds (1.0 / MaxFps); - readonly double initialWidth; - readonly double initialHeight; - - public Session (WebSocket webSocket, Element element, double initialWidth, double initialHeight, CancellationToken serverToken) - { - this.webSocket = webSocket; - this.element = element; - this.initialWidth = initialWidth; - this.initialHeight = initialHeight; - - // - // Create a new session cancellation token that will trigger - // automatically if the server shutsdown or the session shutsdown. - // - linkedCts = CancellationTokenSource.CreateLinkedTokenSource (serverToken, sessionCts.Token); - token = linkedCts.Token; - - // - // Keep a list of all the elements for which we've transmitted the initial state - // - createdIds = new HashSet { - "window", - "document", - "document.body", - }; - - // - // Preparse handlers for the element - // - handleElementMessageSent = QueueMessage; - - // - // Create a timer to use as a throttle when sending messages - // - sendThrottle = new System.Timers.Timer (throttleInterval.TotalMilliseconds); - sendThrottle.Elapsed += (s, e) => { - // System.Console.WriteLine ("TICK SEND THROTTLE FOR {0}", element); - if ((e.SignalTime - lastTransmitTime) >= throttleInterval) { - sendThrottle.Enabled = false; - lastTransmitTime = e.SignalTime; - TransmitQueuedMessages (); - } - }; - } - - public async Task RunAsync () - { - // - // Start watching for changes in the element - // - element.MessageSent += handleElementMessageSent; - - try { - // - // Add it to the document body - // - if (element.WantsFullScreen) { - element.Style.Width = initialWidth; - element.Style.Height = initialHeight; - } - QueueMessage (Message.Call ("document.body", "appendChild", element)); - - // - // Start the Read Loop - // - var receiveBuffer = new byte[64*1024]; - - while (webSocket.State == WebSocketState.Open && !token.IsCancellationRequested) { - var receiveResult = await webSocket.ReceiveAsync(new ArraySegment(receiveBuffer), token).ConfigureAwait (false); - - if (receiveResult.MessageType == WebSocketMessageType.Close) { - await webSocket.CloseAsync (WebSocketCloseStatus.NormalClosure, "", token).ConfigureAwait (false); - sessionCts.Cancel (); - } - else if (receiveResult.MessageType == WebSocketMessageType.Binary) { - await webSocket.CloseAsync (WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", token).ConfigureAwait (false); - sessionCts.Cancel (); - } - else { - var size = receiveResult.Count; - while (!receiveResult.EndOfMessage) { - if (size >= receiveBuffer.Length) { - await webSocket.CloseAsync (WebSocketCloseStatus.MessageTooBig, "Message too big", token).ConfigureAwait (false); - return; - } - receiveResult = await webSocket.ReceiveAsync (new ArraySegment(receiveBuffer, size, receiveBuffer.Length - size), token).ConfigureAwait (false); - size += receiveResult.Count; - } - var receivedString = Encoding.UTF8.GetString (receiveBuffer, 0, size); - - try { - // Console.WriteLine ("RECEIVED: {0}", receivedString); - var message = Newtonsoft.Json.JsonConvert.DeserializeObject (receivedString); - element.Receive (message); - } - catch (Exception ex) { - Error ("Failed to process received message", ex); - } - } - } - } - finally { - element.MessageSent -= handleElementMessageSent; - } - } - - void QueueStateMessagesLocked (EventTarget target) - { - if (target == null) return; - var created = false; - foreach (var m in target.StateMessages) { - if (m.MessageType == MessageType.Create) { - createdIds.Add (m.TargetId); - created = true; - } - if (created) { - QueueMessageLocked (m); - } - } - } - - void QueueMessageLocked (Message message) - { - // - // Make sure all the referenced objects have been created - // - if (!createdIds.Contains (message.TargetId)) { - QueueStateMessagesLocked (element.GetElementById (message.TargetId)); - } - if (message.Value is EventTarget ve) { - if (!createdIds.Contains (ve.Id)) { - QueueStateMessagesLocked (ve); - } - } - else if (message.Value is Array a) { - for (var i = 0; i < a.Length; i++) { - // Console.WriteLine ($"A{i} = {a.GetValue(i)}"); - if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) { - QueueStateMessagesLocked (e); - } - } - } - - // - // Add it to the queue - // - //Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}"); - queuedMessages.Add (message); - } - - void QueueMessage (Message message) - { - lock (queuedMessages) { - QueueMessageLocked (message); - } - sendThrottle.Enabled = true; - } - - async void TransmitQueuedMessages () - { - try { - // - // Dequeue as many messages as we can - // - var messagesToSend = new List (); - System.Runtime.CompilerServices.ConfiguredTaskAwaitable task; - lock (queuedMessages) { - messagesToSend.AddRange (queuedMessages); - queuedMessages.Clear (); - - if (messagesToSend.Count == 0) - return; - - // - // Now actually send this message - // Do this while locked to make sure SendAsync is called in the right order - // - var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend); - var outputBuffer = new ArraySegment (Encoding.UTF8.GetBytes (json)); - //Console.WriteLine ("TRANSMIT " + json); - task = webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false); - } - await task; - } - catch (Exception ex) { - Error ("Failed to send queued messages, aborting session", ex); - element.MessageSent -= handleElementMessageSent; - sessionCts.Cancel (); - } + lock (globalElements) { + globalElements[globalElementId] = element; } } -#endif + public static void StartWebAssemblySession (string sessionId, string globalElementId) + { + Element element; + lock (globalElements) { + if (!globalElements.TryGetValue (globalElementId, out element)) + return; + } + + var g = new WebAssemblySession (sessionId, element, 640, 480); + lock (globalElementSessions) { + globalElementSessions[sessionId] = g; + } + g.StartSession (); + } + + public static void ReceiveWebAssemblySessionMessageJson (string sessionId, string json) + { + WebAssemblySession g; + lock (globalElementSessions) { + if (!globalElementSessions.TryGetValue (sessionId, out g)) + return; + } + g.ReceiveMessageJson (json); + } + static readonly Dictionary styles = new Dictionary (); diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs new file mode 100644 index 0000000..41e8aba --- /dev/null +++ b/Ooui/WebAssemblySession.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; + +namespace Ooui +{ + public class WebAssemblySession : Session + { + readonly string id; + readonly Action handleElementMessageSent; + + public WebAssemblySession (string id, Element element, double initialWidth, double initialHeight) + : base (element, initialWidth, initialHeight) + { + this.id = id; + handleElementMessageSent = QueueMessage; + } + + void TransmitQueuedMessages () + { + // + // Dequeue as many messages as we can + // + var messagesToSend = new List (); + lock (queuedMessages) { + messagesToSend.AddRange (queuedMessages); + queuedMessages.Clear (); + } + + if (messagesToSend.Count == 0) + return; + + // + // Now actually send the messages + // + var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend); + SendMessagesJson (json); + } + + protected override void QueueMessage (Message message) + { + base.QueueMessage (message); + TransmitQueuedMessages (); + } + + void SendMessagesJson (string json) + { + Info ("SEND: " + json); + } + + public void ReceiveMessageJson (string json) + { + try { + Info ("RECEIVED: " + json); + var message = Newtonsoft.Json.JsonConvert.DeserializeObject (json); + element.Receive (message); + } + catch (Exception ex) { + Error ("Failed to process received message", ex); + } + } + + public void StartSession () + { + // + // Start watching for changes in the element + // + element.MessageSent += handleElementMessageSent; + + // + // Add it to the document body + // + if (element.WantsFullScreen) { + element.Style.Width = initialWidth; + element.Style.Height = initialHeight; + } + QueueMessage (Message.Call ("document.body", "appendChild", element)); + } + + public void StopSession () + { + element.MessageSent -= handleElementMessageSent; + } + } +} diff --git a/Ooui/WebSocketSession.cs b/Ooui/WebSocketSession.cs new file mode 100644 index 0000000..3f8e3be --- /dev/null +++ b/Ooui/WebSocketSession.cs @@ -0,0 +1,205 @@ +#if !PCL + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Net; +using System.Net.WebSockets; + +namespace Ooui +{ + public class WebSocketSession : Session + { + readonly WebSocket webSocket; + readonly Action handleElementMessageSent; + + readonly CancellationTokenSource sessionCts = new CancellationTokenSource (); + readonly CancellationTokenSource linkedCts; + readonly CancellationToken token; + + readonly System.Timers.Timer sendThrottle; + DateTime lastTransmitTime = DateTime.MinValue; + readonly TimeSpan throttleInterval = TimeSpan.FromSeconds (1.0 / UI.MaxFps); + + public WebSocketSession (WebSocket webSocket, Element element, double initialWidth, double initialHeight, CancellationToken serverToken) + : base (element, initialWidth, initialHeight) + { + this.webSocket = webSocket; + + // + // Create a new session cancellation token that will trigger + // automatically if the server shutsdown or the session shutsdown. + // + linkedCts = CancellationTokenSource.CreateLinkedTokenSource (serverToken, sessionCts.Token); + token = linkedCts.Token; + + // + // Preparse handlers for the element + // + handleElementMessageSent = QueueMessage; + + // + // Create a timer to use as a throttle when sending messages + // + sendThrottle = new System.Timers.Timer (throttleInterval.TotalMilliseconds); + sendThrottle.Elapsed += (s, e) => { + // System.Console.WriteLine ("TICK SEND THROTTLE FOR {0}", element); + if ((e.SignalTime - lastTransmitTime) >= throttleInterval) { + sendThrottle.Enabled = false; + lastTransmitTime = e.SignalTime; + TransmitQueuedMessages (); + } + }; + } + + public async Task RunAsync () + { + // + // Start watching for changes in the element + // + element.MessageSent += handleElementMessageSent; + + try { + // + // Add it to the document body + // + if (element.WantsFullScreen) { + element.Style.Width = initialWidth; + element.Style.Height = initialHeight; + } + QueueMessage (Message.Call ("document.body", "appendChild", element)); + + // + // Start the Read Loop + // + var receiveBuffer = new byte[64 * 1024]; + + while (webSocket.State == WebSocketState.Open && !token.IsCancellationRequested) { + var receiveResult = await webSocket.ReceiveAsync (new ArraySegment (receiveBuffer), token).ConfigureAwait (false); + + if (receiveResult.MessageType == WebSocketMessageType.Close) { + await webSocket.CloseAsync (WebSocketCloseStatus.NormalClosure, "", token).ConfigureAwait (false); + sessionCts.Cancel (); + } + else if (receiveResult.MessageType == WebSocketMessageType.Binary) { + await webSocket.CloseAsync (WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", token).ConfigureAwait (false); + sessionCts.Cancel (); + } + else { + var size = receiveResult.Count; + while (!receiveResult.EndOfMessage) { + if (size >= receiveBuffer.Length) { + await webSocket.CloseAsync (WebSocketCloseStatus.MessageTooBig, "Message too big", token).ConfigureAwait (false); + return; + } + receiveResult = await webSocket.ReceiveAsync (new ArraySegment (receiveBuffer, size, receiveBuffer.Length - size), token).ConfigureAwait (false); + size += receiveResult.Count; + } + var receivedString = Encoding.UTF8.GetString (receiveBuffer, 0, size); + + try { + // Console.WriteLine ("RECEIVED: {0}", receivedString); + var message = Newtonsoft.Json.JsonConvert.DeserializeObject (receivedString); + element.Receive (message); + } + catch (Exception ex) { + Error ("Failed to process received message", ex); + } + } + } + } + finally { + element.MessageSent -= handleElementMessageSent; + } + } + + void QueueStateMessagesLocked (EventTarget target) + { + if (target == null) return; + var created = false; + foreach (var m in target.StateMessages) { + if (m.MessageType == MessageType.Create) { + createdIds.Add (m.TargetId); + created = true; + } + if (created) { + QueueMessageLocked (m); + } + } + } + + void QueueMessageLocked (Message message) + { + // + // Make sure all the referenced objects have been created + // + if (!createdIds.Contains (message.TargetId)) { + QueueStateMessagesLocked (element.GetElementById (message.TargetId)); + } + if (message.Value is EventTarget ve) { + if (!createdIds.Contains (ve.Id)) { + QueueStateMessagesLocked (ve); + } + } + else if (message.Value is Array a) { + for (var i = 0; i < a.Length; i++) { + // Console.WriteLine ($"A{i} = {a.GetValue(i)}"); + if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) { + QueueStateMessagesLocked (e); + } + } + } + + // + // Add it to the queue + // + //Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}"); + queuedMessages.Add (message); + } + + protected override void QueueMessage (Message message) + { + base.QueueMessage (message); + sendThrottle.Enabled = true; + } + + async void TransmitQueuedMessages () + { + try { + // + // Dequeue as many messages as we can + // + var messagesToSend = new List (); + System.Runtime.CompilerServices.ConfiguredTaskAwaitable task; + lock (queuedMessages) { + messagesToSend.AddRange (queuedMessages); + queuedMessages.Clear (); + + if (messagesToSend.Count == 0) + return; + + // + // Now actually send this message + // Do this while locked to make sure SendAsync is called in the right order + // + var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend); + var outputBuffer = new ArraySegment (Encoding.UTF8.GetBytes (json)); + //Console.WriteLine ("TRANSMIT " + json); + task = webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false); + } + await task; + } + catch (Exception ex) { + Error ("Failed to send queued messages, aborting session", ex); + element.MessageSent -= handleElementMessageSent; + sessionCts.Cancel (); + } + } + } +} + +#endif From 3de84bb20ff2677ce9c1ac57d067d798f00f19d6 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 15:58:17 -0800 Subject: [PATCH 07/52] Add Ooui.Wasm sample --- .gitignore | 13 +++ Ooui.Wasm/README.md | 26 ++++++ Ooui.Wasm/ooui-sample.cs | 16 ++++ Ooui.Wasm/ooui-sample.html | 176 +++++++++++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 Ooui.Wasm/README.md create mode 100644 Ooui.Wasm/ooui-sample.cs create mode 100644 Ooui.Wasm/ooui-sample.html diff --git a/.gitignore b/.gitignore index fd8fca6..7da3b43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,16 @@ + +# Wasm SDK +/Ooui.Wasm/bcl +/Ooui.Wasm/debug +/Ooui.Wasm/release +/Ooui.Wasm/managed +/Ooui.Wasm/driver.c +/Ooui.Wasm/mono.js +/Ooui.Wasm/server.py +/Ooui.Wasm/sample* +/Ooui.Wasm/lib* +/Ooui.Wasm/*.w* + # Social media files /Media diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md new file mode 100644 index 0000000..f2518cf --- /dev/null +++ b/Ooui.Wasm/README.md @@ -0,0 +1,26 @@ + +## Install + +Download [Mono WebAssembly support on Jenkins](https://jenkins.mono-project.com//job/test-mono-mainline-webassembly/) by +grabbing the latest build's Azure Artifact. + +[mono-wasm-03914603a3b.zip](https://jenkins.mono-project.com/job/test-mono-mainline-webassembly/71/label=highsierra/Azure/processDownloadRequest/71/highsierra/sdks/wasm/mono-wasm-03914603a3b.zip) + +Expand that into this directory. + + + +## Build + +```bash +csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/Ooui.dll /out:managed/Ooui.Sample.dll ooui-sample.cs +``` + + +## Run + +```bash +python server.py +``` + +Go to `locahost:8000/ooui.html` diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs new file mode 100644 index 0000000..a080e86 --- /dev/null +++ b/Ooui.Wasm/ooui-sample.cs @@ -0,0 +1,16 @@ +using System; +using Ooui; + +public class Math { + public static string Add (string a, string b) { + try { + Console.WriteLine ("ENTER"); + var e = new Div (); + return e.Id; + } + catch (Exception e) { + Console.WriteLine (e); + return e.ToString (); + } + } +} diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html new file mode 100644 index 0000000..9062d34 --- /dev/null +++ b/Ooui.Wasm/ooui-sample.html @@ -0,0 +1,176 @@ + + + + + + C# output: +
+ +
+ +
+ + + + + \ No newline at end of file From 7bab361a63a3b45f3a52cd9045da3deb83aae3f5 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 16:43:50 -0800 Subject: [PATCH 08/52] Add convenience ctor --- Ooui/Label.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Ooui/Label.cs b/Ooui/Label.cs index ca27a15..8bb063b 100644 --- a/Ooui/Label.cs +++ b/Ooui/Label.cs @@ -13,5 +13,12 @@ namespace Ooui : base ("label") { } + + public Label (string text) + : this () + { + Text = text; + } + } } From 406a08c9ccd8b9085194dea65bed056131d94741 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 16:44:10 -0800 Subject: [PATCH 09/52] Refactor the JS wasm loader --- Ooui.Wasm/README.md | 2 +- Ooui.Wasm/ooui-sample.cs | 15 +++-- Ooui.Wasm/ooui-sample.html | 135 +++++++++++++++++++++---------------- 3 files changed, 87 insertions(+), 65 deletions(-) diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md index f2518cf..d9af42a 100644 --- a/Ooui.Wasm/README.md +++ b/Ooui.Wasm/README.md @@ -13,7 +13,7 @@ Expand that into this directory. ## Build ```bash -csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/Ooui.dll /out:managed/Ooui.Sample.dll ooui-sample.cs +csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/System.Runtime.dll /r:managed/Ooui.dll /out:managed/Ooui.Sample.dll ooui-sample.cs ``` diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs index a080e86..d53d6ad 100644 --- a/Ooui.Wasm/ooui-sample.cs +++ b/Ooui.Wasm/ooui-sample.cs @@ -1,12 +1,17 @@ using System; using Ooui; -public class Math { - public static string Add (string a, string b) { +public class Program +{ + public static string Main (string a0, string a1) + { try { - Console.WriteLine ("ENTER"); - var e = new Div (); - return e.Id; + var l = new Label { Text = "Hello" }; + var b = new Button ("Click Me"); + var e = new Div (new Div (l), b); + + UI.SetGlobalElement ("main", e); + return e.ToString (); } catch (Exception e) { Console.WriteLine (e); diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html index 9062d34..3588989 100644 --- a/Ooui.Wasm/ooui-sample.html +++ b/Ooui.Wasm/ooui-sample.html @@ -1,14 +1,16 @@ - - - - - - C# output: -
- -
- -
+ + + + Ooui Wasm + + + + + +
+

  Loading...

+ +
- - - \ No newline at end of file + + + + From 2d15d2977477b28f05ea32045cd46323d855eec3 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 17:13:27 -0800 Subject: [PATCH 10/52] Add wasm support to client library --- .gitignore | 1 + Ooui.Wasm/ooui-sample.html | 208 ++++++------------------------------- Ooui/Client.js | 161 +++++++++++++++++++++++++++- 3 files changed, 195 insertions(+), 175 deletions(-) diff --git a/.gitignore b/.gitignore index 7da3b43..eaa0c2e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /Ooui.Wasm/managed /Ooui.Wasm/driver.c /Ooui.Wasm/mono.js +/Ooui.Wasm/ooui.js /Ooui.Wasm/server.py /Ooui.Wasm/sample* /Ooui.Wasm/lib* diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html index 3588989..2c9dc7d 100644 --- a/Ooui.Wasm/ooui-sample.html +++ b/Ooui.Wasm/ooui-sample.html @@ -8,185 +8,45 @@
-

  Loading...

+

Loading...

+ diff --git a/Ooui/Client.js b/Ooui/Client.js index e50d972..e89fbe2 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -120,10 +120,35 @@ function ooui (rootElementPath) { if (socket != null) socket.send (ems); if (debug) console.log ("Event", em); - } + } }()); } +function oouiWasm (mainAsmName, mainNamspace, mainClassName, mainMethodNmae, assemblies) +{ + Module.assemblies = assemblies; + + var initialSize = getSize (); + + function send (json) { + if (debug) console.log ("Send", json); + } + + function resizeHandler() { + const em = { + m: "event", + id: "window", + k: "resize", + v: getSize (), + }; + const ems = JSON.stringify (em); + send (ems); + if (debug) console.log ("Event", em); + } + + window.addEventListener ("resize", resizeHandler, false); +} + function getNode (id) { switch (id) { case "window": return window; @@ -294,3 +319,137 @@ function fixupValue (v) { } return v; } + +// == WASM Support == + +var Module = { + onRuntimeInitialized: function () { + console.log ("Done with WASM module instantiation."); + + Module.FS_createPath ("/", "managed", true, true); + + var pending = 0; + this.assemblies.forEach (function(asm_name) { + console.log ("Loading", asm_name); + ++pending; + fetch ("managed/" + asm_name, { credentials: 'same-origin' }).then (function (response) { + if (!response.ok) + throw "failed to load Assembly '" + asm_name + "'"; + return response['arrayBuffer'](); + }).then (function (blob) { + var asm = new Uint8Array (blob); + Module.FS_createDataFile ("managed/" + asm_name, null, asm, true, true, true); + --pending; + if (pending == 0) + Module.bclLoadingDone (); + }); + }); + }, + + bclLoadingDone: function () { + console.log ("Done loading the BCL."); + MonoRuntime.init (); + } +}; + +var MonoRuntime = { + init: function () { + this.load_runtime = Module.cwrap ('mono_wasm_load_runtime', null, ['string', 'number']); + this.assembly_load = Module.cwrap ('mono_wasm_assembly_load', 'number', ['string']); + this.find_class = Module.cwrap ('mono_wasm_assembly_find_class', 'number', ['number', 'string', 'string']); + this.find_method = Module.cwrap ('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']); + this.invoke_method = Module.cwrap ('mono_wasm_invoke_method', 'number', ['number', 'number', 'number']); + this.mono_string_get_utf8 = Module.cwrap ('mono_wasm_string_get_utf8', 'number', ['number']); + this.mono_string = Module.cwrap ('mono_wasm_string_from_js', 'number', ['string']); + + this.load_runtime ("managed", 1); + + console.log ("Done initializing the runtime."); + + WebAssemblyApp.init (); + }, + + conv_string: function (mono_obj) { + if (mono_obj == 0) + return null; + var raw = this.mono_string_get_utf8 (mono_obj); + var res = Module.UTF8ToString (raw); + Module._free (raw); + + return res; + }, + + call_method: function (method, this_arg, args) { + var args_mem = Module._malloc (args.length * 4); + var eh_throw = Module._malloc (4); + for (var i = 0; i < args.length; ++i) + Module.setValue (args_mem + i * 4, args [i], "i32"); + Module.setValue (eh_throw, 0, "i32"); + + var res = this.invoke_method (method, this_arg, args_mem, eh_throw); + + var eh_res = Module.getValue (eh_throw, "i32"); + + Module._free (args_mem); + Module._free (eh_throw); + + if (eh_res != 0) { + var msg = this.conv_string (res); + throw new Error (msg); + } + + return res; + }, +}; + +var WebAssemblyApp = { + init: function () { + this.loading = document.getElementById ("loading"); + this.output = document.getElementById ("output"); + + this.findMethods (); + + var res = this.runApp ("1", "2"); + + this.output.value = res; + this.output.hidden = false; + this.loading.hidden = true; + }, + + runApp: function (a, b) { + try { + MonoRuntime.call_method (this.ooui_method, null, [MonoRuntime.mono_string ("main"), MonoRuntime.mono_string ("main")]); + var res = MonoRuntime.call_method (this.add_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); + return MonoRuntime.conv_string (res); + } catch (e) { + return e.msg; + } + }, + + findMethods: function () { + this.ooui_module = MonoRuntime.assembly_load ("Ooui") + if (!this.ooui_module) + throw "Could not find Ooui.dll"; + + this.ooui_class = MonoRuntime.find_class (this.ooui_module, "Ooui", "UI") + if (!this.ooui_class) + throw "Could not find UI class in Ooui module"; + + this.ooui_method = MonoRuntime.find_method (this.ooui_class, "StartWebAssemblySession", -1) + if (!this.ooui_method) + throw "Could not find StartWebAssemblySession method"; + + this.main_module = MonoRuntime.assembly_load (mainAsmName) + if (!this.main_module) + throw "Could not find Main Module " + mainAsmName + ".dll"; + + this.math_class = MonoRuntime.find_class (this.main_module, "", "Program") + if (!this.math_class) + throw "Could not find Program class in main module"; + + this.add_method = MonoRuntime.find_method (this.math_class, "Main", -1) + if (!this.add_method) + throw "Could not find Main method"; + }, +}; + From 99b229ebedc7dd9b4b02938a5d340de6bfeaf9de Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 18:43:02 -0800 Subject: [PATCH 11/52] Call JS methods for wasm --- Ooui/WebAssemblySession.cs | 47 +++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs index 41e8aba..47199ac 100644 --- a/Ooui/WebAssemblySession.cs +++ b/Ooui/WebAssemblySession.cs @@ -15,8 +15,18 @@ namespace Ooui handleElementMessageSent = QueueMessage; } + protected override void QueueMessage (Message message) + { + WebAssembly.Runtime.InvokeJS ("console.log('q 0')"); + base.QueueMessage (message); + WebAssembly.Runtime.InvokeJS ("console.log('q 1')"); + TransmitQueuedMessages (); + WebAssembly.Runtime.InvokeJS ("console.log('q end')"); + } + void TransmitQueuedMessages () { + WebAssembly.Runtime.InvokeJS ("console.log('t 0')"); // // Dequeue as many messages as we can // @@ -26,25 +36,19 @@ namespace Ooui queuedMessages.Clear (); } + WebAssembly.Runtime.InvokeJS ("console.log('t 1')"); + if (messagesToSend.Count == 0) return; + WebAssembly.Runtime.InvokeJS ("console.log('t 2')"); + // // Now actually send the messages // - var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend); - SendMessagesJson (json); - } - - protected override void QueueMessage (Message message) - { - base.QueueMessage (message); - TransmitQueuedMessages (); - } - - void SendMessagesJson (string json) - { - Info ("SEND: " + json); + //var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend); + WebAssembly.Runtime.InvokeJS ("alert(" + messagesToSend.Count + ")"); + WebAssembly.Runtime.InvokeJS ("console.log('t end')"); } public void ReceiveMessageJson (string json) @@ -61,6 +65,7 @@ namespace Ooui public void StartSession () { + WebAssembly.Runtime.InvokeJS ("console.log('was start session 0')"); // // Start watching for changes in the element // @@ -73,7 +78,9 @@ namespace Ooui element.Style.Width = initialWidth; element.Style.Height = initialHeight; } + WebAssembly.Runtime.InvokeJS ("console.log('was start session 1')"); QueueMessage (Message.Call ("document.body", "appendChild", element)); + WebAssembly.Runtime.InvokeJS ("console.log('was start session end')"); } public void StopSession () @@ -82,3 +89,17 @@ namespace Ooui } } } + +namespace WebAssembly +{ + public sealed class Runtime + { + [System.Runtime.CompilerServices.MethodImplAttribute ((System.Runtime.CompilerServices.MethodImplOptions)4096)] + static extern string InvokeJS (string str, out int exceptional_result); + + public static string InvokeJS (string str) + { + return InvokeJS (str, out var _); + } + } +} From 730b254f6e38dafb2e738d5e199be8dbd8af43b1 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Mar 2018 22:06:46 -0800 Subject: [PATCH 12/52] Remove C# 7 pattern match --- Ooui/Canvas.cs | 6 ++++-- Ooui/Node.cs | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Ooui/Canvas.cs b/Ooui/Canvas.cs index 66ff2cb..edc3029 100644 --- a/Ooui/Canvas.cs +++ b/Ooui/Canvas.cs @@ -36,8 +36,10 @@ namespace Ooui { if (message.TargetId == Id) { switch (message.MessageType) { - case MessageType.Call when message.Key == "getContext" && message.Value is Array a && a.Length == 1 && "2d".Equals (a.GetValue (0)): - AddStateMessage (message); + case MessageType.Call: + if (message.Key == "getContext" && message.Value is Array a && a.Length == 1 && "2d".Equals (a.GetValue (0))) { + AddStateMessage (message); + } break; } } diff --git a/Ooui/Node.cs b/Ooui/Node.cs index efea672..8de9494 100644 --- a/Ooui/Node.cs +++ b/Ooui/Node.cs @@ -124,6 +124,7 @@ namespace Ooui protected override bool SaveStateMessageIfNeeded (Message message) { if (message.TargetId == Id) { + var handled = false; switch (message.MessageType) { case MessageType.Call when message.Key == "insertBefore": AddStateMessage (message); @@ -150,9 +151,9 @@ namespace Ooui } }); break; - default: - base.SaveStateMessageIfNeeded (message); - break; + } + if (!handled) { + base.SaveStateMessageIfNeeded (message); } return true; } From 6fe8b41ca41d8a7bc96e681831778260a13bcf39 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Mar 2018 22:14:22 -0800 Subject: [PATCH 13/52] Remove use of Linq --- Ooui/Node.cs | 9 +++++++-- Ooui/Platform.cs | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Ooui/Node.cs b/Ooui/Node.cs index 8de9494..c7456f6 100644 --- a/Ooui/Node.cs +++ b/Ooui/Node.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; namespace Ooui { @@ -28,7 +27,13 @@ namespace Ooui } public virtual string Text { - get { return String.Join ("", from c in Children select c.Text); } + get { + var sb = new System.Text.StringBuilder (); + foreach (var c in Children) { + sb.Append (c.Text); + } + return sb.ToString (); + } set { ReplaceAll (new TextNode (value ?? "")); } diff --git a/Ooui/Platform.cs b/Ooui/Platform.cs index cb6c85e..d0f3cc9 100644 --- a/Ooui/Platform.cs +++ b/Ooui/Platform.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Linq; using System.Reflection; namespace Ooui From 961fc34b4daafe70b3eb23a2ad2935fe256b95c5 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Mar 2018 22:29:46 -0800 Subject: [PATCH 14/52] Add compile options to turn off features --- Ooui/Element.cs | 4 ++++ Ooui/Node.cs | 4 ++++ Ooui/TextArea.cs | 4 ++++ Ooui/TextNode.cs | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/Ooui/Element.cs b/Ooui/Element.cs index ea203b1..cbd1430 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -232,6 +232,8 @@ namespace Ooui protected virtual bool HtmlNeedsFullEndElement => false; +#if !NO_XML + public override void WriteOuterHtml (System.Xml.XmlWriter w) { w.WriteStartElement (TagName); @@ -262,5 +264,7 @@ namespace Ooui c.WriteOuterHtml (w); } } + +#endif } } diff --git a/Ooui/Node.cs b/Ooui/Node.cs index c7456f6..4d9ad8c 100644 --- a/Ooui/Node.cs +++ b/Ooui/Node.cs @@ -188,6 +188,8 @@ namespace Ooui return false; } +#if !NO_XML + public virtual string OuterHtml { get { using (var stream = new System.IO.MemoryStream ()) { @@ -206,6 +208,8 @@ namespace Ooui } public abstract void WriteOuterHtml (System.Xml.XmlWriter w); + +#endif } class ReadOnlyList : IReadOnlyList diff --git a/Ooui/TextArea.cs b/Ooui/TextArea.cs index f3f2d5f..e6864b2 100644 --- a/Ooui/TextArea.cs +++ b/Ooui/TextArea.cs @@ -55,9 +55,13 @@ namespace Ooui return base.TriggerEventFromMessage (message); } +#if !NO_XML + public override void WriteInnerHtml (System.Xml.XmlWriter w) { w.WriteString (val ?? ""); } + +#endif } } diff --git a/Ooui/TextNode.cs b/Ooui/TextNode.cs index 8fccf8c..b94625c 100644 --- a/Ooui/TextNode.cs +++ b/Ooui/TextNode.cs @@ -21,9 +21,13 @@ namespace Ooui Text = text; } +#if !NO_XML + public override void WriteOuterHtml (System.Xml.XmlWriter w) { w.WriteString (text); } + +#endif } } From 0ada0c749779c4b9dbeb18638749a551589cdeeb Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Mar 2018 23:44:28 -0800 Subject: [PATCH 15/52] Use custom json serialization --- Ooui/Message.cs | 116 +++++++++++++++++++++++++++++++++++++++++++++ Tests/JsonTests.cs | 52 ++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 Tests/JsonTests.cs diff --git a/Ooui/Message.cs b/Ooui/Message.cs index 1e78fc9..485db30 100644 --- a/Ooui/Message.cs +++ b/Ooui/Message.cs @@ -35,6 +35,122 @@ namespace Ooui Key = eventType, Value = value, }; + + static void WriteJsonString (System.IO.TextWriter w, string s) + { + w.Write ('\"'); + for (var i = 0; i < s.Length; i++) { + var c = s[0]; + if (c == '\r') { + w.Write ("\\r"); + } + else if (c == '\n') { + w.Write ("\\n"); + } + else if (c == '\t') { + w.Write ("\\t"); + } + else if (c == '\b') { + w.Write ("\\b"); + } + else if (c == '\\') { + w.Write ("\\"); + } + else { + w.Write (c); + } + } + w.Write ('\"'); + } + + static void WriteJsonValue (System.IO.TextWriter w, object value) + { + if (value == null) { + w.Write ("null"); + return; + } + var s = value as string; + if (s != null) { + WriteJsonString (w, s); + return; + } + + var a = value as Array; + if (a != null) { + w.Write ('['); + var head = ""; + foreach (var o in a) { + w.Write (head); + WriteJsonValue (w, o); + head = ","; + } + w.Write (']'); + return; + } + + var e = value as EventTarget; + if (e != null) { + w.Write ('\"'); + w.Write (e.Id); + w.Write ('\"'); + return; + } + + if (value is Color) { + WriteJsonString (w, ((Color)value).ToString ()); + return; + } + + var icult = System.Globalization.CultureInfo.InvariantCulture; + + if (value is double) { + w.Write (((double)value).ToString (icult)); + } + + if (value is int) { + w.Write (((int)value).ToString (icult)); + } + + if (value is float) { + w.Write (((float)value).ToString (icult)); + } + + WriteJsonString (w, Convert.ToString (value, icult)); + } + + public void WriteJson (System.IO.TextWriter w) + { + w.Write ('{'); + switch (MessageType) { + case MessageType.Call: w.Write ("\"m\":\"call\",\"id\":\""); break; + case MessageType.Create: w.Write ("\"m\":\"create\",\"id\":\""); break; + case MessageType.Event: w.Write ("\"m\":\"event\",\"id\":\""); break; + case MessageType.Listen: w.Write ("\"m\":\"listen\",\"id\":\""); break; + case MessageType.Nop: w.Write ("\"m\":\"nop\",\"id\":\""); break; + case MessageType.RemoveAttribute: w.Write ("\"m\":\"remAttr\",\"id\":\""); break; + case MessageType.Set: w.Write ("\"m\":\"set\",\"id\":\""); break; + case MessageType.SetAttribute: w.Write ("\"m\":\"setAttr\",\"id\":\""); break; + } + w.Write (TargetId); + w.Write ("\",\"k\":\""); + w.Write (Key); + if (Value != null) { + w.Write ("\",\"v\":"); + WriteJsonValue (w, Value); + w.Write ('}'); + } + else { + w.Write ("\"}"); + } + } + + public string ToJson () + { + using (var sw = new System.IO.StringWriter ()) { + WriteJson (sw); + return sw.ToString (); + } + } } [JsonConverter (typeof (StringEnumConverter))] diff --git a/Tests/JsonTests.cs b/Tests/JsonTests.cs new file mode 100644 index 0000000..f27fa35 --- /dev/null +++ b/Tests/JsonTests.cs @@ -0,0 +1,52 @@ +using System; + +#if NUNIT +using NUnit.Framework; +using TestClassAttribute = NUnit.Framework.TestFixtureAttribute; +using TestMethodAttribute = NUnit.Framework.TestCaseAttribute; +#else +using Microsoft.VisualStudio.TestTools.UnitTesting; +#endif + +using Ooui; +using System.IO; +using System.Text.RegularExpressions; + +namespace Tests +{ + [TestClass] + public class JsonTests + { + static readonly Regex noid = new Regex ("⦙\\d+"); + static string NoId (string s) + { + return noid.Replace (s, "⦙"); + } + + [TestMethod] + public void ButtonIndividualMessages () + { + var b = new Button (); + b.Text = "Hello"; + b.Click += (sender, e) => { }; + Assert.AreEqual ("{\"m\":\"create\",\"id\":\"⦙\",\"k\":\"button\"}", NoId (b.StateMessages[0].ToJson ())); + Assert.AreEqual ("{\"m\":\"call\",\"id\":\"⦙\",\"k\":\"insertBefore\",\"v\":[\"⦙\",null]}", NoId (b.StateMessages[1].ToJson ())); + Assert.AreEqual ("{\"m\":\"listen\",\"id\":\"⦙\",\"k\":\"click\"}", NoId (b.StateMessages[2].ToJson ())); + } + + [TestMethod] + public void ButtonWriteMessages () + { + var b = new Button (); + b.Text = "Hello"; + b.Click += (sender, e) => { }; + var sw = new StringWriter (); + foreach (var m in b.StateMessages) { + m.WriteJson (sw); + } + Assert.AreEqual ("{\"m\":\"create\",\"id\":\"⦙\",\"k\":\"button\"}" + + "{\"m\":\"call\",\"id\":\"⦙\",\"k\":\"insertBefore\",\"v\":[\"⦙\",null]}" + + "{\"m\":\"listen\",\"id\":\"⦙\",\"k\":\"click\"}", NoId (sw.ToString ())); + } + } +} From b5dadbceb9b40c4a7242f169e2839134677fc556 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Sat, 3 Mar 2018 00:21:32 -0800 Subject: [PATCH 16/52] Use new WriteJson method in Sessions --- Ooui/Message.cs | 7 +++++-- Tests/JsonTests.cs | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Ooui/Message.cs b/Ooui/Message.cs index 485db30..7254abd 100644 --- a/Ooui/Message.cs +++ b/Ooui/Message.cs @@ -40,8 +40,11 @@ namespace Ooui { w.Write ('\"'); for (var i = 0; i < s.Length; i++) { - var c = s[0]; - if (c == '\r') { + var c = s[i]; + if (c == '\"') { + w.Write ("\\\""); + } + else if (c == '\r') { w.Write ("\\r"); } else if (c == '\n') { diff --git a/Tests/JsonTests.cs b/Tests/JsonTests.cs index f27fa35..37c637a 100644 --- a/Tests/JsonTests.cs +++ b/Tests/JsonTests.cs @@ -29,9 +29,11 @@ namespace Tests var b = new Button (); b.Text = "Hello"; b.Click += (sender, e) => { }; + b.Title = "\"Quote\""; Assert.AreEqual ("{\"m\":\"create\",\"id\":\"⦙\",\"k\":\"button\"}", NoId (b.StateMessages[0].ToJson ())); Assert.AreEqual ("{\"m\":\"call\",\"id\":\"⦙\",\"k\":\"insertBefore\",\"v\":[\"⦙\",null]}", NoId (b.StateMessages[1].ToJson ())); Assert.AreEqual ("{\"m\":\"listen\",\"id\":\"⦙\",\"k\":\"click\"}", NoId (b.StateMessages[2].ToJson ())); + Assert.AreEqual ("{\"m\":\"setAttr\",\"id\":\"⦙\",\"k\":\"title\",\"v\":\"\\\"Quote\\\"\"}", NoId (b.StateMessages[3].ToJson ())); } [TestMethod] From e78bde556bdd03df20fd02b1140f4092116b1651 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 8 Mar 2018 13:28:28 -0800 Subject: [PATCH 17/52] Implement own Json --- Ooui/JsonConvert.cs | 161 ++++++++++++++++++++++++++++++++++++++++++++ Ooui/Message.cs | 152 ++++++++++++++++++----------------------- Ooui/UI.cs | 5 +- 3 files changed, 230 insertions(+), 88 deletions(-) create mode 100644 Ooui/JsonConvert.cs diff --git a/Ooui/JsonConvert.cs b/Ooui/JsonConvert.cs new file mode 100644 index 0000000..fdeeed2 --- /dev/null +++ b/Ooui/JsonConvert.cs @@ -0,0 +1,161 @@ +using System; + +namespace Ooui +{ + class JsonConvert + { + static void WriteJsonString (System.IO.TextWriter w, string s) + { + w.Write ('\"'); + for (var i = 0; i < s.Length; i++) { + var c = s[i]; + if (c == '\"') { + w.Write ("\\\""); + } + else if (c == '\r') { + w.Write ("\\r"); + } + else if (c == '\n') { + w.Write ("\\n"); + } + else if (c == '\t') { + w.Write ("\\t"); + } + else if (c == '\b') { + w.Write ("\\b"); + } + else if (c == '\\') { + w.Write ("\\"); + } + else { + w.Write (c); + } + } + w.Write ('\"'); + } + + public static void WriteJsonValue (System.IO.TextWriter w, object value) + { + if (value == null) { + w.Write ("null"); + return; + } + var s = value as string; + if (s != null) { + WriteJsonString (w, s); + return; + } + + var a = value as Array; + if (a != null) { + w.Write ('['); + var head = ""; + foreach (var o in a) { + w.Write (head); + WriteJsonValue (w, o); + head = ","; + } + w.Write (']'); + return; + } + + var e = value as EventTarget; + if (e != null) { + w.Write ('\"'); + w.Write (e.Id); + w.Write ('\"'); + return; + } + + if (value is Color) { + WriteJsonString (w, ((Color)value).ToString ()); + return; + } + + var icult = System.Globalization.CultureInfo.InvariantCulture; + + if (value is double) { + w.Write (((double)value).ToString (icult)); + } + + if (value is int) { + w.Write (((int)value).ToString (icult)); + } + + if (value is float) { + w.Write (((float)value).ToString (icult)); + } + + WriteJsonString (w, Convert.ToString (value, icult)); + } + + public static string SerializeObject (object value) + { + using (var sw = new System.IO.StringWriter ()) { + WriteJsonValue (sw, value); + return sw.ToString (); + } + } + + static object ReadJsonArray (string j, ref int i) + { + throw new NotImplementedException (); + } + + static object ReadJsonObject (string json, ref int i) + { + var e = json.Length; + while (i < e) { + while (i < e && char.IsWhiteSpace (json[i])) + i++; + if (i >= e) + throw new Exception ("JSON Unexpected end"); + var n = e - i; + i++; + } + throw new NotImplementedException (); + } + + static object ReadJsonString (string j, ref int i) + { + throw new NotImplementedException (); + } + + static object ReadJsonNumber (string j, ref int i) + { + throw new NotImplementedException (); + } + + static object ReadJsonValue (string json, ref int i) + { + var e = json.Length; + while (i < e && char.IsWhiteSpace (json[i])) + i++; + if (i >= e) + throw new Exception ("JSON Unexpected end"); + var n = e - i; + switch (json[i]) { + case '[': + return ReadJsonArray (json, ref i); + case '{': + return ReadJsonObject (json, ref i); + case '\"': + return ReadJsonString (json, ref i); + case 'f': + i += 5; + return false; + case 't': + i += 4; + return true; + default: + return ReadJsonNumber (json, ref i); + } + } + + public static object ReadJsonValue (string json, int startIndex) + { + var i = startIndex; + return ReadJsonValue (json, ref i); + } + } +} diff --git a/Ooui/Message.cs b/Ooui/Message.cs index 7254abd..0ba8798 100644 --- a/Ooui/Message.cs +++ b/Ooui/Message.cs @@ -36,91 +36,6 @@ namespace Ooui Value = value, }; - static void WriteJsonString (System.IO.TextWriter w, string s) - { - w.Write ('\"'); - for (var i = 0; i < s.Length; i++) { - var c = s[i]; - if (c == '\"') { - w.Write ("\\\""); - } - else if (c == '\r') { - w.Write ("\\r"); - } - else if (c == '\n') { - w.Write ("\\n"); - } - else if (c == '\t') { - w.Write ("\\t"); - } - else if (c == '\b') { - w.Write ("\\b"); - } - else if (c == '\\') { - w.Write ("\\"); - } - else { - w.Write (c); - } - } - w.Write ('\"'); - } - - static void WriteJsonValue (System.IO.TextWriter w, object value) - { - if (value == null) { - w.Write ("null"); - return; - } - var s = value as string; - if (s != null) { - WriteJsonString (w, s); - return; - } - - var a = value as Array; - if (a != null) { - w.Write ('['); - var head = ""; - foreach (var o in a) { - w.Write (head); - WriteJsonValue (w, o); - head = ","; - } - w.Write (']'); - return; - } - - var e = value as EventTarget; - if (e != null) { - w.Write ('\"'); - w.Write (e.Id); - w.Write ('\"'); - return; - } - - if (value is Color) { - WriteJsonString (w, ((Color)value).ToString ()); - return; - } - - var icult = System.Globalization.CultureInfo.InvariantCulture; - - if (value is double) { - w.Write (((double)value).ToString (icult)); - } - - if (value is int) { - w.Write (((int)value).ToString (icult)); - } - - if (value is float) { - w.Write (((float)value).ToString (icult)); - } - - WriteJsonString (w, Convert.ToString (value, icult)); - } - public void WriteJson (System.IO.TextWriter w) { w.Write ('{'); @@ -139,7 +54,7 @@ namespace Ooui w.Write (Key); if (Value != null) { w.Write ("\",\"v\":"); - WriteJsonValue (w, Value); + JsonConvert.WriteJsonValue (w, Value); w.Write ('}'); } else { @@ -154,6 +69,71 @@ namespace Ooui return sw.ToString (); } } + + public static Message FromJson (string json) + { + var m = new Message (); + + var i = 0; + var e = json.Length; + + while (i < e) { + while (i < e && (json[i]==',' || json[i]=='{' || char.IsWhiteSpace (json[i]))) + i++; + if (i >= e) + throw new Exception ("JSON Unexpected end"); + var n = e - i; + if (json[i] == '}') + break; + if (n > 4 && json[i] == '\"' && json[i+2] == '\"' && json[i+3] == ':') { + switch (json[i + 1]) { + case 'm': + if (json[i + 4] == '\"' && json[i + 5] == 'e') { + m.MessageType = MessageType.Event; + } + i += 5; + while (i < e && json[i] != '\"') i++; + i++; + break; + case 'k': { + i += 5; + var se = i; + while (se < e && json[se] != '\"') + se++; + m.Key = json.Substring (i, se - i); + i = se + 1; + } + break; + case 'v': + m.Value = JsonConvert.ReadJsonValue (json, i + 4); + break; + } + } + else if (n > 5 && json[i] == '\"' && json[i + 3] == '\"' && json[i + 4] == ':' && json[i+5] == '\"') { + switch (json[i + 1]) { + case 'i': { + i += 6; + var se = i; + while (se < e && json[se] != '\"') + se++; + m.TargetId = json.Substring (i, se - i); + i = se + 1; + } + break; + } + } + else { + throw new Exception ("JSON Expected property"); + } + } + + return m; + } + + public override string ToString () + { + return ToJson (); + } } [JsonConverter (typeof (StringEnumConverter))] diff --git a/Ooui/UI.cs b/Ooui/UI.cs index aff0c38..893ef63 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -464,8 +464,9 @@ namespace Ooui public static byte[] GetData (object obj) { - var r = Newtonsoft.Json.JsonConvert.SerializeObject (obj); - return System.Text.Encoding.UTF8.GetBytes (r); + var r = Ooui.JsonConvert.SerializeObject (obj); + var e = new UTF8Encoding (false); + return e.GetBytes (r); } public override void Respond (HttpListenerContext listenerContext, CancellationToken token) From 881330e49ec8a029663a5b03bb1702355464cc58 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 8 Mar 2018 13:30:06 -0800 Subject: [PATCH 18/52] Add wasm build --- Ooui.Wasm/Dockerfile | 66 ++++++++++++++++++++++++++++++++++ Ooui.Wasm/Makefile | 18 ++++++++++ Ooui.Wasm/System.Core.cs | 16 +++++++++ Ooui.Wasm/hello.cs | 77 ++++++++++++++++++++++++++++++++++++++++ Ooui.Wasm/index.html | 44 +++++++++++++++++++++++ 5 files changed, 221 insertions(+) create mode 100644 Ooui.Wasm/Dockerfile create mode 100644 Ooui.Wasm/Makefile create mode 100644 Ooui.Wasm/System.Core.cs create mode 100644 Ooui.Wasm/hello.cs create mode 100644 Ooui.Wasm/index.html diff --git a/Ooui.Wasm/Dockerfile b/Ooui.Wasm/Dockerfile new file mode 100644 index 0000000..bf88989 --- /dev/null +++ b/Ooui.Wasm/Dockerfile @@ -0,0 +1,66 @@ +FROM alpine:3.7 + +RUN apk update \ + && apk upgrade \ + && apk add --no-cache g++ musl-dev make cmake git subversion python + +RUN mkdir /mono-wasm \ + && cd /mono-wasm \ + && git clone --depth 1 https://github.com/lrz/mono-wasm.git build + +# RUN cd /mono-wasm \ +# && svn co --quiet http://llvm.org/svn/llvm-project/llvm/trunk llvm \ +# && cd llvm/tools \ +# && svn co --quiet http://llvm.org/svn/llvm-project/cfe/trunk clang \ +# && svn co --quiet http://llvm.org/svn/llvm-project/lld/trunk lld + +RUN cd /mono-wasm \ + && wget http://releases.llvm.org/5.0.1/llvm-5.0.1.src.tar.xz \ + && unxz llvm-5.0.1.src.tar.xz \ + && tar xf llvm-5.0.1.src.tar \ + && rm llvm-5.0.1.src.tar \ + && mv llvm-5.0.1.src llvm \ + && cd llvm/tools \ + && wget http://releases.llvm.org/5.0.1/cfe-5.0.1.src.tar.xz \ + && unxz cfe-5.0.1.src.tar.xz \ + && tar xf cfe-5.0.1.src.tar \ + && rm cfe-5.0.1.src.tar \ + && mv cfe-5.0.1.src clang \ + && wget http://releases.llvm.org/5.0.1/lld-5.0.1.src.tar.xz \ + && unxz lld-5.0.1.src.tar.xz \ + && tar xf lld-5.0.1.src.tar \ + && rm lld-5.0.1.src.tar \ + && mv lld-5.0.1.src lld + +RUN mkdir /mono-wasm/llvm-build \ + && cd /mono-wasm/llvm-build \ + && cmake -G "Unix Makefiles" -DCMAKE_C_COMPILER=/usr/bin/gcc -DCMAKE_CXX_COMPILER=/usr/bin/g++ -DLLVM_TARGETS_TO_BUILD=X86 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly -DCMAKE_BUILD_TYPE=Release ../llvm \ + && make -j7 + +RUN cd /mono-wasm \ + && git clone --depth 1 https://github.com/mono/llvm.git llvm-mono + +ENV CXXFLAGS="-fpermissive" + +RUN mkdir /mono-wasm/llvm-mono-build \ + && cd /mono-wasm/llvm-mono-build \ + && cmake -G "Unix Makefiles" -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" -DLLVM_TARGETS_TO_BUILD=X86 ../llvm-mono \ + && cp -R ../llvm-mono/include/* include/ \ + && sed -i "s/#if defined(_LARGEFILE64_SOURCE) || defined(_GNU_SOURCE)/#if 0/" /usr/include/stdio.h \ + && sed -i "s/#if defined(_LARGEFILE64_SOURCE) || defined(_GNU_SOURCE)/#if 0/" /usr/include/sys/stat.h \ + # && sed -i "s/namespace llvm/#undef fopen\n#undef fopen64\n#undef fseeko\n#undef fseeko64\n#undef fstat\n#undef fstat64\n#undef ftello\n#undef ftello64\n#undef lstat\n#undef lstat64\n#undef stat\n#undef stat64\n#undef tmpfile\n#undef tmpfile64\n\nnamespace llvm/" /mono-wasm/llvm-mono-build/include/llvm/Target/TargetLibraryInfo.h + && make -j7 + +ENV CXXFLAGS="" + +RUN cd /mono-wasm \ + && git clone --depth 1 https://github.com/lrz/mono-wasm-mono.git mono-compiler + +RUN apk add --no-cache bash autoconf automake libtool linux-headers \ + && cd /mono-wasm/mono-compiler \ + && ./autogen.sh --with-cross-offsets=offsets-wasm32.h CFLAGS="-DCOMPILE_WASM32 -DMONO_CROSS_COMPILE" CXXFLAGS="-DCOMPILE_WASM32 -DMONO_CROSS_COMPILE" --disable-boehm --with-sigaltstack=no --enable-llvm --enable-llvm-runtime --with-llvm=../llvm-mono-build --disable-btls --with-runtime_preset=testing_aot_full + +# RUN cd /mono-wasm/mono-compiler/eglib \ +# && make -j7 \ +# && cd ../mono \ +# && make -j7 diff --git a/Ooui.Wasm/Makefile b/Ooui.Wasm/Makefile new file mode 100644 index 0000000..cd11c33 --- /dev/null +++ b/Ooui.Wasm/Makefile @@ -0,0 +1,18 @@ +OOUI_SRCS=$(wildcard ../Ooui/*.cs) + +all: hello.exe output/index.wasm output/index.html + +hello.exe: hello.cs Ooui.dll Makefile + mcs -nostdlib -noconfig -r:mono-wasm-macos/dist/lib/mscorlib.dll -r:Ooui.dll hello.cs -out:hello.exe + +Ooui.dll: $(OOUI_SRCS) System.Core.cs Makefile + mcs -nostdlib -noconfig -r:mono-wasm-macos/dist/lib/mscorlib.dll -define:NO_PROCESS,NO_SERVER,NO_XML System.Core.cs $(OOUI_SRCS) -out:Ooui.dll + +output/index.wasm: hello.exe Makefile + mono-wasm-macos/dist/bin/mono-wasm -i hello.exe -o output + +output/index.html: index.html + cp index.html output + +clean: + rm -rf build output hello.exe diff --git a/Ooui.Wasm/System.Core.cs b/Ooui.Wasm/System.Core.cs new file mode 100644 index 0000000..f8d5d11 --- /dev/null +++ b/Ooui.Wasm/System.Core.cs @@ -0,0 +1,16 @@ +using System; + +namespace System.ComponentModel +{ + public interface INotifyPropertyChanged + { + + } + + public class PropertyChangedEventArgs : EventArgs + { + + } + + public delegate void PropertyChangedEventHandler (object sender, PropertyChangedEventArgs e); +} \ No newline at end of file diff --git a/Ooui.Wasm/hello.cs b/Ooui.Wasm/hello.cs new file mode 100644 index 0000000..b802c97 --- /dev/null +++ b/Ooui.Wasm/hello.cs @@ -0,0 +1,77 @@ +using Mono.WebAssembly; +using System; + +class Hello +{ + static int Factorial(int n) + { + if (n == 0) { + return 1; + } + return n * Factorial(n - 1); + } + + // This function is called from the browser by JavaScript. + // Here we calculate the factorial of the given number then use the + // Mono.WebAssembly API to retrieve the element from the DOM and set its + // innerText property to the factorial result. + static void FactorialInElement(int n, string element_id) + { + Console.WriteLine( + "Calculating factorial of {0} into DOM element {1}", + n, element_id); + + int f = Factorial(n); + + var elem = HtmlPage.Document.GetElementById(element_id); + elem.InnerText = f.ToString(); + } + + static void PrintHtmlElements(HtmlElement elem, int level) + { + string str = ""; + for (int i = 0; i < level; i++) { + str += " "; + } + + str += $"<{elem.TagName}"; + + foreach (var name in elem.AttributeNames) { + var value = elem.GetAttribute(name); + str += $" {name}='{value}'"; + } + + str += ">"; + + Console.WriteLine(str); + + var list = elem.Children; + for (int i = 0; i < list.Count; i++) { + var child = list[i]; + PrintHtmlElements(child, level + 1); + } + } + + static int Main(string[] args) + { + Ooui.Div odiv = new Ooui.Div (); + int f = Factorial(6); + HtmlPage.Window.Alert($"Hello world! factorial(6) -> {odiv}"); + + var bi = HtmlPage.BrowserInformation; + Console.WriteLine($"BrowserInformation: Name {bi.Name} BrowserVersion {bi.BrowserVersion} UserAgent {bi.UserAgent} Platform {bi.Platform} CookiesEnabled {bi.CookiesEnabled} ProductName {bi.ProductName}"); + + var d = HtmlPage.Document; + Console.WriteLine($"Document Location: {d.Location}"); + + PrintHtmlElements(d.DocumentElement, 0); + + var p = d.CreateElement("p"); + p.InnerText = "This text was added at runtime."; + d.Body.AppendChild(p); + + if (args.Length > 0) FactorialInElement(0, ""); // this is a hack so that the linker does not remove the FactorialInElement() method + + return f; + } +} diff --git a/Ooui.Wasm/index.html b/Ooui.Wasm/index.html new file mode 100644 index 0000000..121112d --- /dev/null +++ b/Ooui.Wasm/index.html @@ -0,0 +1,44 @@ + + + + + WebAssembly Example + + + + +

+

+ Number: +

+ Result: ... +

+ +
+

+ + + + + From 9482b65b86ab27c027238375bc53fa48dba969e4 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 18:54:50 -0800 Subject: [PATCH 19/52] Remove AOT wasm --- Ooui.Wasm/Dockerfile | 66 ---------------------------------- Ooui.Wasm/Makefile | 18 ---------- Ooui.Wasm/System.Core.cs | 16 --------- Ooui.Wasm/hello.cs | 77 ---------------------------------------- Ooui.Wasm/index.html | 44 ----------------------- 5 files changed, 221 deletions(-) delete mode 100644 Ooui.Wasm/Dockerfile delete mode 100644 Ooui.Wasm/Makefile delete mode 100644 Ooui.Wasm/System.Core.cs delete mode 100644 Ooui.Wasm/hello.cs delete mode 100644 Ooui.Wasm/index.html diff --git a/Ooui.Wasm/Dockerfile b/Ooui.Wasm/Dockerfile deleted file mode 100644 index bf88989..0000000 --- a/Ooui.Wasm/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -FROM alpine:3.7 - -RUN apk update \ - && apk upgrade \ - && apk add --no-cache g++ musl-dev make cmake git subversion python - -RUN mkdir /mono-wasm \ - && cd /mono-wasm \ - && git clone --depth 1 https://github.com/lrz/mono-wasm.git build - -# RUN cd /mono-wasm \ -# && svn co --quiet http://llvm.org/svn/llvm-project/llvm/trunk llvm \ -# && cd llvm/tools \ -# && svn co --quiet http://llvm.org/svn/llvm-project/cfe/trunk clang \ -# && svn co --quiet http://llvm.org/svn/llvm-project/lld/trunk lld - -RUN cd /mono-wasm \ - && wget http://releases.llvm.org/5.0.1/llvm-5.0.1.src.tar.xz \ - && unxz llvm-5.0.1.src.tar.xz \ - && tar xf llvm-5.0.1.src.tar \ - && rm llvm-5.0.1.src.tar \ - && mv llvm-5.0.1.src llvm \ - && cd llvm/tools \ - && wget http://releases.llvm.org/5.0.1/cfe-5.0.1.src.tar.xz \ - && unxz cfe-5.0.1.src.tar.xz \ - && tar xf cfe-5.0.1.src.tar \ - && rm cfe-5.0.1.src.tar \ - && mv cfe-5.0.1.src clang \ - && wget http://releases.llvm.org/5.0.1/lld-5.0.1.src.tar.xz \ - && unxz lld-5.0.1.src.tar.xz \ - && tar xf lld-5.0.1.src.tar \ - && rm lld-5.0.1.src.tar \ - && mv lld-5.0.1.src lld - -RUN mkdir /mono-wasm/llvm-build \ - && cd /mono-wasm/llvm-build \ - && cmake -G "Unix Makefiles" -DCMAKE_C_COMPILER=/usr/bin/gcc -DCMAKE_CXX_COMPILER=/usr/bin/g++ -DLLVM_TARGETS_TO_BUILD=X86 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly -DCMAKE_BUILD_TYPE=Release ../llvm \ - && make -j7 - -RUN cd /mono-wasm \ - && git clone --depth 1 https://github.com/mono/llvm.git llvm-mono - -ENV CXXFLAGS="-fpermissive" - -RUN mkdir /mono-wasm/llvm-mono-build \ - && cd /mono-wasm/llvm-mono-build \ - && cmake -G "Unix Makefiles" -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" -DLLVM_TARGETS_TO_BUILD=X86 ../llvm-mono \ - && cp -R ../llvm-mono/include/* include/ \ - && sed -i "s/#if defined(_LARGEFILE64_SOURCE) || defined(_GNU_SOURCE)/#if 0/" /usr/include/stdio.h \ - && sed -i "s/#if defined(_LARGEFILE64_SOURCE) || defined(_GNU_SOURCE)/#if 0/" /usr/include/sys/stat.h \ - # && sed -i "s/namespace llvm/#undef fopen\n#undef fopen64\n#undef fseeko\n#undef fseeko64\n#undef fstat\n#undef fstat64\n#undef ftello\n#undef ftello64\n#undef lstat\n#undef lstat64\n#undef stat\n#undef stat64\n#undef tmpfile\n#undef tmpfile64\n\nnamespace llvm/" /mono-wasm/llvm-mono-build/include/llvm/Target/TargetLibraryInfo.h - && make -j7 - -ENV CXXFLAGS="" - -RUN cd /mono-wasm \ - && git clone --depth 1 https://github.com/lrz/mono-wasm-mono.git mono-compiler - -RUN apk add --no-cache bash autoconf automake libtool linux-headers \ - && cd /mono-wasm/mono-compiler \ - && ./autogen.sh --with-cross-offsets=offsets-wasm32.h CFLAGS="-DCOMPILE_WASM32 -DMONO_CROSS_COMPILE" CXXFLAGS="-DCOMPILE_WASM32 -DMONO_CROSS_COMPILE" --disable-boehm --with-sigaltstack=no --enable-llvm --enable-llvm-runtime --with-llvm=../llvm-mono-build --disable-btls --with-runtime_preset=testing_aot_full - -# RUN cd /mono-wasm/mono-compiler/eglib \ -# && make -j7 \ -# && cd ../mono \ -# && make -j7 diff --git a/Ooui.Wasm/Makefile b/Ooui.Wasm/Makefile deleted file mode 100644 index cd11c33..0000000 --- a/Ooui.Wasm/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -OOUI_SRCS=$(wildcard ../Ooui/*.cs) - -all: hello.exe output/index.wasm output/index.html - -hello.exe: hello.cs Ooui.dll Makefile - mcs -nostdlib -noconfig -r:mono-wasm-macos/dist/lib/mscorlib.dll -r:Ooui.dll hello.cs -out:hello.exe - -Ooui.dll: $(OOUI_SRCS) System.Core.cs Makefile - mcs -nostdlib -noconfig -r:mono-wasm-macos/dist/lib/mscorlib.dll -define:NO_PROCESS,NO_SERVER,NO_XML System.Core.cs $(OOUI_SRCS) -out:Ooui.dll - -output/index.wasm: hello.exe Makefile - mono-wasm-macos/dist/bin/mono-wasm -i hello.exe -o output - -output/index.html: index.html - cp index.html output - -clean: - rm -rf build output hello.exe diff --git a/Ooui.Wasm/System.Core.cs b/Ooui.Wasm/System.Core.cs deleted file mode 100644 index f8d5d11..0000000 --- a/Ooui.Wasm/System.Core.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace System.ComponentModel -{ - public interface INotifyPropertyChanged - { - - } - - public class PropertyChangedEventArgs : EventArgs - { - - } - - public delegate void PropertyChangedEventHandler (object sender, PropertyChangedEventArgs e); -} \ No newline at end of file diff --git a/Ooui.Wasm/hello.cs b/Ooui.Wasm/hello.cs deleted file mode 100644 index b802c97..0000000 --- a/Ooui.Wasm/hello.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Mono.WebAssembly; -using System; - -class Hello -{ - static int Factorial(int n) - { - if (n == 0) { - return 1; - } - return n * Factorial(n - 1); - } - - // This function is called from the browser by JavaScript. - // Here we calculate the factorial of the given number then use the - // Mono.WebAssembly API to retrieve the element from the DOM and set its - // innerText property to the factorial result. - static void FactorialInElement(int n, string element_id) - { - Console.WriteLine( - "Calculating factorial of {0} into DOM element {1}", - n, element_id); - - int f = Factorial(n); - - var elem = HtmlPage.Document.GetElementById(element_id); - elem.InnerText = f.ToString(); - } - - static void PrintHtmlElements(HtmlElement elem, int level) - { - string str = ""; - for (int i = 0; i < level; i++) { - str += " "; - } - - str += $"<{elem.TagName}"; - - foreach (var name in elem.AttributeNames) { - var value = elem.GetAttribute(name); - str += $" {name}='{value}'"; - } - - str += ">"; - - Console.WriteLine(str); - - var list = elem.Children; - for (int i = 0; i < list.Count; i++) { - var child = list[i]; - PrintHtmlElements(child, level + 1); - } - } - - static int Main(string[] args) - { - Ooui.Div odiv = new Ooui.Div (); - int f = Factorial(6); - HtmlPage.Window.Alert($"Hello world! factorial(6) -> {odiv}"); - - var bi = HtmlPage.BrowserInformation; - Console.WriteLine($"BrowserInformation: Name {bi.Name} BrowserVersion {bi.BrowserVersion} UserAgent {bi.UserAgent} Platform {bi.Platform} CookiesEnabled {bi.CookiesEnabled} ProductName {bi.ProductName}"); - - var d = HtmlPage.Document; - Console.WriteLine($"Document Location: {d.Location}"); - - PrintHtmlElements(d.DocumentElement, 0); - - var p = d.CreateElement("p"); - p.InnerText = "This text was added at runtime."; - d.Body.AppendChild(p); - - if (args.Length > 0) FactorialInElement(0, ""); // this is a hack so that the linker does not remove the FactorialInElement() method - - return f; - } -} diff --git a/Ooui.Wasm/index.html b/Ooui.Wasm/index.html deleted file mode 100644 index 121112d..0000000 --- a/Ooui.Wasm/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - WebAssembly Example - - - - -

-

- Number: -

- Result: ... -

- -
-

- - - - - From 93229cf3c8907cfab8f5bfd2c13c9b9f502601ec Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 18:57:25 -0800 Subject: [PATCH 20/52] Fix merge mistake --- Ooui/Platform.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Ooui/Platform.cs b/Ooui/Platform.cs index d0f3cc9..9884253 100644 --- a/Ooui/Platform.cs +++ b/Ooui/Platform.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Reflection; +using System.Linq; namespace Ooui { From 5d47c9adef55b8067f772fbab62ca9a017038e16 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 19:11:57 -0800 Subject: [PATCH 21/52] Wasm is working --- Ooui.Wasm/ooui-sample.cs | 4 ++++ Ooui/Client.js | 18 +++++++++++++----- Ooui/WebAssemblySession.cs | 23 ++++++++++++----------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs index d53d6ad..bcfb254 100644 --- a/Ooui.Wasm/ooui-sample.cs +++ b/Ooui.Wasm/ooui-sample.cs @@ -9,6 +9,10 @@ public class Program var l = new Label { Text = "Hello" }; var b = new Button ("Click Me"); var e = new Div (new Div (l), b); + var c = 0; + b.Click += (s, ee) => { + b.Text = $"Clicked {c} times"; + }; UI.SetGlobalElement ("main", e); return e.ToString (); diff --git a/Ooui/Client.js b/Ooui/Client.js index e89fbe2..028ca5e 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -322,6 +322,16 @@ function fixupValue (v) { // == WASM Support == +window["__oouiReceiveMessages"] = function (sessionId, messages) +{ + console.log ("RCV", messages); + messages.forEach (function (m) { + console.log ('Raw value from server', m.v); + m.v = fixupValue (m.v); + processMessage (m); + }); +}; + var Module = { onRuntimeInitialized: function () { console.log ("Done with WASM module instantiation."); @@ -405,22 +415,20 @@ var MonoRuntime = { var WebAssemblyApp = { init: function () { this.loading = document.getElementById ("loading"); - this.output = document.getElementById ("output"); this.findMethods (); var res = this.runApp ("1", "2"); - this.output.value = res; - this.output.hidden = false; this.loading.hidden = true; }, runApp: function (a, b) { try { + var rres = MonoRuntime.call_method (this.add_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); + var res = MonoRuntime.conv_string (rres); MonoRuntime.call_method (this.ooui_method, null, [MonoRuntime.mono_string ("main"), MonoRuntime.mono_string ("main")]); - var res = MonoRuntime.call_method (this.add_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); - return MonoRuntime.conv_string (res); + return res; } catch (e) { return e.msg; } diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs index 47199ac..7ea2f20 100644 --- a/Ooui/WebAssemblySession.cs +++ b/Ooui/WebAssemblySession.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; namespace Ooui { @@ -17,16 +18,12 @@ namespace Ooui protected override void QueueMessage (Message message) { - WebAssembly.Runtime.InvokeJS ("console.log('q 0')"); base.QueueMessage (message); - WebAssembly.Runtime.InvokeJS ("console.log('q 1')"); TransmitQueuedMessages (); - WebAssembly.Runtime.InvokeJS ("console.log('q end')"); } void TransmitQueuedMessages () { - WebAssembly.Runtime.InvokeJS ("console.log('t 0')"); // // Dequeue as many messages as we can // @@ -36,19 +33,23 @@ namespace Ooui queuedMessages.Clear (); } - WebAssembly.Runtime.InvokeJS ("console.log('t 1')"); - if (messagesToSend.Count == 0) return; - WebAssembly.Runtime.InvokeJS ("console.log('t 2')"); - // // Now actually send the messages // - //var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend); - WebAssembly.Runtime.InvokeJS ("alert(" + messagesToSend.Count + ")"); - WebAssembly.Runtime.InvokeJS ("console.log('t end')"); + var sb = new StringBuilder (); + var head = ""; + sb.Append ("["); + foreach (var m in messagesToSend) { + sb.Append (head); + sb.Append (m.ToJson ()); + head = ","; + } + sb.Append ("]"); + var json = sb.ToString (); + WebAssembly.Runtime.InvokeJS ("__oouiReceiveMessages(\"" + id + "\", " + json + ")"); } public void ReceiveMessageJson (string json) From 03781339f4dcd3a3c5ee5e2773e13e1da07a520b Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 19:39:01 -0800 Subject: [PATCH 22/52] Send events back to Ooui from JS --- Ooui.Wasm/ooui-sample.cs | 5 +-- Ooui.Wasm/ooui-sample.html | 1 + Ooui/Client.js | 65 +++++++++++++++++++++++--------------- Ooui/WebAssemblySession.cs | 4 --- 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs index bcfb254..ac57e88 100644 --- a/Ooui.Wasm/ooui-sample.cs +++ b/Ooui.Wasm/ooui-sample.cs @@ -9,9 +9,10 @@ public class Program var l = new Label { Text = "Hello" }; var b = new Button ("Click Me"); var e = new Div (new Div (l), b); - var c = 0; + int count = 0; b.Click += (s, ee) => { - b.Text = $"Clicked {c} times"; + count++; + b.Text = $"Clicked {count} times"; }; UI.SetGlobalElement ("main", e); diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html index 2c9dc7d..cfb1dfb 100644 --- a/Ooui.Wasm/ooui-sample.html +++ b/Ooui.Wasm/ooui-sample.html @@ -39,6 +39,7 @@ "System.Reflection.Extensions.dll", "System.Runtime.dll", "System.Runtime.Extensions.dll", + "System.Runtime.Serialization.dll", "System.Runtime.Serialization.Primitives.dll", "System.Xml.dll", "System.Xml.ReaderWriter.dll", diff --git a/Ooui/Client.js b/Ooui/Client.js index 028ca5e..9126093 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -6,6 +6,17 @@ const nodes = {}; const hasText = {}; let socket = null; +let wasmSession = null; + +function send (json) { + if (debug) console.log ("Send", json); + if (socket != null) { + socket.send (json); + } + else if (wasmSession != null) { + WebAssemblyApp.receiveMessagesJson (wasmSession, json); + } +} const mouseEvents = { click: true, @@ -117,8 +128,7 @@ function ooui (rootElementPath) { }; saveSize (em.v); const ems = JSON.stringify (em); - if (socket != null) - socket.send (ems); + send (ems); if (debug) console.log ("Event", em); } }()); @@ -130,10 +140,6 @@ function oouiWasm (mainAsmName, mainNamspace, mainClassName, mainMethodNmae, ass var initialSize = getSize (); - function send (json) { - if (debug) console.log ("Send", json); - } - function resizeHandler() { const em = { m: "event", @@ -269,8 +275,7 @@ function msgListen (m) { }; } const ems = JSON.stringify (em); - if (socket != null) - socket.send (ems); + send (ems); if (debug) console.log ("Event", em); if (em.k === "submit") e.preventDefault (); @@ -324,9 +329,9 @@ function fixupValue (v) { window["__oouiReceiveMessages"] = function (sessionId, messages) { - console.log ("RCV", messages); + if (debug) console.log ("WebAssembly Receive", messages); messages.forEach (function (m) { - console.log ('Raw value from server', m.v); + // console.log ('Raw value from server', m.v); m.v = fixupValue (m.v); processMessage (m); }); @@ -334,13 +339,13 @@ window["__oouiReceiveMessages"] = function (sessionId, messages) var Module = { onRuntimeInitialized: function () { - console.log ("Done with WASM module instantiation."); + if (debug) console.log ("Done with WASM module instantiation."); Module.FS_createPath ("/", "managed", true, true); var pending = 0; this.assemblies.forEach (function(asm_name) { - console.log ("Loading", asm_name); + if (debug) console.log ("Loading", asm_name); ++pending; fetch ("managed/" + asm_name, { credentials: 'same-origin' }).then (function (response) { if (!response.ok) @@ -357,7 +362,7 @@ var Module = { }, bclLoadingDone: function () { - console.log ("Done loading the BCL."); + if (debug) console.log ("Done loading the BCL."); MonoRuntime.init (); } }; @@ -374,7 +379,7 @@ var MonoRuntime = { this.load_runtime ("managed", 1); - console.log ("Done initializing the runtime."); + if (debug) console.log ("Done initializing the runtime."); WebAssemblyApp.init (); }, @@ -425,38 +430,48 @@ var WebAssemblyApp = { runApp: function (a, b) { try { - var rres = MonoRuntime.call_method (this.add_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); + var sessionId = "main"; + var rres = MonoRuntime.call_method (this.main_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); var res = MonoRuntime.conv_string (rres); - MonoRuntime.call_method (this.ooui_method, null, [MonoRuntime.mono_string ("main"), MonoRuntime.mono_string ("main")]); + MonoRuntime.call_method (this.ooui_StartWebAssemblySession_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string ("main")]); + wasmSession = sessionId; return res; } catch (e) { return e.msg; } }, + receiveMessagesJson: function (sessionId, json) { + MonoRuntime.call_method (this.ooui_ReceiveWebAssemblySessionMessageJson_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string (json)]); + }, + findMethods: function () { - this.ooui_module = MonoRuntime.assembly_load ("Ooui") + this.ooui_module = MonoRuntime.assembly_load ("Ooui"); if (!this.ooui_module) throw "Could not find Ooui.dll"; - this.ooui_class = MonoRuntime.find_class (this.ooui_module, "Ooui", "UI") + this.ooui_class = MonoRuntime.find_class (this.ooui_module, "Ooui", "UI"); if (!this.ooui_class) throw "Could not find UI class in Ooui module"; - this.ooui_method = MonoRuntime.find_method (this.ooui_class, "StartWebAssemblySession", -1) - if (!this.ooui_method) + this.ooui_StartWebAssemblySession_method = MonoRuntime.find_method (this.ooui_class, "StartWebAssemblySession", -1); + if (!this.ooui_StartWebAssemblySession_method) throw "Could not find StartWebAssemblySession method"; - this.main_module = MonoRuntime.assembly_load (mainAsmName) + this.ooui_ReceiveWebAssemblySessionMessageJson_method = MonoRuntime.find_method (this.ooui_class, "ReceiveWebAssemblySessionMessageJson", -1); + if (!this.ooui_ReceiveWebAssemblySessionMessageJson_method) + throw "Could not find ReceiveWebAssemblySessionMessageJson method"; + + this.main_module = MonoRuntime.assembly_load (mainAsmName); if (!this.main_module) throw "Could not find Main Module " + mainAsmName + ".dll"; - this.math_class = MonoRuntime.find_class (this.main_module, "", "Program") - if (!this.math_class) + this.main_class = MonoRuntime.find_class (this.main_module, "", "Program") + if (!this.main_class) throw "Could not find Program class in main module"; - this.add_method = MonoRuntime.find_method (this.math_class, "Main", -1) - if (!this.add_method) + this.main_method = MonoRuntime.find_method (this.main_class, "Main", -1) + if (!this.main_method) throw "Could not find Main method"; }, }; diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs index 7ea2f20..df1b3e9 100644 --- a/Ooui/WebAssemblySession.cs +++ b/Ooui/WebAssemblySession.cs @@ -55,7 +55,6 @@ namespace Ooui public void ReceiveMessageJson (string json) { try { - Info ("RECEIVED: " + json); var message = Newtonsoft.Json.JsonConvert.DeserializeObject (json); element.Receive (message); } @@ -66,7 +65,6 @@ namespace Ooui public void StartSession () { - WebAssembly.Runtime.InvokeJS ("console.log('was start session 0')"); // // Start watching for changes in the element // @@ -79,9 +77,7 @@ namespace Ooui element.Style.Width = initialWidth; element.Style.Height = initialHeight; } - WebAssembly.Runtime.InvokeJS ("console.log('was start session 1')"); QueueMessage (Message.Call ("document.body", "appendChild", element)); - WebAssembly.Runtime.InvokeJS ("console.log('was start session end')"); } public void StopSession () From 0f085717adedc8b5234ec4c8b477a603b9ffe2b4 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 21:28:39 -0800 Subject: [PATCH 23/52] Add PCL target to Xamarin.Forms --- Ooui.Forms/DisplayAlert.cs | 4 +- Ooui.Forms/Forms.cs | 31 +++++++++++- Ooui.Forms/Ooui.Forms.csproj | 12 ++--- Ooui.Forms/PageExtensions.cs | 2 + Ooui.Forms/Platform.cs | 5 +- Ooui.Forms/Renderers/ImageRenderer.cs | 11 ++++- Ooui.Forms/ResourcesProvider.cs | 9 ++++ Ooui.Wasm/ooui-sample.html | 3 ++ Ooui/UI.cs | 34 +++++-------- Ooui/Utilities.cs | 69 +++++++++++++++++++++++++++ 10 files changed, 143 insertions(+), 37 deletions(-) create mode 100644 Ooui/Utilities.cs diff --git a/Ooui.Forms/DisplayAlert.cs b/Ooui.Forms/DisplayAlert.cs index fae811a..0770cfa 100644 --- a/Ooui.Forms/DisplayAlert.cs +++ b/Ooui.Forms/DisplayAlert.cs @@ -1,4 +1,4 @@ -using System.Web; +using System; using Xamarin.Forms.Internals; namespace Ooui.Forms @@ -31,7 +31,7 @@ namespace Ooui.Forms ClassName = "close" }; - _closeButton.AppendChild(new Span(HttpUtility.HtmlDecode("×"))); + _closeButton.AppendChild(new Span("×")); var h4 = new Heading(4) { diff --git a/Ooui.Forms/Forms.cs b/Ooui.Forms/Forms.cs index c648cfa..e8e22e9 100644 --- a/Ooui.Forms/Forms.cs +++ b/Ooui.Forms/Forms.cs @@ -21,7 +21,7 @@ namespace Xamarin.Forms return; IsInitialized = true; - Log.Listeners.Add (new DelegateLogListener ((c, m) => Trace.WriteLine (m, c))); + Log.Listeners.Add (new DelegateLogListener ((c, m) => System.Diagnostics.Debug.WriteLine (m, c))); Device.SetIdiom (TargetIdiom.Desktop); Device.PlatformServices = new OouiPlatformServices (); @@ -64,7 +64,11 @@ namespace Xamarin.Forms public Assembly[] GetAssemblies () { +#if PCL + return new[] { typeof (Xamarin.Forms.View).GetTypeInfo ().Assembly, typeof (Forms.OouiPlatformServices).GetTypeInfo ().Assembly }; +#else return AppDomain.CurrentDomain.GetAssemblies (); +#endif } public string GetMD5Hash (string input) @@ -140,6 +144,31 @@ namespace Xamarin.Forms } } +#if PCL + + public delegate void TimerCallback(object state); + + public sealed class Timer : CancellationTokenSource, IDisposable + { + public Timer (TimerCallback callback, object state, int dueTime, int period) + { + Task.Run (async () => { + await Task.Delay (dueTime).ConfigureAwait (false); + if (!IsCancellationRequested) + callback (state); + while (!IsCancellationRequested) { + await Task.Delay (period).ConfigureAwait (false); + if (!IsCancellationRequested) + callback (state); + } + }); + } + + public new void Dispose() { base.Cancel(); } + } + +#endif + public void QuitApplication() { } diff --git a/Ooui.Forms/Ooui.Forms.csproj b/Ooui.Forms/Ooui.Forms.csproj index 440e8f2..b439ba6 100644 --- a/Ooui.Forms/Ooui.Forms.csproj +++ b/Ooui.Forms/Ooui.Forms.csproj @@ -9,16 +9,14 @@ https://github.com/praeclarum/Ooui https://github.com/praeclarum/Ooui/blob/master/LICENSE https://github.com/praeclarum/Ooui.git - netstandard2.0 + netstandard2.0;netstandard1.0 + true - - true - - - - true + + PCL + diff --git a/Ooui.Forms/PageExtensions.cs b/Ooui.Forms/PageExtensions.cs index 4ffe01d..b2174cb 100644 --- a/Ooui.Forms/PageExtensions.cs +++ b/Ooui.Forms/PageExtensions.cs @@ -5,6 +5,7 @@ namespace Xamarin.Forms { public static class PageExtensions { +#if !PCL public static void Publish (this Xamarin.Forms.Page page, string path) { Ooui.UI.Publish (path, () => page.CreateElement ()); @@ -15,6 +16,7 @@ namespace Xamarin.Forms var lazyPage = new Lazy ((() => page.CreateElement ()), true); Ooui.UI.Publish (path, () => lazyPage.Value); } +#endif public static Ooui.Element GetOouiElement (this Xamarin.Forms.Page page) { diff --git a/Ooui.Forms/Platform.cs b/Ooui.Forms/Platform.cs index 96197cb..635fe97 100644 --- a/Ooui.Forms/Platform.cs +++ b/Ooui.Forms/Platform.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Ooui.Forms.Renderers; using Xamarin.Forms; using Xamarin.Forms.Internals; -using System.Web; namespace Ooui.Forms { @@ -142,7 +141,7 @@ namespace Ooui.Forms void AddChild (VisualElement view) { if (!Application.IsApplicationOrNull (view.RealParent)) - Console.Error.WriteLine ("Tried to add parented view to canvas directly"); + System.Diagnostics.Debug.WriteLine ("Tried to add parented view to canvas directly"); if (GetRenderer (view) == null) { var viewRenderer = CreateRenderer (view); @@ -152,7 +151,7 @@ namespace Ooui.Forms viewRenderer.SetElementSize (new Size (640, 480)); } else - Console.Error.WriteLine ("Potential view double add"); + System.Diagnostics.Debug.WriteLine ("Potential view double add"); } void HandleRendererStyle_PropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e) diff --git a/Ooui.Forms/Renderers/ImageRenderer.cs b/Ooui.Forms/Renderers/ImageRenderer.cs index 3f9e6e2..7b429bc 100644 --- a/Ooui.Forms/Renderers/ImageRenderer.cs +++ b/Ooui.Forms/Renderers/ImageRenderer.cs @@ -139,8 +139,12 @@ namespace Ooui.Forms.Renderers public sealed class FileImageSourceHandler : IImageSourceHandler { +#pragma warning disable 1998 public async Task LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f) { +#if PCL + return null; +#else string image = null; var filesource = imagesource as FileImageSource; var file = filesource?.File; @@ -155,6 +159,7 @@ namespace Ooui.Forms.Renderers } } return image; +#endif } } @@ -162,6 +167,9 @@ namespace Ooui.Forms.Renderers { public async Task LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f) { +#if PCL + return null; +#else string image = null; var streamsource = imagesource as StreamImageSource; if (streamsource?.Stream != null) { @@ -171,7 +179,7 @@ namespace Ooui.Forms.Renderers using (var outputStream = new System.IO.MemoryStream (data)) { await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false); } - var hash = Ooui.UI.Hash (data); + var hash = Ooui.Utilities.Hash (data); var etag = "\"" + hash + "\""; image = "/images/" + hash; if (Ooui.UI.TryGetFileContentAtPath (image, out var file) && file.Etag == etag) { @@ -188,6 +196,7 @@ namespace Ooui.Forms.Renderers System.Diagnostics.Debug.WriteLine ("Could not load image: {0}", streamsource); } return image; +#endif } } diff --git a/Ooui.Forms/ResourcesProvider.cs b/Ooui.Forms/ResourcesProvider.cs index c0c05a3..c71be52 100644 --- a/Ooui.Forms/ResourcesProvider.cs +++ b/Ooui.Forms/ResourcesProvider.cs @@ -40,3 +40,12 @@ namespace Ooui.Forms } } } + +#if PCL +namespace System.Collections.Concurrent +{ + class ConcurrentDictionary : Dictionary + { + } +} +#endif diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html index cfb1dfb..f47470c 100644 --- a/Ooui.Wasm/ooui-sample.html +++ b/Ooui.Wasm/ooui-sample.html @@ -44,6 +44,9 @@ "System.Xml.dll", "System.Xml.ReaderWriter.dll", "System.Xml.XDocument.dll", + "Xamarin.Forms.Core.dll", + "Xamarin.Forms.Platform.dll", + "Xamarin.Forms.Xaml.dll", "Ooui.dll", mainAsmName + ".dll" ]; diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 893ef63..dc87fa9 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -14,10 +14,8 @@ namespace Ooui public const int MaxFps = 30; #if !PCL - static readonly ManualResetEvent started = new ManualResetEvent (false); - [ThreadStatic] - static System.Security.Cryptography.SHA256 sha256; + static readonly ManualResetEvent started = new ManualResetEvent (false); static CancellationTokenSource serverCts; @@ -82,22 +80,7 @@ namespace Ooui clientJsBytes = Encoding.UTF8.GetBytes (r.ReadToEnd ()); } } - clientJsEtag = "\"" + Hash (clientJsBytes) + "\""; - } - - public static string Hash (byte[] bytes) - { - var sha = sha256; - if (sha == null) { - sha = System.Security.Cryptography.SHA256.Create (); - sha256 = sha; - } - var data = sha.ComputeHash (bytes); - StringBuilder sBuilder = new StringBuilder (); - for (int i = 0; i < data.Length; i++) { - sBuilder.Append (data[i].ToString ("x2")); - } - return sBuilder.ToString (); + clientJsEtag = "\"" + Utilities.Hash (clientJsBytes) + "\""; } static void Publish (string path, RequestHandler handler) @@ -129,13 +112,13 @@ namespace Ooui if (contentType == null) { contentType = GuessContentType (path, filePath); } - var etag = "\"" + Hash (data) + "\""; + var etag = "\"" + Utilities.Hash (data) + "\""; Publish (path, new DataHandler (data, etag, contentType)); } public static void PublishFile (string path, byte[] data, string contentType) { - var etag = "\"" + Hash (data) + "\""; + var etag = "\"" + Utilities.Hash (data) + "\""; Publish (path, new DataHandler (data, etag, contentType)); } @@ -180,7 +163,7 @@ namespace Ooui public static void PublishJson (string path, object value) { var data = JsonHandler.GetData (value); - var etag = "\"" + Hash (data) + "\""; + var etag = "\"" + Utilities.Hash (data) + "\""; Publish (path, new DataHandler (data, etag, JsonHandler.ContentType)); } @@ -379,13 +362,18 @@ namespace Ooui } } + static string EscapeHtml (string text) + { + return text.Replace ("&", "&").Replace ("<", "<"); + } + public static void RenderTemplate (TextWriter writer, string webSocketPath, string title, string initialHtml) { writer.Write (@" "); - writer.Write (title.Replace ("&", "&").Replace ("<", "<")); + writer.Write (EscapeHtml (title)); writer.Write (@" "); diff --git a/Ooui/Utilities.cs b/Ooui/Utilities.cs new file mode 100644 index 0000000..a850099 --- /dev/null +++ b/Ooui/Utilities.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; + +namespace Ooui +{ + public static class Utilities + { +#if PCL + + static readonly uint[] crcTable; + + static Utilities () + { + uint p = 0x04C11DB7; + crcTable = new uint[256]; + for (uint c = 0; c <= 0xFF; c++) { + crcTable[c] = CrcReflect (c, 8) << 24; + for (uint i = 0; i < 8; i++) { + crcTable[c] = (crcTable[c] << 1) ^ (((crcTable[c] & (1u << 31)) != 0) ? p : 0); + } + crcTable[c] = CrcReflect (crcTable[c], 32); + } + } + + static uint CrcReflect (uint r, byte c) + { + uint v = 0; + for (int i = 1; i < (c + 1); i++) { + if ((r & 1) != 0) { + v |= (1u << (c - i)); + } + r >>= 1; + } + return v; + } + + public static string Hash (byte[] bytes) + { + uint crc = 0xffffffffu; + for (var i = 0; i < bytes.Length; i++) { + crc = (crc >> 8) ^ crcTable[(crc & 0xff) ^ bytes[i]]; + } + crc ^= 0xffffffffu; + return crc.ToString ("x8"); + } + +#else + + [ThreadStatic] + static System.Security.Cryptography.SHA256 sha256; + + public static string Hash (byte[] bytes) + { + var sha = sha256; + if (sha == null) { + sha = System.Security.Cryptography.SHA256.Create (); + sha256 = sha; + } + var data = sha.ComputeHash (bytes); + StringBuilder sBuilder = new StringBuilder (); + for (int i = 0; i < data.Length; i++) { + sBuilder.Append (data[i].ToString ("x2")); + } + return sBuilder.ToString (); + } + +#endif + } +} From 78528a0c87f078923b32e9c1831658579ba851c3 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 23:12:07 -0800 Subject: [PATCH 24/52] BoxViewClock Forms sample working in wasm --- Ooui.Wasm/README.md | 2 +- Ooui.Wasm/ooui-sample.cs | 30 ++++++++++++++++++-------- Ooui.Wasm/ooui-sample.html | 2 ++ Ooui/Session.cs | 2 +- Ooui/WebAssemblySession.cs | 43 +++++++++++++++++++++----------------- 5 files changed, 49 insertions(+), 30 deletions(-) diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md index d9af42a..d48b9fb 100644 --- a/Ooui.Wasm/README.md +++ b/Ooui.Wasm/README.md @@ -13,7 +13,7 @@ Expand that into this directory. ## Build ```bash -csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/System.Runtime.dll /r:managed/Ooui.dll /out:managed/Ooui.Sample.dll ooui-sample.cs +csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/System.Runtime.dll /r:managed/Xamarin.Forms.Core.dll /r:managed/Ooui.dll /r:managed/Ooui.Forms.dll /out:managed/Ooui.Sample.dll ../Samples/ISample.cs ../Samples/BoxViewClockSample.cs ooui-sample.cs ``` diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs index ac57e88..896bd1c 100644 --- a/Ooui.Wasm/ooui-sample.cs +++ b/Ooui.Wasm/ooui-sample.cs @@ -3,18 +3,30 @@ using Ooui; public class Program { + static Element GetButtonElement () + { + var l = new Label { Text = "Hello" }; + var b = new Button ("Click Me"); + var e = new Div (new Div (l), b); + int count = 0; + b.Click += (s, ee) => { + count++; + b.Text = $"Clicked {count} times"; + }; + return e; + } + + static Element GetBoxViewClockElement () + { + var s = new Samples.BoxViewClockSample (); + return s.CreateElement (); + } + public static string Main (string a0, string a1) { + Xamarin.Forms.Forms.Init (); try { - var l = new Label { Text = "Hello" }; - var b = new Button ("Click Me"); - var e = new Div (new Div (l), b); - int count = 0; - b.Click += (s, ee) => { - count++; - b.Text = $"Clicked {count} times"; - }; - + var e = GetBoxViewClockElement (); UI.SetGlobalElement ("main", e); return e.ToString (); } diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html index f47470c..1a610a9 100644 --- a/Ooui.Wasm/ooui-sample.html +++ b/Ooui.Wasm/ooui-sample.html @@ -37,6 +37,7 @@ "System.Threading.Tasks.dll", "System.Reflection.dll", "System.Reflection.Extensions.dll", + "System.Resources.ResourceManager.dll", "System.Runtime.dll", "System.Runtime.Extensions.dll", "System.Runtime.Serialization.dll", @@ -48,6 +49,7 @@ "Xamarin.Forms.Platform.dll", "Xamarin.Forms.Xaml.dll", "Ooui.dll", + "Ooui.Forms.dll", mainAsmName + ".dll" ]; oouiWasm (mainAsmName, "", "Program", "Main", assemblies); diff --git a/Ooui/Session.cs b/Ooui/Session.cs index 029c530..2b8570e 100644 --- a/Ooui/Session.cs +++ b/Ooui/Session.cs @@ -44,7 +44,7 @@ namespace Ooui } } - void QueueMessageLocked (Message message) + protected void QueueMessageLocked (Message message) { // // Make sure all the referenced objects have been created diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs index df1b3e9..b7d53f4 100644 --- a/Ooui/WebAssemblySession.cs +++ b/Ooui/WebAssemblySession.cs @@ -18,38 +18,43 @@ namespace Ooui protected override void QueueMessage (Message message) { - base.QueueMessage (message); - TransmitQueuedMessages (); - } - - void TransmitQueuedMessages () - { - // - // Dequeue as many messages as we can - // - var messagesToSend = new List (); lock (queuedMessages) { - messagesToSend.AddRange (queuedMessages); + QueueMessageLocked (message); + var max = 1; + var i = 0; + while (i < queuedMessages.Count) { + TransmitQueuedMessagesLocked (queuedMessages, i, max); + i += max; + } + // WebAssembly.Runtime.InvokeJS("console.log('TRANSMITTED'," + queuedMessages.Count + ")"); queuedMessages.Clear (); } + } + void TransmitQueuedMessagesLocked (List messagesToSend, int startIndex, int max) + { if (messagesToSend.Count == 0) return; // // Now actually send the messages // - var sb = new StringBuilder (); + var sb = new System.IO.StringWriter (); + sb.Write ("__oouiReceiveMessages(\""); + sb.Write (id); + sb.Write ("\","); + sb.Write ("["); var head = ""; - sb.Append ("["); - foreach (var m in messagesToSend) { - sb.Append (head); - sb.Append (m.ToJson ()); + int n = 0; + for (var i = startIndex; i < messagesToSend.Count && n < max; i++, n++) { + sb.Write (head); + messagesToSend[i].WriteJson (sb); head = ","; } - sb.Append ("]"); - var json = sb.ToString (); - WebAssembly.Runtime.InvokeJS ("__oouiReceiveMessages(\"" + id + "\", " + json + ")"); + sb.Write ("])"); + var jsonp = sb.ToString (); + // WebAssembly.Runtime.InvokeJS("console.log('TRANSMIT',"+n+")"); + WebAssembly.Runtime.InvokeJS (jsonp); } public void ReceiveMessageJson (string json) From 5f382847f94948294330e912b0061f52b17d39c1 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 23:13:41 -0800 Subject: [PATCH 25/52] Update README.md --- Ooui.Wasm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md index d48b9fb..243f84e 100644 --- a/Ooui.Wasm/README.md +++ b/Ooui.Wasm/README.md @@ -23,4 +23,4 @@ csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/System.Runtime. python server.py ``` -Go to `locahost:8000/ooui.html` +Go to `locahost:8000/ooui-sample.html` From ea8cdfa25e4398e322b11d624f601cb5800ed650 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 23:14:18 -0800 Subject: [PATCH 26/52] Update README.md --- Ooui.Wasm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md index 243f84e..5bdc6b9 100644 --- a/Ooui.Wasm/README.md +++ b/Ooui.Wasm/README.md @@ -23,4 +23,4 @@ csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/System.Runtime. python server.py ``` -Go to `locahost:8000/ooui-sample.html` +Go to [locahost:8000/ooui-sample.html](http://locahost:8000/ooui-sample.html) From 80dc2bfaf99dd730ca814ce6bc3a8ef0cb2fb2cd Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Mon, 12 Mar 2018 19:42:08 -0700 Subject: [PATCH 27/52] Fix json serializing unknown types --- Ooui/JsonConvert.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ooui/JsonConvert.cs b/Ooui/JsonConvert.cs index fdeeed2..fc8e86a 100644 --- a/Ooui/JsonConvert.cs +++ b/Ooui/JsonConvert.cs @@ -86,7 +86,7 @@ namespace Ooui w.Write (((float)value).ToString (icult)); } - WriteJsonString (w, Convert.ToString (value, icult)); + w.Write (Newtonsoft.Json.JsonConvert.SerializeObject (value)); } public static string SerializeObject (object value) From 1f5b1fe13a5ee7846ddc955d918ed6def02cdb3d Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Mon, 12 Mar 2018 20:56:12 -0700 Subject: [PATCH 28/52] Stop using XAML for editor sample --- Samples/Samples.csproj | 6 -- Samples/XamlPreviewPage.xaml | 22 ------- Samples/XamlPreviewPage.xaml.cs | 72 --------------------- Samples/XamlPreviewPageSample.cs | 105 ++++++++++++++++++++++++++++++- 4 files changed, 103 insertions(+), 102 deletions(-) delete mode 100644 Samples/XamlPreviewPage.xaml delete mode 100644 Samples/XamlPreviewPage.xaml.cs diff --git a/Samples/Samples.csproj b/Samples/Samples.csproj index 98ffe10..effd0d6 100644 --- a/Samples/Samples.csproj +++ b/Samples/Samples.csproj @@ -32,9 +32,6 @@ DisplayAlertPage.xaml - - XamlPreviewPage.xaml - @@ -56,9 +53,6 @@ MSBuild:Compile - - MSBuild:UpdateDesignTimeXaml - diff --git a/Samples/XamlPreviewPage.xaml b/Samples/XamlPreviewPage.xaml deleted file mode 100644 index a96fa29..0000000 --- a/Samples/XamlPreviewPage.xaml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/XamlPreviewPage.xaml.cs b/Samples/XamlPreviewPage.xaml.cs deleted file mode 100644 index 21f9318..0000000 --- a/Samples/XamlPreviewPage.xaml.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Xamarin.Forms; - -namespace Samples -{ - public partial class XamlPreviewPage : ContentPage - { - public XamlPreviewPage () - { - InitializeComponent (); - - editor.Text = @" - - - - - - - - - - -