diff --git a/Documentation/MeasureText.html b/Documentation/MeasureText.html new file mode 100644 index 0000000..e645b1e --- /dev/null +++ b/Documentation/MeasureText.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ooui.AspNetCore/Ooui.AspNetCore.csproj b/Ooui.AspNetCore/Ooui.AspNetCore.csproj index bb62fe3..786c6ac 100644 --- a/Ooui.AspNetCore/Ooui.AspNetCore.csproj +++ b/Ooui.AspNetCore/Ooui.AspNetCore.csproj @@ -1,6 +1,14 @@ + 1.0.0 + praeclarum + ASP.NET Core MVC extensions to make working with Ooui easy. + Ooui;UI;CrossPlatform;ASP.NET + https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png + https://github.com/praeclarum/Ooui + https://github.com/praeclarum/Ooui/blob/master/LICENSE + https://github.com/praeclarum/Ooui.git netstandard2.0 diff --git a/Ooui.AspNetCore/OouiMiddlewareExtensions.cs b/Ooui.AspNetCore/OouiMiddlewareExtensions.cs index deaa51e..d07f8e9 100644 --- a/Ooui.AspNetCore/OouiMiddlewareExtensions.cs +++ b/Ooui.AspNetCore/OouiMiddlewareExtensions.cs @@ -25,17 +25,27 @@ namespace Microsoft.AspNetCore.Builder }; app.UseWebSockets (webSocketOptions); + Ooui.UI.ServerEnabled = false; + app.Use (async (context, next) => { + var response = context.Response; + if (context.Request.Path == jsPath) { - var response = context.Response; var clientJsBytes = Ooui.UI.ClientJsBytes; - response.StatusCode = 200; - response.ContentLength = clientJsBytes.Length; - response.ContentType = "application/javascript; charset=utf-8"; - response.Headers.Add ("Cache-Control", "public, max-age=3600"); - using (var s = response.Body) { - await s.WriteAsync (clientJsBytes, 0, clientJsBytes.Length).ConfigureAwait (false); + var clientJsEtag = Ooui.UI.ClientJsEtag; + if (context.Request.Headers.TryGetValue ("If-None-Match", out var inms) && inms.Count > 0 && inms[0] == clientJsEtag) { + response.StatusCode = 304; + } + else { + response.StatusCode = 200; + response.ContentLength = clientJsBytes.Length; + response.ContentType = "application/javascript; charset=utf-8"; + response.Headers.Add ("Cache-Control", "public, max-age=60"); + response.Headers.Add ("Etag", clientJsEtag); + using (var s = response.Body) { + await s.WriteAsync (clientJsBytes, 0, clientJsBytes.Length).ConfigureAwait (false); + } } } else if (context.Request.Path == WebSocketHandler.WebSocketPath) { @@ -46,6 +56,21 @@ namespace Microsoft.AspNetCore.Builder context.Response.StatusCode = 400; } } + else if (Ooui.UI.TryGetFileContentAtPath (context.Request.Path, out var file)) { + if (context.Request.Headers.TryGetValue ("If-None-Match", out var inms) && inms.Count > 0 && inms[0] == file.Etag) { + response.StatusCode = 304; + } + else { + response.StatusCode = 200; + response.ContentLength = file.Content.Length; + response.ContentType = file.ContentType; + response.Headers.Add ("Cache-Control", "public, max-age=60"); + response.Headers.Add ("Etag", file.Etag); + using (var s = response.Body) { + await s.WriteAsync (file.Content, 0, file.Content.Length).ConfigureAwait (false); + } + } + } else { await next ().ConfigureAwait (false); } diff --git a/Ooui.AspNetCore/WebSocketHandler.cs b/Ooui.AspNetCore/WebSocketHandler.cs index ce6f417..a483e01 100644 --- a/Ooui.AspNetCore/WebSocketHandler.cs +++ b/Ooui.AspNetCore/WebSocketHandler.cs @@ -86,9 +86,10 @@ namespace Ooui.AspNetCore BadRequest ("Missing `h`"); return; } - if (!double.TryParse (wValues.Last (), out var w)) + var icult = System.Globalization.CultureInfo.InvariantCulture; + if (!double.TryParse (wValues.Last (), System.Globalization.NumberStyles.Any, icult, out var w)) w = 640; - if (!double.TryParse (hValues.Last (), out var h)) + if (!double.TryParse (hValues.Last (), System.Globalization.NumberStyles.Any, icult, out var h)) h = 480; // diff --git a/Ooui.Forms/DisplayAlert.cs b/Ooui.Forms/DisplayAlert.cs index d5f22f5..fae811a 100644 --- a/Ooui.Forms/DisplayAlert.cs +++ b/Ooui.Forms/DisplayAlert.cs @@ -59,7 +59,7 @@ namespace Ooui.Forms { ClassName = "btn btn-default" }; - _cancelButton.Clicked += (s, e) => SetResult(false); + _cancelButton.Click += (s, e) => SetResult(false); footer.AppendChild(_cancelButton); @@ -70,7 +70,7 @@ namespace Ooui.Forms ClassName = "btn btn-default" }; - _acceptButton.Clicked += (s, e) => SetResult(true); + _acceptButton.Click += (s, e) => SetResult(true); footer.AppendChild(_acceptButton); } @@ -90,23 +90,23 @@ namespace Ooui.Forms { add { - _closeButton.Clicked += value; + _closeButton.Click += value; if(_cancelButton != null) - _cancelButton.Clicked += value; + _cancelButton.Click += value; if(_acceptButton != null) - _acceptButton.Clicked += value; + _acceptButton.Click += value; } remove { - _closeButton.Clicked -= value; + _closeButton.Click -= value; if (_cancelButton != null) - _cancelButton.Clicked -= value; + _cancelButton.Click -= value; if (_acceptButton != null) - _acceptButton.Clicked -= value; + _acceptButton.Click -= value; } } public Element Element { get; private set; } diff --git a/Ooui.Forms/EventTracker.cs b/Ooui.Forms/EventTracker.cs new file mode 100644 index 0000000..afa79f0 --- /dev/null +++ b/Ooui.Forms/EventTracker.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using Xamarin.Forms; + +using NativeView = Ooui.Element; + +namespace Ooui.Forms +{ + public class EventTracker + { + readonly NotifyCollectionChangedEventHandler _collectionChangedHandler; + + readonly Dictionary _gestureRecognizers = new Dictionary (); + + readonly IVisualElementRenderer _renderer; + bool _disposed; + NativeView _handler; + + public EventTracker (IVisualElementRenderer renderer) + { + if (renderer == null) + throw new ArgumentNullException (nameof (renderer)); + + _collectionChangedHandler = ModelGestureRecognizersOnCollectionChanged; + + _renderer = renderer; + _renderer.ElementChanged += OnElementChanged; + } + + ObservableCollection ElementGestureRecognizers { + get { + if (_renderer?.Element is View) + return ((View)_renderer.Element).GestureRecognizers as ObservableCollection; + return null; + } + } + + public void Dispose () + { + if (_disposed) + return; + + _disposed = true; + + foreach (var kvp in _gestureRecognizers) { + RemoveGestureRecognizer (_handler, kvp.Value); + kvp.Value.Dispose (); + } + + _gestureRecognizers.Clear (); + + if (ElementGestureRecognizers != null) + ElementGestureRecognizers.CollectionChanged -= _collectionChangedHandler; + + _handler = null; + } + + void ModelGestureRecognizersOnCollectionChanged (object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) + { + LoadRecognizers (); + } + + void OnElementChanged (object sender, VisualElementChangedEventArgs e) + { + if (e.OldElement != null) { + // unhook + var oldView = e.OldElement as View; + if (oldView != null) { + var oldRecognizers = (ObservableCollection)oldView.GestureRecognizers; + oldRecognizers.CollectionChanged -= _collectionChangedHandler; + } + } + + if (e.NewElement != null) { + // hook + if (ElementGestureRecognizers != null) { + ElementGestureRecognizers.CollectionChanged += _collectionChangedHandler; + LoadRecognizers (); + } + } + } + + void LoadRecognizers () + { + if (ElementGestureRecognizers == null) + return; + + foreach (var recognizer in ElementGestureRecognizers) { + if (_gestureRecognizers.ContainsKey (recognizer)) + continue; + + var nativeRecognizer = GetNativeRecognizer (recognizer); + if (nativeRecognizer != null) { + AddGestureRecognizer (_handler, nativeRecognizer); + + _gestureRecognizers[recognizer] = nativeRecognizer; + } + } + + var toRemove = _gestureRecognizers.Keys.Where (key => !ElementGestureRecognizers.Contains (key)).ToArray (); + foreach (var gestureRecognizer in toRemove) { + var uiRecognizer = _gestureRecognizers[gestureRecognizer]; + _gestureRecognizers.Remove (gestureRecognizer); + + RemoveGestureRecognizer (_handler, uiRecognizer); + uiRecognizer.Dispose (); + } + } + + protected virtual NativeGestureRecognizer GetNativeRecognizer (IGestureRecognizer recognizer) + { + if (recognizer == null) + return null; + + var weakRecognizer = new WeakReference (recognizer); + var weakEventTracker = new WeakReference (this); + + var tapRecognizer = recognizer as TapGestureRecognizer; + if (tapRecognizer != null && tapRecognizer.NumberOfTapsRequired == 1) { + var returnAction = new TargetEventHandler ((s, e) => { + var tapGestureRecognizer = weakRecognizer.Target as TapGestureRecognizer; + var eventTracker = weakEventTracker.Target as EventTracker; + var view = eventTracker?._renderer?.Element as View; + + if (tapGestureRecognizer != null && view != null) + tapGestureRecognizer.SendTapped (view); + }); + var uiRecognizer = new NativeGestureRecognizer { + EventType = "click", + Handler = returnAction, + }; + return uiRecognizer; + } + if (tapRecognizer != null && tapRecognizer.NumberOfTapsRequired == 2) { + var returnAction = new TargetEventHandler ((s, e) => { + var tapGestureRecognizer = weakRecognizer.Target as TapGestureRecognizer; + var eventTracker = weakEventTracker.Target as EventTracker; + var view = eventTracker?._renderer?.Element as View; + + if (tapGestureRecognizer != null && view != null) + tapGestureRecognizer.SendTapped (view); + }); + var uiRecognizer = new NativeGestureRecognizer { + EventType = "dblclick", + Handler = returnAction, + }; + return uiRecognizer; + } + + return null; + } + + static void AddGestureRecognizer (Element element, NativeGestureRecognizer recognizer) + { + element.AddEventListener (recognizer.EventType, recognizer.Handler); + } + + static void RemoveGestureRecognizer (Element element, NativeGestureRecognizer recognizer) + { + element.RemoveEventListener (recognizer.EventType, recognizer.Handler); + } + + public void LoadEvents (NativeView handler) + { + if (_disposed) + throw new ObjectDisposedException (null); + + _handler = handler; + OnElementChanged (this, new VisualElementChangedEventArgs (null, _renderer.Element)); + } + + protected class NativeGestureRecognizer : IDisposable + { + public string EventType; + public TargetEventHandler Handler; + public void Dispose () + { + } + } + } +} diff --git a/Ooui.Forms/Exports.cs b/Ooui.Forms/Exports.cs index c823d1b..b093983 100644 --- a/Ooui.Forms/Exports.cs +++ b/Ooui.Forms/Exports.cs @@ -11,8 +11,14 @@ using Xamarin.Forms.Internals; [assembly: ExportRenderer (typeof (DatePicker), typeof (DatePickerRenderer))] [assembly: ExportRenderer (typeof (Editor), typeof (EditorRenderer))] [assembly: ExportRenderer (typeof (Entry), typeof (EntryRenderer))] +[assembly: ExportRenderer (typeof (Frame), typeof (FrameRenderer))] +[assembly: ExportRenderer (typeof (Image), typeof (ImageRenderer))] [assembly: ExportRenderer (typeof (Label), typeof (LabelRenderer))] [assembly: ExportRenderer (typeof (ProgressBar), typeof (ProgressBarRenderer))] +[assembly: ExportRenderer (typeof (Switch), typeof (SwitchRenderer))] +[assembly: ExportImageSourceHandler (typeof (FileImageSource), typeof (FileImageSourceHandler))] +[assembly: ExportImageSourceHandler (typeof (StreamImageSource), typeof (StreamImagesourceHandler))] +[assembly: ExportImageSourceHandler (typeof (UriImageSource), typeof (ImageLoaderSourceHandler))] namespace Ooui.Forms { @@ -24,4 +30,13 @@ namespace Ooui.Forms { } } + + [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class ExportImageSourceHandlerAttribute : HandlerAttribute + { + public ExportImageSourceHandlerAttribute (Type handler, Type target) + : base (handler, target) + { + } + } } diff --git a/Ooui.Forms/Extensions/ElementExtensions.cs b/Ooui.Forms/Extensions/ElementExtensions.cs index 50b7cb6..53c1366 100644 --- a/Ooui.Forms/Extensions/ElementExtensions.cs +++ b/Ooui.Forms/Extensions/ElementExtensions.cs @@ -16,7 +16,7 @@ namespace Ooui.Forms.Extensions if (self.Style.Width.Equals ("inherit")) { s = self.Text.MeasureSize (self.Style); measured = true; - rw = double.IsPositiveInfinity (s.Width) ? double.PositiveInfinity : s.Width; + rw = double.IsPositiveInfinity (s.Width) ? double.PositiveInfinity : Math.Ceiling (s.Width); } else { rw = self.Style.GetNumberWithUnits ("width", "px", 640); @@ -27,7 +27,7 @@ namespace Ooui.Forms.Extensions s = self.Text.MeasureSize (self.Style); measured = true; } - rh = double.IsPositiveInfinity (s.Height) ? double.PositiveInfinity : s.Height; + rh = double.IsPositiveInfinity (s.Height) ? double.PositiveInfinity : Math.Ceiling (s.Height * 1.4); } else { rh = self.Style.GetNumberWithUnits ("height", "px", 480); diff --git a/Ooui.Forms/Extensions/FontExtensions.cs b/Ooui.Forms/Extensions/FontExtensions.cs index 310ece6..f364559 100644 --- a/Ooui.Forms/Extensions/FontExtensions.cs +++ b/Ooui.Forms/Extensions/FontExtensions.cs @@ -44,9 +44,23 @@ namespace Ooui.Forms.Extensions return Size.Zero; var fontHeight = fontSize; - var charWidth = fontSize * 0.5; - var width = text.Length * charWidth; + var isBold = fontAttrs.HasFlag (FontAttributes.Bold); + + var props = isBold ? BoldCharacterProportions : CharacterProportions; + var avgp = isBold ? BoldAverageCharProportion : AverageCharProportion; + + var pwidth = 1.0e-6; // Tiny little padding to account for sampling errors + for (var i = 0; i < text.Length; i++) { + var c = (int)text[i]; + if (c < 128) { + pwidth += props[c]; + } + else { + pwidth += avgp; + } + } + var width = fontSize * pwidth; return new Size (width, fontHeight); } @@ -69,5 +83,57 @@ namespace Ooui.Forms.Extensions } } + public static string ToOouiVerticalAlign (this TextAlignment align) + { + switch (align) { + case TextAlignment.Start: + default: + return "top"; + case TextAlignment.Center: + return "middle"; + case TextAlignment.End: + return "bottom"; + } + } + + static readonly double[] CharacterProportions = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0.27799999713897705, 0.27799999713897705, 0.27799999713897705, 0.27799999713897705, 0.27799999713897705, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0.27799999713897705, 0.25899994373321533, 0.4259999990463257, 0.5560001134872437, 0.5560001134872437, 1.0000001192092896, 0.6299999952316284, 0.27799999713897705, + 0.25899994373321533, 0.25899994373321533, 0.3520001173019409, 0.6000000238418579, 0.27799999713897705, 0.3890000581741333, 0.27799999713897705, 0.3330000638961792, + 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, + 0.5560001134872437, 0.5560001134872437, 0.27799999713897705, 0.27799999713897705, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.5560001134872437, + 0.8000000715255737, 0.6480001211166382, 0.6850000619888306, 0.722000002861023, 0.7040001153945923, 0.6110001802444458, 0.5740000009536743, 0.7589999437332153, + 0.722000002861023, 0.25899994373321533, 0.5190001726150513, 0.6669999361038208, 0.5560001134872437, 0.8709999322891235, 0.722000002861023, 0.7600001096725464, + 0.6480001211166382, 0.7600001096725464, 0.6850000619888306, 0.6480001211166382, 0.5740000009536743, 0.722000002861023, 0.6110001802444458, 0.9259999990463257, + 0.6110001802444458, 0.6480001211166382, 0.6110001802444458, 0.25899994373321533, 0.3330000638961792, 0.25899994373321533, 0.6000000238418579, 0.5000001192092896, + 0.22200000286102295, 0.5370000600814819, 0.593000054359436, 0.5370000600814819, 0.593000054359436, 0.5370000600814819, 0.2960001230239868, 0.5740000009536743, + 0.5560001134872437, 0.22200000286102295, 0.22200000286102295, 0.5190001726150513, 0.22200000286102295, 0.8530000448226929, 0.5560001134872437, 0.5740000009536743, + 0.593000054359436, 0.593000054359436, 0.3330000638961792, 0.5000001192092896, 0.31500017642974854, 0.5560001134872437, 0.5000001192092896, 0.7580000162124634, + 0.5180000066757202, 0.5000001192092896, 0.4800001382827759, 0.3330000638961792, 0.22200000286102295, 0.3330000638961792, 0.6000000238418579, 0 + }; + const double AverageCharProportion = 0.5131400561332703; + + static readonly double[] BoldCharacterProportions = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0.27799999713897705, 0.27799999713897705, 0.27799999713897705, 0.27799999713897705, 0.27799999713897705, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0.27799999713897705, 0.27799999713897705, 0.46299993991851807, 0.5560001134872437, 0.5560001134872437, 1.0000001192092896, 0.6850000619888306, 0.27799999713897705, + 0.2960001230239868, 0.2960001230239868, 0.40700018405914307, 0.6000000238418579, 0.27799999713897705, 0.40700018405914307, 0.27799999713897705, 0.37099993228912354, + 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, 0.5560001134872437, + 0.5560001134872437, 0.5560001134872437, 0.27799999713897705, 0.27799999713897705, 0.6000000238418579, 0.6000000238418579, 0.6000000238418579, 0.5560001134872437, + 0.8000000715255737, 0.6850000619888306, 0.7040001153945923, 0.7410000562667847, 0.7410000562667847, 0.6480001211166382, 0.593000054359436, 0.7589999437332153, + 0.7410000562667847, 0.29499995708465576, 0.5560001134872437, 0.722000002861023, 0.593000054359436, 0.9070001840591431, 0.7410000562667847, 0.777999997138977, + 0.6669999361038208, 0.777999997138977, 0.722000002861023, 0.6490000486373901, 0.6110001802444458, 0.7410000562667847, 0.6299999952316284, 0.9440001249313354, + 0.6669999361038208, 0.6669999361038208, 0.6480001211166382, 0.3330000638961792, 0.37099993228912354, 0.3330000638961792, 0.6000000238418579, 0.5000001192092896, + 0.25899994373321533, 0.5740000009536743, 0.6110001802444458, 0.5740000009536743, 0.6110001802444458, 0.5740000009536743, 0.3330000638961792, 0.6110001802444458, + 0.593000054359436, 0.2580000162124634, 0.27799999713897705, 0.5740000009536743, 0.2580000162124634, 0.906000018119812, 0.593000054359436, 0.6110001802444458, + 0.6110001802444458, 0.6110001802444458, 0.3890000581741333, 0.5370000600814819, 0.3520001173019409, 0.593000054359436, 0.5200001001358032, 0.8140000104904175, + 0.5370000600814819, 0.5190001726150513, 0.5190001726150513, 0.3330000638961792, 0.223000168800354, 0.3330000638961792, 0.6000000238418579, 0 + }; + const double BoldAverageCharProportion = 0.5346300601959229; } } diff --git a/Ooui.Forms/Forms.cs b/Ooui.Forms/Forms.cs index c5a39f7..94bc20e 100644 --- a/Ooui.Forms/Forms.cs +++ b/Ooui.Forms/Forms.cs @@ -26,13 +26,13 @@ namespace Xamarin.Forms Device.SetIdiom (TargetIdiom.Desktop); Device.PlatformServices = new OouiPlatformServices (); Device.Info = new OouiDeviceInfo (); - Color.SetAccent (Color.FromHex ("#0000EE")); // Safari Blue + Color.SetAccent (Color.FromHex ("#337ab7")); // Bootstrap Blue Registrar.RegisterAll (new[] { typeof(ExportRendererAttribute), - //typeof(ExportCellAttribute), - //typeof(ExportImageSourceHandlerAttribute), - }); + //typeof(ExportCellAttribute), + typeof(ExportImageSourceHandlerAttribute), + }); } public static event EventHandler ViewInitialized; @@ -62,11 +62,6 @@ namespace Xamarin.Forms Task.Run (action); } - public Ticker CreateTicker () - { - throw new NotImplementedException (); - } - public Assembly[] GetAssemblies () { return AppDomain.CurrentDomain.GetAssemblies (); @@ -119,6 +114,31 @@ namespace Xamarin.Forms } }), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds); } + + public Ticker CreateTicker () + { + return new OouiTicker (); + } + + class OouiTicker : Ticker + { + Timer timer; + protected override void DisableTimer () + { + var t = timer; + timer = null; + t?.Dispose (); + } + protected override void EnableTimer () + { + if (timer != null) + return; + var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.Session.MaxFps); + timer = new Timer ((_ => { + this.SendSignals (); + }), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds); + } + } } public class ViewInitializedEventArgs diff --git a/Ooui.Forms/Ooui.Forms.csproj b/Ooui.Forms/Ooui.Forms.csproj index e081c10..8bcdc99 100644 --- a/Ooui.Forms/Ooui.Forms.csproj +++ b/Ooui.Forms/Ooui.Forms.csproj @@ -1,6 +1,14 @@ + 1.0.0 + praeclarum + Xamarin.Forms backend for the web using Ooui technologies. + Ooui;UI;CrossPlatform;Xamarin.Forms + https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png + https://github.com/praeclarum/Ooui + https://github.com/praeclarum/Ooui/blob/master/LICENSE + https://github.com/praeclarum/Ooui.git netstandard2.0 diff --git a/Ooui.Forms/Platform.cs b/Ooui.Forms/Platform.cs index 938adb2..96197cb 100644 --- a/Ooui.Forms/Platform.cs +++ b/Ooui.Forms/Platform.cs @@ -57,6 +57,10 @@ namespace Ooui.Forms MessagingCenter.Unsubscribe (this, Page.ActionSheetSignalName); MessagingCenter.Unsubscribe (this, Page.AlertSignalName); MessagingCenter.Unsubscribe (this, Page.BusySetSignalName); + + DisposeModelAndChildrenRenderers (Page); + //foreach (var modal in _modals) + //DisposeModelAndChildrenRenderers (modal); } public static IVisualElementRenderer CreateRenderer (VisualElement element) @@ -110,7 +114,29 @@ namespace Ooui.Forms void HandleChildRemoved (object sender, ElementEventArgs e) { - throw new NotImplementedException (); + var view = e.Element; + DisposeModelAndChildrenRenderers (view); + } + + void DisposeModelAndChildrenRenderers (Xamarin.Forms.Element view) + { + IVisualElementRenderer renderer; + foreach (VisualElement child in view.Descendants ()) { + renderer = GetRenderer (child); + child.ClearValue (RendererProperty); + + if (renderer != null) { + //renderer.NativeView.RemoveFromSuperview (); + renderer.Dispose (); + } + } + + renderer = GetRenderer ((VisualElement)view); + if (renderer != null) { + //renderer.NativeView.RemoveFromSuperview (); + renderer.Dispose (); + } + view.ClearValue (RendererProperty); } void AddChild (VisualElement view) diff --git a/Ooui.Forms/PlatformRenderer.cs b/Ooui.Forms/PlatformRenderer.cs index d555803..922d171 100644 --- a/Ooui.Forms/PlatformRenderer.cs +++ b/Ooui.Forms/PlatformRenderer.cs @@ -15,5 +15,19 @@ namespace Ooui.Forms { this.platform = platform; } + + protected override bool TriggerEventFromMessage (Message message) + { + if (message.TargetId == "window" && message.Key == "resize" && message.Value is Newtonsoft.Json.Linq.JObject j) { + var width = (double)j["width"]; + var height = (double)j["height"]; + Platform.Element.Style.Width = width; + Platform.Element.Style.Height = height; + return true; + } + else { + return base.TriggerEventFromMessage (message); + } + } } } diff --git a/Ooui.Forms/Renderers/ButtonRenderer.cs b/Ooui.Forms/Renderers/ButtonRenderer.cs index 727d835..04f30b2 100644 --- a/Ooui.Forms/Renderers/ButtonRenderer.cs +++ b/Ooui.Forms/Renderers/ButtonRenderer.cs @@ -22,7 +22,7 @@ namespace Ooui.Forms.Renderers protected override void Dispose (bool disposing) { if (Control != null) { - Control.Clicked -= OnButtonTouchUpInside; + Control.Click -= OnButtonTouchUpInside; } base.Dispose (disposing); @@ -44,7 +44,7 @@ namespace Ooui.Forms.Renderers _buttonTextColorDefaultHighlighted = Ooui.Colors.Black; _buttonTextColorDefaultDisabled = Ooui.Colors.Black; - Control.Clicked += OnButtonTouchUpInside; + Control.Click += OnButtonTouchUpInside; } UpdateText (); diff --git a/Ooui.Forms/Renderers/DatePickerRenderer.cs b/Ooui.Forms/Renderers/DatePickerRenderer.cs index 79bb422..3df8d05 100644 --- a/Ooui.Forms/Renderers/DatePickerRenderer.cs +++ b/Ooui.Forms/Renderers/DatePickerRenderer.cs @@ -31,8 +31,8 @@ namespace Ooui.Forms.Renderers Type = InputType.Date, }; - entry.Inputted += OnStarted; - entry.Changed += OnEnded; + //entry.Input += OnStarted; + entry.Change += OnEnded; SetNativeControl (entry); } @@ -100,8 +100,8 @@ namespace Ooui.Forms.Renderers if (disposing) { if (Control != null) { - Control.Inputted -= OnStarted; - Control.Changed -= OnEnded; + //Control.Input -= OnStarted; + Control.Change -= OnEnded; } } diff --git a/Ooui.Forms/Renderers/EditorRenderer.cs b/Ooui.Forms/Renderers/EditorRenderer.cs index 880c51e..84a8f60 100644 --- a/Ooui.Forms/Renderers/EditorRenderer.cs +++ b/Ooui.Forms/Renderers/EditorRenderer.cs @@ -10,6 +10,12 @@ namespace Ooui.Forms.Renderers bool _disposed; IEditorController ElementController => Element; + public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) + { + var size = new Size (160, 100); + return new SizeRequest (size, size); + } + protected override void Dispose (bool disposing) { if (_disposed) @@ -19,9 +25,9 @@ namespace Ooui.Forms.Renderers if (disposing) { if (Control != null) { - Control.Changed -= HandleChanged; + Control.Input -= HandleChanged; //Control.Started -= OnStarted; - //Control.Ended -= OnEnded; + Control.Change -= OnEnded; } } @@ -40,9 +46,9 @@ namespace Ooui.Forms.Renderers ClassName = "form-control" }); - Control.Changed += HandleChanged; + Control.Input += HandleChanged; //Control.Started += OnStarted; - //Control.Ended += OnEnded; + Control.Change += OnEnded; } UpdateText (); @@ -75,13 +81,13 @@ namespace Ooui.Forms.Renderers void HandleChanged (object sender, EventArgs e) { - ElementController.SetValueFromRenderer (Editor.TextProperty, Control.Text); + ElementController.SetValueFromRenderer (Editor.TextProperty, Control.Value); } void OnEnded (object sender, EventArgs eventArgs) { - if (Control.Text != Element.Text) - ElementController.SetValueFromRenderer (Editor.TextProperty, Control.Text); + if (Control.Value != Element.Text) + ElementController.SetValueFromRenderer (Editor.TextProperty, Control.Value); Element.SetValue (VisualElement.IsFocusedPropertyKey, false); ElementController.SendCompleted (); @@ -108,8 +114,8 @@ namespace Ooui.Forms.Renderers void UpdateText () { - if (Control.Text != Element.Text) - Control.Text = Element.Text; + if (Control.Value != Element.Text) + Control.Value = Element.Text; } void UpdateTextAlignment () diff --git a/Ooui.Forms/Renderers/EntryRenderer.cs b/Ooui.Forms/Renderers/EntryRenderer.cs index b4e5dfd..4e54dbd 100644 --- a/Ooui.Forms/Renderers/EntryRenderer.cs +++ b/Ooui.Forms/Renderers/EntryRenderer.cs @@ -6,7 +6,7 @@ using Xamarin.Forms; namespace Ooui.Forms.Renderers { - public class EntryRenderer : ViewRenderer + public class EntryRenderer : ViewRenderer { Ooui.Color _defaultTextColor; bool _disposed; @@ -17,7 +17,17 @@ namespace Ooui.Forms.Renderers public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) { - var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); + var text = Element.Text; + if (text == null || text.Length == 0) { + text = Element.Placeholder; + } + Size size; + if (text == null || text.Length == 0) { + size = new Size (Element.FontSize * 0.25, Element.FontSize); + } + else { + size = text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); + } size = new Size (size.Width, size.Height * 1.428 + 14); return new SizeRequest (size, size); } @@ -32,8 +42,8 @@ namespace Ooui.Forms.Renderers if (disposing) { if (Control != null) { //Control.Inputted -= OnEditingBegan; - Control.Inputted -= OnEditingChanged; - Control.Changed -= OnEditingEnded; + Control.Input -= OnEditingChanged; + Control.Change -= OnEditingEnded; } } @@ -48,7 +58,7 @@ namespace Ooui.Forms.Renderers return; if (Control == null) { - var textField = new Ooui.Input (InputType.Text); + var textField = new Ooui.TextInput (); SetNativeControl (textField); Debug.Assert (Control != null, "Control != null"); @@ -57,10 +67,10 @@ namespace Ooui.Forms.Renderers _defaultTextColor = Colors.Black; - textField.Inputted += OnEditingChanged; + textField.Input += OnEditingChanged; //textField.EditingDidBegin += OnEditingBegan; - textField.Changed += OnEditingEnded; + textField.Change += OnEditingEnded; } UpdatePlaceholder (); @@ -113,8 +123,8 @@ namespace Ooui.Forms.Renderers void OnEditingEnded (object sender, EventArgs e) { // Typing aid changes don't always raise EditingChanged event - if (Control.Text != Element.Text) { - ElementController.SetValueFromRenderer (Entry.TextProperty, Control.Text); + if (Control.Value != Element.Text) { + ElementController.SetValueFromRenderer (Entry.TextProperty, Control.Value); } ElementController.SetValueFromRenderer (VisualElement.IsFocusedPropertyKey, false); diff --git a/Ooui.Forms/Renderers/FrameRenderer.cs b/Ooui.Forms/Renderers/FrameRenderer.cs new file mode 100644 index 0000000..752f534 --- /dev/null +++ b/Ooui.Forms/Renderers/FrameRenderer.cs @@ -0,0 +1,64 @@ +using System; +using System.ComponentModel; +using Xamarin.Forms; +using Ooui.Forms.Extensions; + +namespace Ooui.Forms.Renderers +{ + public class FrameRenderer : VisualElementRenderer + { + protected override void OnElementChanged (ElementChangedEventArgs e) + { + base.OnElementChanged (e); + + if (e.NewElement != null) + SetupLayer (); + } + + protected override void OnElementPropertyChanged (object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged (sender, e); + + if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName || + e.PropertyName == Xamarin.Forms.Frame.OutlineColorProperty.PropertyName || + e.PropertyName == Xamarin.Forms.Frame.HasShadowProperty.PropertyName || + e.PropertyName == Xamarin.Forms.Frame.CornerRadiusProperty.PropertyName) + SetupLayer (); + } + + void SetupLayer () + { + float cornerRadius = Element.CornerRadius; + + if (cornerRadius == -1f) + cornerRadius = 5f; // default corner radius + + var Layer = this.Style; + + Layer.BorderRadius = cornerRadius; + + if (Element.BackgroundColor == Xamarin.Forms.Color.Default) + Layer.BackgroundColor = "white"; + else + Layer.BackgroundColor = Element.BackgroundColor.ToOouiColor (); + + if (Element.HasShadow) { + //Layer.ShadowRadius = 5; + //Layer.ShadowColor = "black"; + //Layer.ShadowOpacity = 0.8f; + //Layer.ShadowOffset = new SizeF (); + } + else { + //Layer.ShadowOpacity = 0; + } + + if (Element.OutlineColor == Xamarin.Forms.Color.Default) + Layer.BorderColor = Colors.Clear; + else { + Layer.BorderColor = Element.OutlineColor.ToOouiColor (); + Layer.BorderWidth = 1; + Layer.BorderStyle = "solid"; + } + } + } +} diff --git a/Ooui.Forms/Renderers/ImageRenderer.cs b/Ooui.Forms/Renderers/ImageRenderer.cs new file mode 100644 index 0000000..3f9e6e2 --- /dev/null +++ b/Ooui.Forms/Renderers/ImageRenderer.cs @@ -0,0 +1,202 @@ +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Ooui.Forms.Renderers +{ + public class ImageRenderer : ViewRenderer + { + bool _isDisposed; + + protected override void Dispose (bool disposing) + { + if (_isDisposed) + return; + + if (disposing) { + } + + _isDisposed = true; + + base.Dispose (disposing); + } + + protected override async void OnElementChanged (ElementChangedEventArgs e) + { + if (Control == null) { + var imageView = new Ooui.Image (); + SetNativeControl (imageView); + this.Style.Overflow = "hidden"; + } + + if (e.NewElement != null) { + SetAspect (); + await TrySetImage (e.OldElement); + SetOpacity (); + } + + base.OnElementChanged (e); + } + + protected override async void OnElementPropertyChanged (object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged (sender, e); + if (e.PropertyName == Xamarin.Forms.Image.SourceProperty.PropertyName) + await TrySetImage (); + else if (e.PropertyName == Xamarin.Forms.Image.IsOpaqueProperty.PropertyName) + SetOpacity (); + else if (e.PropertyName == Xamarin.Forms.Image.AspectProperty.PropertyName) + SetAspect (); + } + + void SetAspect () + { + if (_isDisposed || Element == null || Control == null) { + return; + } + } + + protected virtual async Task TrySetImage (Xamarin.Forms.Image previous = null) + { + // By default we'll just catch and log any exceptions thrown by SetImage so they don't bring down + // the application; a custom renderer can override this method and handle exceptions from + // SetImage differently if it wants to + + try { + await SetImage (previous).ConfigureAwait (false); + } + catch (Exception ex) { + System.Diagnostics.Debug.WriteLine ("Error loading image: {0}", ex); + } + finally { + ((IImageController)Element)?.SetIsLoading (false); + } + } + + protected async Task SetImage (Xamarin.Forms.Image oldElement = null) + { + if (_isDisposed || Element == null || Control == null) { + return; + } + + var source = Element.Source; + + if (oldElement != null) { + var oldSource = oldElement.Source; + if (Equals (oldSource, source)) + return; + + if (oldSource is FileImageSource && source is FileImageSource && ((FileImageSource)oldSource).File == ((FileImageSource)source).File) + return; + + Control.Source = ""; + } + + IImageSourceHandler handler; + + Element.SetIsLoading (true); + + if (source != null && + (handler = Xamarin.Forms.Internals.Registrar.Registered.GetHandler (source.GetType ())) != null) { + string uiimage; + try { + uiimage = await handler.LoadImageAsync (source, scale: 1.0f); + } + catch (OperationCanceledException) { + uiimage = null; + } + + if (_isDisposed) + return; + + var imageView = Control; + if (imageView != null) + imageView.Source = uiimage; + + ((IVisualElementController)Element).NativeSizeChanged (); + } + else { + Control.Source = ""; + } + + Element.SetIsLoading (false); + } + + void SetOpacity () + { + if (_isDisposed || Element == null || Control == null) { + return; + } + } + } + + public interface IImageSourceHandler : IRegisterable + { + Task LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1); + } + + public sealed class FileImageSourceHandler : IImageSourceHandler + { + public async Task LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f) + { + string image = null; + var filesource = imagesource as FileImageSource; + var file = filesource?.File; + if (!string.IsNullOrEmpty (file)) { + var name = System.IO.Path.GetFileName (file); + image = "/images/" + name; + if (Ooui.UI.TryGetFileContentAtPath (image, out var f)) { + // Already published + } + else { + await Task.Run (() => Ooui.UI.PublishFile (image, file), cancelationToken); + } + } + return image; + } + } + + public sealed class StreamImagesourceHandler : IImageSourceHandler + { + public async Task LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f) + { + string image = null; + var streamsource = imagesource as StreamImageSource; + if (streamsource?.Stream != null) { + using (var streamImage = await ((IStreamImageSource)streamsource).GetStreamAsync (cancelationToken).ConfigureAwait (false)) { + if (streamImage != null) { + var data = new byte[streamImage.Length]; + using (var outputStream = new System.IO.MemoryStream (data)) { + await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false); + } + var hash = Ooui.UI.Hash (data); + var etag = "\"" + hash + "\""; + image = "/images/" + hash; + if (Ooui.UI.TryGetFileContentAtPath (image, out var file) && file.Etag == etag) { + // Already published + } + else { + Ooui.UI.PublishFile (image, data, etag, "image"); + } + } + } + } + + if (image == null) { + System.Diagnostics.Debug.WriteLine ("Could not load image: {0}", streamsource); + } + return image; + } + } + + public sealed class ImageLoaderSourceHandler : IImageSourceHandler + { + public Task LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f) + { + var imageLoader = imagesource as UriImageSource; + return Task.FromResult (imageLoader?.Uri.ToString () ?? ""); + } + } +} diff --git a/Ooui.Forms/Renderers/LabelRenderer.cs b/Ooui.Forms/Renderers/LabelRenderer.cs index 6a6104d..85ddc50 100644 --- a/Ooui.Forms/Renderers/LabelRenderer.cs +++ b/Ooui.Forms/Renderers/LabelRenderer.cs @@ -18,6 +18,8 @@ namespace Ooui.Forms.Renderers { if (!_perfectSizeValid) { var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); + size.Width = Math.Ceiling (size.Width); + size.Height = Math.Ceiling (size.Height * 1.4); _perfectSize = new SizeRequest (size, size); _perfectSizeValid = true; } @@ -72,6 +74,9 @@ namespace Ooui.Forms.Renderers { base.OnElementPropertyChanged (sender, e); + if (Control == null) + return; + if (e.PropertyName == Xamarin.Forms.Label.HorizontalTextAlignmentProperty.PropertyName) UpdateAlignment (); else if (e.PropertyName == Xamarin.Forms.Label.VerticalTextAlignmentProperty.PropertyName) @@ -98,8 +103,10 @@ namespace Ooui.Forms.Renderers void UpdateAlignment () { - Control.Style.TextAlign = Element.HorizontalTextAlignment.ToOouiTextAlign (); - Control.Style.VerticalAlign = Element.VerticalTextAlignment.ToOouiTextAlign (); + this.Style.Display = "table"; + Control.Style.Display = "table-cell"; + this.Style.TextAlign = Element.HorizontalTextAlignment.ToOouiTextAlign (); + Control.Style.VerticalAlign = Element.VerticalTextAlignment.ToOouiVerticalAlign (); } void UpdateLineBreakMode () diff --git a/Ooui.Forms/Renderers/SwitchRenderer.cs b/Ooui.Forms/Renderers/SwitchRenderer.cs new file mode 100644 index 0000000..737eb50 --- /dev/null +++ b/Ooui.Forms/Renderers/SwitchRenderer.cs @@ -0,0 +1,53 @@ +using System; +using Xamarin.Forms; + +namespace Ooui.Forms.Renderers +{ + public class SwitchRenderer : ViewRenderer + { + public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) + { + var size = new Size (54, 38); + return new SizeRequest (size, size); + } + + protected override void Dispose (bool disposing) + { + if (disposing) + Control.Change -= OnControlValueChanged; + + base.Dispose (disposing); + } + + protected override void OnElementChanged (ElementChangedEventArgs e) + { + if (e.OldElement != null) + e.OldElement.Toggled -= OnElementToggled; + + if (e.NewElement != null) { + if (Control == null) { + var input = new Input (InputType.Checkbox); + input.SetAttribute ("data-toggle", "toggle"); + SetNativeControl (input); + input.Call ("$.bootstrapToggle"); + Control.Change += OnControlValueChanged; + } + + Control.IsChecked = Element.IsToggled; + e.NewElement.Toggled += OnElementToggled; + } + + base.OnElementChanged (e); + } + + void OnControlValueChanged (object sender, EventArgs e) + { + ((IElementController)Element).SetValueFromRenderer (Switch.IsToggledProperty, Control.IsChecked); + } + + void OnElementToggled (object sender, EventArgs e) + { + Control.IsChecked = Element.IsToggled; + } + } +} diff --git a/Ooui.Forms/VisualElementRenderer.cs b/Ooui.Forms/VisualElementRenderer.cs index a9f9af1..f6f4548 100644 --- a/Ooui.Forms/VisualElementRenderer.cs +++ b/Ooui.Forms/VisualElementRenderer.cs @@ -38,6 +38,7 @@ namespace Ooui.Forms VisualElementRendererFlags _flags = VisualElementRendererFlags.AutoPackage | VisualElementRendererFlags.AutoTrack; + EventTracker _events; VisualElementPackager _packager; VisualElementTracker _tracker; @@ -107,10 +108,10 @@ namespace Ooui.Forms _packager.Load (); } - //if (AutoTrack && _events == null) { - // _events = new EventTracker (this); - // _events.LoadEvents (this); - //} + if (AutoTrack && _events == null) { + _events = new EventTracker (this); + _events.LoadEvents (this); + } element.PropertyChanged += _propertyChangedHandler; } diff --git a/Ooui/CanvasRenderingContext2D.cs b/Ooui/CanvasRenderingContext2D.cs index 210017d..64d80f8 100644 --- a/Ooui/CanvasRenderingContext2D.cs +++ b/Ooui/CanvasRenderingContext2D.cs @@ -81,97 +81,97 @@ namespace Ooui public void Save () { - SendCall ("save"); + Call ("save"); } public void Restore () { - SendCall ("restore"); + Call ("restore"); } public void ClearRect (double x, double y, double w, double h) { - SendCall ("clearRect", x, y, w, h); + Call ("clearRect", x, y, w, h); } public void FillRect (double x, double y, double w, double h) { - SendCall ("fillRect", x, y, w, h); + Call ("fillRect", x, y, w, h); } public void StrokeRect (double x, double y, double w, double h) { - SendCall ("strokeRect", x, y, w, h); + Call ("strokeRect", x, y, w, h); } public void BeginPath () { - SendCall ("beginPath"); + Call ("beginPath"); } public void ClosePath () { - SendCall ("closePath"); + Call ("closePath"); } public void MoveTo (double x, double y) { - SendCall ("moveTo", x, y); + Call ("moveTo", x, y); } public void LineTo (double x, double y) { - SendCall ("lineTo", x, y); + Call ("lineTo", x, y); } public void QuadraticCurveTo (double cpx, double cpy, double x, double y) { - SendCall ("quadraticCurveTo", cpx, cpy, x, y); + Call ("quadraticCurveTo", cpx, cpy, x, y); } public void BezierCurveTo (double cp1x, double cp1y, double cp2x, double cp2y, double x, double y) { - SendCall ("bezierCurveTo", cp1x, cp1y, cp2x, cp2y, x, y); + Call ("bezierCurveTo", cp1x, cp1y, cp2x, cp2y, x, y); } public void ArcTo (double x1, double y1, double x2, double y2, double radius) { - SendCall ("arcTo", x1, y1, x2, y2, radius); + Call ("arcTo", x1, y1, x2, y2, radius); } public void Rect (double x, double y, double w, double h) { - SendCall ("rect", x, y, w, h); + Call ("rect", x, y, w, h); } public void Arc (double x, double y, double radius, double startAngle, double endAngle, bool counterclockwise) { - SendCall ("arc", x, y, radius, startAngle, endAngle, counterclockwise); + Call ("arc", x, y, radius, startAngle, endAngle, counterclockwise); } public void Fill () { - SendCall ("fill"); + Call ("fill"); } public void Stroke () { - SendCall ("stroke"); + Call ("stroke"); } public void Clip () { - SendCall ("clip"); + Call ("clip"); } public void FillText (string text, double x, double y, double? maxWidth) { - SendCall ("fillText", text, x, y, maxWidth); + Call ("fillText", text, x, y, maxWidth); } public void StrokeText (string text, double x, double y, double? maxWidth) { - SendCall ("strokeText", text, x, y, maxWidth); + Call ("strokeText", text, x, y, maxWidth); } } diff --git a/Ooui/Client.js b/Ooui/Client.js index e796069..f77d6e1 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -57,7 +57,8 @@ function ooui (rootElementPath) { socket.addEventListener ("close", function (event) { console.error ("Web socket close", event); if (opened) { - location.reload (); + alert ("Connection to the server has been lost. Please try refreshing the page."); + opened = false; } }); @@ -65,11 +66,19 @@ function ooui (rootElementPath) { const messages = JSON.parse (event.data); if (debug) console.log("Messages", messages); if (Array.isArray (messages)) { + const jqs = [] messages.forEach (function (m) { // console.log('Raw value from server', m.v); m.v = fixupValue (m.v); - processMessage (m); + if (m.k.startsWith ("$.")) { + jqs.push (m); + } + else { + processMessage (m); + } }); + // Run jQuery functions last since they usually require a fully built DOM + jqs.forEach (processMessage); } }); @@ -92,8 +101,8 @@ function ooui (rootElementPath) { function resizeHandler() { const em = { m: "event", - id: 42, - k: "window.resize", + id: "window", + k: "resize", v: getSize (), }; const ems = JSON.stringify (em); @@ -163,9 +172,11 @@ function msgCall (m) { console.error ("Unknown node id", m); return; } - const f = node[m.k]; + const isJQuery = m.k.startsWith ("$."); + const target = isJQuery ? $(node) : node; + const f = isJQuery ? target[m.k.slice(2)] : target[m.k]; if (debug) console.log ("Call", node, f, m.v); - const r = f.apply (node, m.v); + const r = f.apply (target, m.v); if (typeof m.rid === 'string' || m.rid instanceof String) { nodes[m.rid] = r; } diff --git a/Ooui/Div.cs b/Ooui/Div.cs index 02012c5..d4d8009 100644 --- a/Ooui/Div.cs +++ b/Ooui/Div.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Ooui { @@ -8,5 +9,21 @@ namespace Ooui : base ("div") { } + + public Div (params Element[] children) + : this () + { + foreach (var c in children) { + AppendChild (c); + } + } + + public Div (IEnumerable children) + : this () + { + foreach (var c in children) { + AppendChild (c); + } + } } } diff --git a/Ooui/Element.cs b/Ooui/Element.cs index a7287d1..bc08de7 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -25,12 +25,12 @@ namespace Ooui set => SetProperty (ref hidden, value, "hidden"); } - public event TargetEventHandler Clicked { + public event TargetEventHandler Click { add => AddEventListener ("click", value); remove => RemoveEventListener ("click", value); } - public event TargetEventHandler DoubleClicked { + public event TargetEventHandler DoubleClick { add => AddEventListener ("dblclick", value); remove => RemoveEventListener ("dblclick", value); } @@ -40,7 +40,7 @@ namespace Ooui remove => RemoveEventListener ("keydown", value); } - public event TargetEventHandler KeyPressed { + public event TargetEventHandler KeyPress { add => AddEventListener ("keypress", value); remove => RemoveEventListener ("keypress", value); } @@ -55,17 +55,17 @@ namespace Ooui remove => RemoveEventListener ("mousedown", value); } - public event TargetEventHandler MouseEntered { + public event TargetEventHandler MouseEnter { add => AddEventListener ("mouseenter", value); remove => RemoveEventListener ("mouseenter", value); } - public event TargetEventHandler MouseLeft { + public event TargetEventHandler MouseLeave { add => AddEventListener ("mouseleave", value); remove => RemoveEventListener ("mouseleave", value); } - public event TargetEventHandler MouseMoved { + public event TargetEventHandler MouseMove { add => AddEventListener ("mousemove", value); remove => RemoveEventListener ("mousemove", value); } @@ -85,7 +85,7 @@ namespace Ooui remove => RemoveEventListener ("mouseup", value); } - public event TargetEventHandler Wheeled { + public event TargetEventHandler Wheel { add => AddEventListener ("wheel", value); remove => RemoveEventListener ("wheel", value); } @@ -116,5 +116,19 @@ namespace Ooui { SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]); } + + protected override bool SaveStateMessageIfNeeded (Message message) + { + if (message.TargetId != Id) + return false; + + switch (message.MessageType) { + case MessageType.Call when message.Key.StartsWith ("$.", StringComparison.Ordinal): + AddStateMessage (message); + return true; + default: + return base.SaveStateMessageIfNeeded (message); + } + } } } diff --git a/Ooui/EventTarget.cs b/Ooui/EventTarget.cs index a7be894..e3e26c1 100644 --- a/Ooui/EventTarget.cs +++ b/Ooui/EventTarget.cs @@ -115,7 +115,7 @@ namespace Ooui MessageSent?.Invoke (message); } - protected void SendCall (string methodName, params object[] args) + public void Call (string methodName, params object[] args) { Send (Message.Call (Id, methodName, args)); } diff --git a/Ooui/Form.cs b/Ooui/Form.cs index e23e22a..809397a 100644 --- a/Ooui/Form.cs +++ b/Ooui/Form.cs @@ -22,7 +22,7 @@ namespace Ooui set => SetProperty (ref enctype, value ?? "", "enctype"); } - public event TargetEventHandler Submitted { + public event TargetEventHandler Submit { add => AddEventListener ("submit", value); remove => RemoveEventListener ("submit", value); } diff --git a/Ooui/Input.cs b/Ooui/Input.cs index 9a2d2f2..90f73d8 100644 --- a/Ooui/Input.cs +++ b/Ooui/Input.cs @@ -30,16 +30,11 @@ namespace Ooui } } - public event TargetEventHandler Changed { + public event TargetEventHandler Change { add => AddEventListener ("change", value); remove => RemoveEventListener ("change", value); } - public event TargetEventHandler Inputted { - add => AddEventListener ("input", value); - remove => RemoveEventListener ("input", value); - } - string placeholder = ""; public string Placeholder { get => placeholder; @@ -77,7 +72,7 @@ namespace Ooui : base ("input") { // Subscribe to the change event so we always get up-to-date values - Changed += (s, e) => {}; + Change += (s, e) => {}; } public Input (InputType type) @@ -88,7 +83,7 @@ namespace Ooui protected override bool TriggerEventFromMessage (Message message) { - if (message.TargetId == Id && message.MessageType == MessageType.Event && message.Key == "change") { + if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { // Don't need to notify here because the base implementation will fire the event if (Type == InputType.Checkbox) { isChecked = message.Value != null ? Convert.ToBoolean (message.Value) : false; diff --git a/Ooui/Node.cs b/Ooui/Node.cs index 5a80ca1..18d16a0 100644 --- a/Ooui/Node.cs +++ b/Ooui/Node.cs @@ -73,7 +73,8 @@ namespace Ooui } } newChild.MessageSent += HandleChildMessageSent; - SendCall ("insertBefore", newChild, referenceChild); + Call ("insertBefore", newChild, referenceChild); + OnChildInsertedBefore (newChild, referenceChild); return newChild; } @@ -87,10 +88,19 @@ namespace Ooui } } child.MessageSent -= HandleChildMessageSent; - SendCall ("removeChild", child); + Call ("removeChild", child); + OnChildRemoved (child); return child; } + protected virtual void OnChildInsertedBefore (Node newChild, Node referenceChild) + { + } + + protected virtual void OnChildRemoved (Node child) + { + } + protected void ReplaceAll (Node newNode) { var toRemove = new List (); @@ -100,7 +110,7 @@ namespace Ooui } foreach (var child in toRemove) { child.MessageSent -= HandleChildMessageSent; - SendCall ("removeChild", child); + Call ("removeChild", child); } InsertBefore (newNode, null); } diff --git a/Ooui/Ooui.csproj b/Ooui/Ooui.csproj index 3b692d5..98357fe 100644 --- a/Ooui/Ooui.csproj +++ b/Ooui/Ooui.csproj @@ -1,9 +1,9 @@  - 0.2.0 + 1.0.0 praeclarum Small cross-platform UI library for .NET that uses web technologies. - UI;CrossPlatform + Ooui;UI;CrossPlatform https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png https://github.com/praeclarum/Ooui https://github.com/praeclarum/Ooui/blob/master/LICENSE diff --git a/Ooui/Option.cs b/Ooui/Option.cs new file mode 100644 index 0000000..86d918e --- /dev/null +++ b/Ooui/Option.cs @@ -0,0 +1,30 @@ +using System; + +namespace Ooui +{ + public class Option : Element + { + string val = ""; + public string Value { + get => val; + set => SetProperty (ref val, value ?? "", "value"); + } + + string label = ""; + public string Label { + get => label; + set => SetProperty (ref label, value ?? "", "label"); + } + + bool defaultSelected = false; + public bool DefaultSelected { + get => defaultSelected; + set => SetProperty (ref defaultSelected, value, "defaultSelected"); + } + + public Option () + : base ("option") + { + } + } +} diff --git a/Ooui/Select.cs b/Ooui/Select.cs index 7f53175..c88eed4 100644 --- a/Ooui/Select.cs +++ b/Ooui/Select.cs @@ -10,14 +10,42 @@ namespace Ooui set => SetProperty (ref val, value ?? "", "value"); } - public event TargetEventHandler Changed { + public event TargetEventHandler Change { add => AddEventListener ("change", value); remove => RemoveEventListener ("change", value); } + public event TargetEventHandler Input { + add => AddEventListener ("input", value); + remove => RemoveEventListener ("input", value); + } + public Select () : base ("select") { + // Subscribe to the change event so we always get up-to-date values + Change += (s, e) => { }; + } + + public void AddOption (string label, string value) + { + AppendChild (new Option { Label = label, Value = value }); + } + + protected override void OnChildInsertedBefore (Node newChild, Node referenceChild) + { + base.OnChildInsertedBefore (newChild, referenceChild); + if (string.IsNullOrEmpty (val) && newChild is Option o && !string.IsNullOrEmpty (o.Value)) { + val = o.Value; + } + } + + protected override bool TriggerEventFromMessage (Message message) + { + if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { + val = message.Value != null ? Convert.ToString (message.Value) : ""; + } + return base.TriggerEventFromMessage (message); } } } diff --git a/Ooui/Style.cs b/Ooui/Style.cs index 06b6149..6a6f307 100644 --- a/Ooui/Style.cs +++ b/Ooui/Style.cs @@ -93,38 +93,38 @@ namespace Ooui public Value BorderTopWidth { get => this["border-top-width"]; - set => this["border-top-width"] = value; + set => this["border-top-width"] = AddNumberUnits (value, "px"); } public Value BorderRightWidth { get => this["border-right-width"]; - set => this["border-right-width"] = value; + set => this["border-right-width"] = AddNumberUnits (value, "px"); } public Value BorderBottomWidth { get => this["border-bottom-width"]; - set => this["border-bottom-width"] = value; + set => this["border-bottom-width"] = AddNumberUnits (value, "px"); } public Value BorderLeftWidth { get => this["border-left-width"]; - set => this["border-left-width"] = value; + set => this["border-left-width"] = AddNumberUnits (value, "px"); } public Value BorderRadius { get => this["border-radius"]; set { - this["border-radius"] = value; + this["border-radius"] = AddNumberUnits (value, "px"); } } public Value BorderWidth { get => this["border-top-width"]; set { - this["border-top-width"] = value; - this["border-right-width"] = value; - this["border-bottom-width"] = value; - this["border-left-width"] = value; + this["border-top-width"] = AddNumberUnits (value, "px"); + this["border-right-width"] = AddNumberUnits (value, "px"); + this["border-bottom-width"] = AddNumberUnits (value, "px"); + this["border-left-width"] = AddNumberUnits (value, "px"); } } @@ -253,6 +253,11 @@ namespace Ooui set => this["order"] = value; } + public Value Overflow { + get => this["overflow"]; + set => this["overflow"] = value; + } + public Value PaddingTop { get => this["padding-top"]; set => this["padding-top"] = value; @@ -403,6 +408,8 @@ namespace Ooui static string AddNumberUnits (object val, string units) { + if (val == null) + return null; if (val is string s) return s; if (val is IConvertible c) diff --git a/Ooui/TextArea.cs b/Ooui/TextArea.cs index 94ca6fe..a35b962 100644 --- a/Ooui/TextArea.cs +++ b/Ooui/TextArea.cs @@ -4,12 +4,12 @@ namespace Ooui { public class TextArea : FormControl { - public event TargetEventHandler Changed { + public event TargetEventHandler Change { add => AddEventListener ("change", value); remove => RemoveEventListener ("change", value); } - public event TargetEventHandler Inputted { + public event TargetEventHandler Input { add => AddEventListener ("input", value); remove => RemoveEventListener ("input", value); } @@ -36,18 +36,18 @@ namespace Ooui : base ("textarea") { // Subscribe to the change event so we always get up-to-date values - Changed += (s, e) => {}; + Change += (s, e) => {}; } public TextArea (string text) : this () { - Text = text; + Value = text; } protected override bool TriggerEventFromMessage (Message message) { - if (message.TargetId == Id && message.MessageType == MessageType.Event && message.Key == "change") { + if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { // Don't need to notify here because the base implementation will fire the event val = message.Value != null ? Convert.ToString (message.Value) : ""; } diff --git a/Ooui/TextInput.cs b/Ooui/TextInput.cs new file mode 100644 index 0000000..a8fd587 --- /dev/null +++ b/Ooui/TextInput.cs @@ -0,0 +1,17 @@ +using System; + +namespace Ooui +{ + public class TextInput : Input + { + public event TargetEventHandler Input { + add => AddEventListener ("input", value); + remove => RemoveEventListener ("input", value); + } + + public TextInput () + : base (InputType.Text) + { + } + } +} diff --git a/Ooui/UI.cs b/Ooui/UI.cs index b3a3ac9..33c39ac 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -14,6 +14,9 @@ namespace Ooui { static readonly ManualResetEvent started = new ManualResetEvent (false); + [ThreadStatic] + static System.Security.Cryptography.SHA256 sha256; + static CancellationTokenSource serverCts; static readonly Dictionary publishedPaths = @@ -26,8 +29,10 @@ namespace Ooui public static StyleSelectors Styles => rules; static readonly byte[] clientJsBytes; + static readonly string clientJsEtag; public static byte[] ClientJsBytes => clientJsBytes; + public static string ClientJsEtag => clientJsEtag; public static string Template { get; set; } = $@" @@ -35,10 +40,14 @@ namespace Ooui @Title +
+ + + @@ -64,6 +73,19 @@ namespace Ooui } } } + static bool serverEnabled = true; + public static bool ServerEnabled { + get => serverEnabled; + set { + if (serverEnabled != value) { + serverEnabled = value; + if (serverEnabled) + Restart (); + else + Stop (); + } + } + } static UI () { @@ -79,6 +101,22 @@ 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 (); } static void Publish (string path, RequestHandler handler) @@ -110,7 +148,47 @@ namespace Ooui if (contentType == null) { contentType = GuessContentType (path, filePath); } - Publish (path, new DataHandler (data, contentType)); + var etag = "\"" + Hash (data) + "\""; + Publish (path, new DataHandler (data, etag, contentType)); + } + + public static void PublishFile (string path, byte[] data, string contentType) + { + var etag = "\"" + Hash (data) + "\""; + Publish (path, new DataHandler (data, etag, contentType)); + } + + public static void PublishFile (string path, byte[] data, string etag, string contentType) + { + Publish (path, new DataHandler (data, etag, contentType)); + } + + public static bool TryGetFileContentAtPath (string path, out FileContent file) + { + RequestHandler handler; + lock (publishedPaths) { + if (!publishedPaths.TryGetValue (path, out handler)) { + file = null; + return false; + } + } + if (handler is DataHandler dh) { + file = new FileContent { + Etag = dh.Etag, + Content = dh.Data, + ContentType = dh.ContentType, + }; + return true; + } + file = null; + return false; + } + + public class FileContent + { + public string ContentType { get; set; } + public string Etag { get; set; } + public byte[] Content { get; set; } } public static void PublishJson (string path, Func ctor) @@ -121,7 +199,8 @@ namespace Ooui public static void PublishJson (string path, object value) { var data = JsonHandler.GetData (value); - Publish (path, new DataHandler (data, JsonHandler.ContentType)); + var etag = "\"" + Hash (data) + "\""; + Publish (path, new DataHandler (data, etag, JsonHandler.ContentType)); } public static void PublishCustomResponse (string path, Action responder) @@ -153,6 +232,7 @@ namespace Ooui static void Start () { + if (!serverEnabled) return; if (serverCts != null) return; serverCts = new CancellationTokenSource (); var token = serverCts.Token; @@ -232,12 +312,22 @@ namespace Ooui var response = listenerContext.Response; if (path == "/ooui.js") { - response.ContentLength64 = clientJsBytes.LongLength; - response.ContentType = "application/javascript"; - response.ContentEncoding = Encoding.UTF8; - response.AddHeader ("Cache-Control", "public, max-age=3600"); - using (var s = response.OutputStream) { - s.Write (clientJsBytes, 0, clientJsBytes.Length); + var inm = listenerContext.Request.Headers.Get ("If-None-Match"); + if (string.IsNullOrEmpty (inm) || inm != clientJsEtag) { + response.StatusCode = 200; + response.ContentLength64 = clientJsBytes.LongLength; + response.ContentType = "application/javascript"; + response.ContentEncoding = Encoding.UTF8; + response.AddHeader ("Cache-Control", "public, max-age=60"); + response.AddHeader ("Etag", clientJsEtag); + using (var s = response.OutputStream) { + s.Write (clientJsBytes, 0, clientJsBytes.Length); + } + response.Close (); + } + else { + response.StatusCode = 304; + response.Close (); } } else { @@ -308,11 +398,17 @@ namespace Ooui class DataHandler : RequestHandler { readonly byte[] data; + readonly string etag; readonly string contentType; - public DataHandler (byte[] data, string contentType = null) + public byte[] Data => data; + public string Etag => etag; + public string ContentType => contentType; + + public DataHandler (byte[] data, string etag, string contentType = null) { this.data = data; + this.etag = etag; this.contentType = contentType; } @@ -322,13 +418,20 @@ namespace Ooui var path = url.LocalPath; var response = listenerContext.Response; - response.StatusCode = 200; - if (!string.IsNullOrEmpty (contentType)) - response.ContentType = contentType; - response.ContentLength64 = data.LongLength; + var inm = listenerContext.Request.Headers.Get ("If-None-Match"); + if (!string.IsNullOrEmpty (inm) && inm == etag) { + response.StatusCode = 304; + } + else { + response.StatusCode = 200; + response.AddHeader ("Etag", etag); + if (!string.IsNullOrEmpty (contentType)) + response.ContentType = contentType; + response.ContentLength64 = data.LongLength; - using (var s = response.OutputStream) { - s.Write (data, 0, data.Length); + using (var s = response.OutputStream) { + s.Write (data, 0, data.Length); + } } response.Close (); } @@ -432,11 +535,32 @@ namespace Ooui return; } + // + // Set the element's dimensions + // + var query = + (from part in listenerContext.Request.Url.Query.Split (new[] { '?', '&' }) + where part.Length > 0 + let kvs = part.Split ('=') + where kvs.Length == 2 + select kvs).ToDictionary (x => Uri.UnescapeDataString (x[0]), x => Uri.UnescapeDataString (x[1])); + if (!query.TryGetValue ("w", out var wValue) || string.IsNullOrEmpty (wValue)) { + wValue = "640"; + } + if (!query.TryGetValue ("h", out var hValue) || string.IsNullOrEmpty (hValue)) { + hValue = "480"; + } + var icult = System.Globalization.CultureInfo.InvariantCulture; + if (!double.TryParse (wValue, System.Globalization.NumberStyles.Any, icult, out var w)) + w = 640; + if (!double.TryParse (hValue, System.Globalization.NumberStyles.Any, icult, out var h)) + h = 480; + // // Create a new session and let it handle everything from here // try { - var session = new Session (webSocket, element, 1024, 768, serverToken); + var session = new Session (webSocket, element, w, h, serverToken); await session.RunAsync ().ConfigureAwait (false); } catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { @@ -470,9 +594,11 @@ namespace Ooui 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 / 30); // 30 FPS max + readonly TimeSpan throttleInterval = TimeSpan.FromSeconds (1.0 / MaxFps); readonly double initialWidth; readonly double initialHeight; @@ -619,6 +745,7 @@ namespace Ooui // // Add it to the queue // + //Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}"); queuedMessages.Add (message); } @@ -637,19 +764,24 @@ namespace Ooui // 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 - // - var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend); - var outputBuffer = new ArraySegment (Encoding.UTF8.GetBytes (json)); - await webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false); + 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); diff --git a/PlatformSamples/AspNetCoreMvc/Controllers/SamplesController.cs b/PlatformSamples/AspNetCoreMvc/Controllers/SamplesController.cs index cf4aa7a..fa0d1a3 100644 --- a/PlatformSamples/AspNetCoreMvc/Controllers/SamplesController.cs +++ b/PlatformSamples/AspNetCoreMvc/Controllers/SamplesController.cs @@ -21,7 +21,7 @@ namespace AspNetCoreMvc.Controllers var head = new Heading { Text = "Click away!" }; var label = new Label { Text = "0" }; var btn = new Button { Text = "Increase" }; - btn.Clicked += (sender, e) => { + btn.Click += (sender, e) => { count++; label.Text = count.ToString (); }; diff --git a/README.md b/README.md index 31e7782..7dbe022 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ -# Ooui +# Ooui Web Framework [![Build Status](https://www.bitrise.io/app/86585e168136767d/status.svg?token=G9Svvnv_NvG40gcqu48RNQ)](https://www.bitrise.io/app/86585e168136767d) -[![Build Status](https://www.bitrise.io/app/86585e168136767d/status.svg?token=G9Svvnv_NvG40gcqu48RNQ)](https://www.bitrise.io/app/86585e168136767d) [![NuGet Package](https://img.shields.io/nuget/v/Ooui.svg)](https://www.nuget.org/packages/Ooui) +| Version | Package | Description | +| ------- | ------- | ----------- | +| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.svg)](https://www.nuget.org/packages/Ooui) | [Ooui](https://www.nuget.org/packages/Ooui) | Core library with HTML elements and a server. | +| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.Forms.svg)](https://www.nuget.org/packages/Ooui.Forms) | [Ooui.Forms](https://www.nuget.org/packages/Ooui.Forms) | Xamarin.Forms backend using Ooui | +| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.AspNetCore.svg)](https://www.nuget.org/packages/Ooui.AspNetCore) | [Ooui.AspNetCore](https://www.nuget.org/packages/Ooui.AspNetCore) | Integration with ASP.NET Core MVC | Ooui (pronounced *weeee!*) is a small cross-platform UI library for .NET that uses web technologies. @@ -45,7 +49,7 @@ class Program // Add some logic to it var count = 0; - button.Clicked += (s, e) => { + button.Click += (s, e) => { count++; button.Text = $"Clicked {count} times"; }; @@ -77,7 +81,7 @@ Button MakeButton() { var button = new Button("Click me!"); var count = 0; - button.Clicked += (s, e) => { + button.Click += (s, e) => { count++; button.Text = $"Clicked {count} times"; }; diff --git a/Samples/BugSweeper/App.cs b/Samples/BugSweeper/App.cs new file mode 100755 index 0000000..bc51379 --- /dev/null +++ b/Samples/BugSweeper/App.cs @@ -0,0 +1,13 @@ +using System; +using Xamarin.Forms; + +namespace BugSweeper +{ + public class App : Application + { + public App () + { + MainPage = new BugSweeperPage(); + } + } +} diff --git a/Samples/BugSweeper/Board.cs b/Samples/BugSweeper/Board.cs new file mode 100755 index 0000000..33b3950 --- /dev/null +++ b/Samples/BugSweeper/Board.cs @@ -0,0 +1,235 @@ +using System; +using Xamarin.Forms; + +namespace BugSweeper +{ + class Board : AbsoluteLayout + { + // Alternative sizes make the tiles a tad small. + const int COLS = 9; // 16 + const int ROWS = 9; // 16 + const int BUGS = 10; // 40 + + Tile[,] tiles = new Tile[ROWS, COLS]; + int flaggedTileCount; + bool isGameInProgress; // on first tap + bool isGameInitialized; // on first double-tap + bool isGameEnded; + + // Events to notify page. + public event EventHandler GameStarted; + public event EventHandler GameEnded; + + public Board() + { + for (int row = 0; row < ROWS; row++) + for (int col = 0; col < COLS; col++) + { + Tile tile = new Tile(row, col); + tile.TileStatusChanged += OnTileStatusChanged; + this.Children.Add(tile); + tiles[row, col] = tile; + } + + SizeChanged += (sender, args) => + { + double tileWidth = this.Width / COLS; + double tileHeight = this.Height / ROWS; + + foreach (Tile tile in tiles) + { + Rectangle bounds = new Rectangle(tile.Col * tileWidth, + tile.Row * tileHeight, + tileWidth, tileHeight); + AbsoluteLayout.SetLayoutBounds(tile, bounds); + } + }; + + NewGameInitialize(); + } + + public void NewGameInitialize() + { + // Clear all the tiles. + foreach (Tile tile in tiles) + tile.Initialize(); + + isGameInProgress = false; + isGameInitialized = false; + isGameEnded = false; + this.FlaggedTileCount = 0; + } + + public int FlaggedTileCount + { + set + { + if (flaggedTileCount != value) + { + flaggedTileCount = value; + OnPropertyChanged(); + } + } + get + { + return flaggedTileCount; + } + } + + public int BugCount + { + get + { + return BUGS; + } + } + + + // Not called until the first tile is double-tapped. + void DefineNewBoard(int tappedRow, int tappedCol) + { + // Begin the assignment of bugs. + Random random = new Random(); + int bugCount = 0; + + while (bugCount < BUGS) + { + // Get random row and column. + int row = random.Next(ROWS); + int col = random.Next(COLS); + + // Skip it if it's already a bug. + if (tiles[row, col].IsBug) + { + continue; + } + + // Avoid the tappedRow & Col & surrounding ones. + if (row >= tappedRow - 1 && + row <= tappedRow + 1 && + col >= tappedCol - 1 && + col <= tappedCol + 1) + { + continue; + } + + // It's a bug! + tiles[row, col].IsBug = true; + + // Calculate the surrounding bug count. + CycleThroughNeighbors(row, col, + (neighborRow, neighborCol) => + { + ++tiles[neighborRow, neighborCol].SurroundingBugCount; + }); + + bugCount++; + } + } + + void CycleThroughNeighbors(int row, int col, Action callback) + { + int minRow = Math.Max(0, row - 1); + int maxRow = Math.Min(ROWS - 1, row + 1); + int minCol = Math.Max(0, col - 1); + int maxCol = Math.Min(COLS - 1, col + 1); + + for (int neighborRow = minRow; neighborRow <= maxRow; neighborRow++) + for (int neighborCol = minCol; neighborCol <= maxCol; neighborCol++) + { + if (neighborRow != row || neighborCol != col) + callback(neighborRow, neighborCol); + } + } + + void OnTileStatusChanged(object sender, TileStatus tileStatus) + { + if (isGameEnded) + return; + + // With a first tile tapped, the game is now in progress. + if (!isGameInProgress) + { + isGameInProgress = true; + + // Fire the GameStarted event. + if (GameStarted != null) + { + GameStarted(this, EventArgs.Empty); + } + } + + // Update the "flagged" bug count before checking for a loss. + int flaggedCount = 0; + + foreach (Tile tile in tiles) + if (tile.Status == TileStatus.Flagged) + flaggedCount++; + + this.FlaggedTileCount = flaggedCount; + + // Get the tile whose status has changed. + Tile changedTile = (Tile)sender; + + // If it's exposed, some actions are required. + if (tileStatus == TileStatus.Exposed) + { + if (!isGameInitialized) + { + DefineNewBoard(changedTile.Row, changedTile.Col); + isGameInitialized = true; + } + + if (changedTile.IsBug) + { + isGameInProgress = false; + isGameEnded = true; + + // Fire the GameEnded event! + if (GameEnded != null) + { + GameEnded(this, false); + } + return; + } + + // Auto expose for zero surrounding bugs. + if (changedTile.SurroundingBugCount == 0) + { + CycleThroughNeighbors(changedTile.Row, changedTile.Col, + (neighborRow, neighborCol) => + { + // Expose all the neighbors. + tiles[neighborRow, neighborCol].Status = TileStatus.Exposed; + }); + } + } + + // Check for a win. + bool hasWon = true; + + foreach (Tile til in tiles) + { + if (til.IsBug && til.Status != TileStatus.Flagged) + hasWon = false; + + if (!til.IsBug && til.Status != TileStatus.Exposed) + hasWon = false; + } + + // If there's a win, celebrate! + if (hasWon) + { + isGameInProgress = false; + isGameEnded = true; + + // Fire the GameEnded event! + if (GameEnded != null) + { + GameEnded(this, true); + } + return; + } + } + } +} diff --git a/Samples/BugSweeper/BugSweeperPage.xaml b/Samples/BugSweeper/BugSweeperPage.xaml new file mode 100644 index 0000000..64b74c6 --- /dev/null +++ b/Samples/BugSweeper/BugSweeperPage.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +