diff --git a/.gitignore b/.gitignore index 074320c..fd8fca6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Autosave files *~ +.vs/ # build [Oo]bj/ diff --git a/Ooui.AspNetCore/ElementResult.cs b/Ooui.AspNetCore/ElementResult.cs index f43d342..23234a6 100644 --- a/Ooui.AspNetCore/ElementResult.cs +++ b/Ooui.AspNetCore/ElementResult.cs @@ -7,11 +7,13 @@ namespace Ooui.AspNetCore { public class ElementResult : ActionResult { - readonly Element element; + readonly Element element; + readonly string title; - public ElementResult (Element element) + public ElementResult (Element element, string title = "") { - this.element = element; + this.element = element; + this.title = title; } public override async Task ExecuteResultAsync (ActionContext context) @@ -20,10 +22,11 @@ namespace Ooui.AspNetCore response.StatusCode = 200; response.ContentType = "text/html; charset=utf-8"; var sessionId = WebSocketHandler.BeginSession (context.HttpContext, element); - var html = Encoding.UTF8.GetBytes (UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId)); - response.ContentLength = html.Length; + var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title); + var htmlBytes = Encoding.UTF8.GetBytes (html); + response.ContentLength = htmlBytes.Length; using (var s = response.Body) { - await s.WriteAsync (html, 0, html.Length).ConfigureAwait (false); + await s.WriteAsync (htmlBytes, 0, htmlBytes.Length).ConfigureAwait (false); } } } diff --git a/Ooui.AspNetCore/WebSocketHandler.cs b/Ooui.AspNetCore/WebSocketHandler.cs index 0c22f74..ce6f417 100644 --- a/Ooui.AspNetCore/WebSocketHandler.cs +++ b/Ooui.AspNetCore/WebSocketHandler.cs @@ -32,19 +32,30 @@ namespace Ooui.AspNetCore return id; } + + public static async Task HandleWebSocketRequestAsync (HttpContext context) { + void BadRequest (string message) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.ContentType = "text/plain; charset=utf-8"; + using (var sw = new System.IO.StreamWriter (context.Response.Body)) { + sw.WriteLine (message); + } + } + // // Make sure we get a good ID // if (!context.Request.Query.TryGetValue ("id", out var idValues)) { - context.Response.StatusCode = StatusCodes.Status400BadRequest; + BadRequest ("Missing `id`"); return; } - var id = idValues.FirstOrDefault (); + var id = idValues.LastOrDefault (); if (id == null || id.Length != 32) { - context.Response.StatusCode = StatusCodes.Status400BadRequest; + BadRequest ("Invalid `id`"); return; } @@ -52,7 +63,7 @@ namespace Ooui.AspNetCore // Find the pending session // if (!pendingSessions.TryRemove (id, out var pendingSession)) { - context.Response.StatusCode = StatusCodes.Status400BadRequest; + BadRequest ("Unknown `id`"); return; } @@ -60,16 +71,32 @@ namespace Ooui.AspNetCore // Reject the session if it's old // if ((DateTime.UtcNow - pendingSession.RequestTimeUtc) > SessionTimeout) { - context.Response.StatusCode = StatusCodes.Status400BadRequest; + BadRequest ("Old `id`"); return; } + // + // Set the element's dimensions + // + if (!context.Request.Query.TryGetValue ("w", out var wValues) || wValues.Count < 1) { + BadRequest ("Missing `w`"); + return; + } + if (!context.Request.Query.TryGetValue ("h", out var hValues) || hValues.Count < 1) { + BadRequest ("Missing `h`"); + return; + } + if (!double.TryParse (wValues.Last (), out var w)) + w = 640; + if (!double.TryParse (hValues.Last (), out var h)) + h = 480; + // // OK, Run // var token = CancellationToken.None; var webSocket = await context.WebSockets.AcceptWebSocketAsync ("ooui"); - var session = new Ooui.UI.Session (webSocket, pendingSession.Element, token); + var session = new Ooui.UI.Session (webSocket, pendingSession.Element, w, h, token); await session.RunAsync ().ConfigureAwait (false); } diff --git a/Ooui.Forms/DisplayAlert.cs b/Ooui.Forms/DisplayAlert.cs new file mode 100644 index 0000000..d5f22f5 --- /dev/null +++ b/Ooui.Forms/DisplayAlert.cs @@ -0,0 +1,114 @@ +using System.Web; +using Xamarin.Forms.Internals; + +namespace Ooui.Forms +{ + public class DisplayAlert + { + private readonly Button _closeButton; + private readonly Button _acceptButton; + private readonly Button _cancelButton; + + public DisplayAlert(AlertArguments arguments) + { + Element = new Div + { + ClassName = "modal-dialog" + }; + + var content = new Div + { + ClassName = "modal-content" + }; + + var header = new Div + { + ClassName = "modal-header" + }; + + _closeButton = new Button + { + ClassName = "close" + }; + + _closeButton.AppendChild(new Span(HttpUtility.HtmlDecode("×"))); + + var h4 = new Heading(4) + { + Text = arguments.Title + }; + + header.AppendChild(_closeButton); + header.AppendChild(h4); + + content.AppendChild(header); + content.AppendChild(new Div() + { + ClassName = "modal-body", + Text = arguments.Message + }); + + if (!string.IsNullOrEmpty(arguments.Cancel)) + { + var footer = new Div() + { + ClassName = "modal-footer" + }; + + _cancelButton = new Button(arguments.Cancel) + { + ClassName = "btn btn-default" + }; + _cancelButton.Clicked += (s, e) => SetResult(false); + + footer.AppendChild(_cancelButton); + + if (!string.IsNullOrEmpty(arguments.Accept)) + { + _acceptButton = new Button(arguments.Accept) + { + ClassName = "btn btn-default" + }; + + _acceptButton.Clicked += (s, e) => SetResult(true); + footer.AppendChild(_acceptButton); + } + + content.AppendChild(footer); + } + + + Element.AppendChild(content); + + void SetResult(bool result) + { + arguments.SetResult(result); + } + } + + public event TargetEventHandler Clicked + { + add + { + _closeButton.Clicked += value; + + if(_cancelButton != null) + _cancelButton.Clicked += value; + + if(_acceptButton != null) + _acceptButton.Clicked += value; + } + remove + { + _closeButton.Clicked -= value; + + if (_cancelButton != null) + _cancelButton.Clicked -= value; + + if (_acceptButton != null) + _acceptButton.Clicked -= value; + } + } + public Element Element { get; private set; } + } +} diff --git a/Ooui.Forms/Exports.cs b/Ooui.Forms/Exports.cs index fc910e4..566c468 100644 --- a/Ooui.Forms/Exports.cs +++ b/Ooui.Forms/Exports.cs @@ -5,6 +5,7 @@ using Xamarin.Forms; using Xamarin.Forms.Internals; [assembly: Dependency (typeof (ResourcesProvider))] +[assembly: ExportRenderer (typeof (BoxView), typeof (BoxRenderer))] [assembly: ExportRenderer (typeof (Button), typeof (ButtonRenderer))] [assembly: ExportRenderer (typeof (Label), typeof (LabelRenderer))] diff --git a/Ooui.Forms/Extensions/ColorExtensions.cs b/Ooui.Forms/Extensions/ColorExtensions.cs index 547fc56..ad432ab 100644 --- a/Ooui.Forms/Extensions/ColorExtensions.cs +++ b/Ooui.Forms/Extensions/ColorExtensions.cs @@ -6,7 +6,7 @@ namespace Ooui.Forms.Extensions { public static Color ToOouiColor (this Xamarin.Forms.Color color) { - return new Color ((byte)(color.R * 255.0 + 0.5), (byte)(color.G * 255.0 + 0.5), (byte)(color.B * 255.0 + 0.5), (byte)(color.A * 255.0 + 0.5)); + return new Color ((byte)(color.R * 255.0 + 0.5), (byte)(color.G * 255.0 + 0.5), (byte)(color.B * 255.0 + 0.5), (byte)(color.A * 255.0 + 0.5)); } public static Color ToOouiColor (this Xamarin.Forms.Color color, Xamarin.Forms.Color defaultColor) diff --git a/Ooui.Forms/Extensions/ElementExtensions.cs b/Ooui.Forms/Extensions/ElementExtensions.cs index eb64812..50b7cb6 100644 --- a/Ooui.Forms/Extensions/ElementExtensions.cs +++ b/Ooui.Forms/Extensions/ElementExtensions.cs @@ -8,15 +8,35 @@ namespace Ooui.Forms.Extensions public static SizeRequest GetSizeRequest (this Ooui.Element self, double widthConstraint, double heightConstraint, double minimumWidth = -1, double minimumHeight = -1) { - var s = self.Text.MeasureSize (self.Style); + var rw = 0.0; + var rh = 0.0; + Size s = new Size (0, 0); + var measured = false; - var request = new Size ( - double.IsPositiveInfinity (s.Width) ? double.PositiveInfinity : s.Width, - double.IsPositiveInfinity (s.Height) ? double.PositiveInfinity : s.Height); - var minimum = new Size (minimumWidth < 0 ? request.Width : minimumWidth, - minimumHeight < 0 ? request.Height : minimumHeight); + if (self.Style.Width.Equals ("inherit")) { + s = self.Text.MeasureSize (self.Style); + measured = true; + rw = double.IsPositiveInfinity (s.Width) ? double.PositiveInfinity : s.Width; + } + else { + rw = self.Style.GetNumberWithUnits ("width", "px", 640); + } - return new SizeRequest (request, minimum); + if (self.Style.Height.Equals ("inherit")) { + if (!measured) { + s = self.Text.MeasureSize (self.Style); + measured = true; + } + rh = double.IsPositiveInfinity (s.Height) ? double.PositiveInfinity : s.Height; + } + else { + rh = self.Style.GetNumberWithUnits ("height", "px", 480); + } + + var minimum = new Size (minimumWidth < 0 ? rw : minimumWidth, + minimumHeight < 0 ? rh : minimumHeight); + + return new SizeRequest (new Size (rw, rh), minimum); } } } diff --git a/Ooui.Forms/Extensions/FontExtensions.cs b/Ooui.Forms/Extensions/FontExtensions.cs index 765ee02..310ece6 100644 --- a/Ooui.Forms/Extensions/FontExtensions.cs +++ b/Ooui.Forms/Extensions/FontExtensions.cs @@ -5,8 +5,55 @@ namespace Ooui.Forms.Extensions { public static class FontExtensions { - public static void SetStyleFont (this View view, Style style) + public static void SetStyleFont (this View view, string family, double size, FontAttributes attrs, Style style) { +#pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator + if (size == 14.0) { + style.FontSize = null; + } + else { + style.FontSize = size; + } +#pragma warning restore RECS0018 // Comparison of floating point numbers with equality operator + + if (string.IsNullOrEmpty (family)) { + style.FontFamily = null; + } + else { + style.FontFamily = family; + } + + if (attrs.HasFlag (FontAttributes.Bold)) { + style.FontWeight = "bold"; + } + else { + style.FontWeight = null; + } + + if (attrs.HasFlag (FontAttributes.Italic)) { + style.FontStyle = "italic"; + } + else { + style.FontStyle = null; + } + } + + public static Size MeasureSize (this string text, string fontFamily, double fontSize, FontAttributes fontAttrs) + { + if (string.IsNullOrEmpty (text)) + return Size.Zero; + + var fontHeight = fontSize; + var charWidth = fontSize * 0.5; + + var width = text.Length * charWidth; + + return new Size (width, fontHeight); + } + + public static Size MeasureSize (this string text, Style style) + { + return MeasureSize (text, "", 14, FontAttributes.None); } public static string ToOouiTextAlign (this TextAlignment align) @@ -22,17 +69,5 @@ namespace Ooui.Forms.Extensions } } - public static Size MeasureSize (this string text, Style style) - { - if (string.IsNullOrEmpty (text)) - return Size.Zero; - - var fontHeight = 16.0; - var charWidth = 8.0; - - var width = text.Length * charWidth; - - return new Size (width, fontHeight); - } } } diff --git a/Ooui.Forms/Forms.cs b/Ooui.Forms/Forms.cs index 5782113..c5a39f7 100644 --- a/Ooui.Forms/Forms.cs +++ b/Ooui.Forms/Forms.cs @@ -26,6 +26,7 @@ namespace Xamarin.Forms Device.SetIdiom (TargetIdiom.Desktop); Device.PlatformServices = new OouiPlatformServices (); Device.Info = new OouiDeviceInfo (); + Color.SetAccent (Color.FromHex ("#0000EE")); // Safari Blue Registrar.RegisterAll (new[] { typeof(ExportRendererAttribute), @@ -110,7 +111,13 @@ namespace Xamarin.Forms public void StartTimer (TimeSpan interval, Func callback) { - throw new NotImplementedException (); + Timer timer = null; + timer = new Timer ((_ => { + if (!callback ()) { + timer?.Dispose (); + timer = null; + } + }), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds); } } diff --git a/Ooui.Forms/Ooui.Forms.csproj b/Ooui.Forms/Ooui.Forms.csproj index 5c0c488..e081c10 100644 --- a/Ooui.Forms/Ooui.Forms.csproj +++ b/Ooui.Forms/Ooui.Forms.csproj @@ -15,7 +15,6 @@ - diff --git a/Ooui.Forms/PageExtensions.cs b/Ooui.Forms/PageExtensions.cs index 8f60bd1..4ffe01d 100644 --- a/Ooui.Forms/PageExtensions.cs +++ b/Ooui.Forms/PageExtensions.cs @@ -16,16 +16,24 @@ namespace Xamarin.Forms Ooui.UI.Publish (path, () => lazyPage.Value); } - public static Ooui.Element CreateElement (this Xamarin.Forms.Page page) + public static Ooui.Element GetOouiElement (this Xamarin.Forms.Page page) { if (!Xamarin.Forms.Forms.IsInitialized) throw new InvalidOperationException ("call Forms.Init() before this"); + + var existingRenderer = Ooui.Forms.Platform.GetRenderer (page); + if (existingRenderer != null) + return existingRenderer.NativeView; + return CreateElement (page); + } + + static Ooui.Element CreateElement (this Xamarin.Forms.Page page) + { if (!(page.RealParent is Application)) { var app = new DefaultApplication (); app.MainPage = page; } - var result = new Ooui.Forms.Platform (); result.SetPage (page); return result.Element; diff --git a/Ooui.Forms/Platform.cs b/Ooui.Forms/Platform.cs index 931d867..938adb2 100644 --- a/Ooui.Forms/Platform.cs +++ b/Ooui.Forms/Platform.cs @@ -1,11 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Ooui.Forms.Renderers; using Xamarin.Forms; using Xamarin.Forms.Internals; +using System.Web; -namespace Ooui.Forms +namespace Ooui.Forms { public class Platform : BindableObject, IPlatform, INavigation, IDisposable { @@ -31,6 +32,20 @@ namespace Ooui.Forms public Platform () { _renderer = new PlatformRenderer (this); + + _renderer.Style.PropertyChanged += HandleRendererStyle_PropertyChanged; + + MessagingCenter.Subscribe (this, Page.AlertSignalName, (Page sender, AlertArguments arguments) => { + var alert = new DisplayAlert (arguments); + alert.Clicked += CloseAlert; + + _renderer.AppendChild (alert.Element); + + void CloseAlert (object s, EventArgs e) + { + _renderer.RemoveChild (alert.Element); + } + }); } void IDisposable.Dispose () @@ -70,10 +85,10 @@ namespace Ooui.Forms public SizeRequest GetNativeSize (VisualElement view, double widthConstraint, double heightConstraint) { - var renderView = GetRenderer (view); - if (renderView == null || renderView.NativeView == null) - return new SizeRequest (Size.Zero); - + var renderView = GetRenderer (view); + if (renderView == null || renderView.NativeView == null) + return new SizeRequest (Size.Zero); + return renderView.GetDesiredSize (widthConstraint, heightConstraint); } @@ -114,6 +129,12 @@ namespace Ooui.Forms Console.Error.WriteLine ("Potential view double add"); } + void HandleRendererStyle_PropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var pageRenderer = GetRenderer (Page); + pageRenderer?.SetElementSize (Ooui.Forms.Extensions.ElementExtensions.GetSizeRequest (_renderer, double.PositiveInfinity, double.PositiveInfinity).Request); + } + void INavigation.InsertPageBefore (Page page, Page before) { throw new NotImplementedException (); @@ -173,5 +194,5 @@ namespace Ooui.Forms { throw new NotImplementedException (); } - } -} + } +} diff --git a/Ooui.Forms/PlatformRenderer.cs b/Ooui.Forms/PlatformRenderer.cs index 6e8d939..d555803 100644 --- a/Ooui.Forms/PlatformRenderer.cs +++ b/Ooui.Forms/PlatformRenderer.cs @@ -9,6 +9,8 @@ namespace Ooui.Forms public Platform Platform => platform; + public override bool WantsFullScreen => true; + public PlatformRenderer (Platform platform) { this.platform = platform; diff --git a/Ooui.Forms/Renderers/BoxRenderer.cs b/Ooui.Forms/Renderers/BoxRenderer.cs new file mode 100644 index 0000000..ee64082 --- /dev/null +++ b/Ooui.Forms/Renderers/BoxRenderer.cs @@ -0,0 +1,41 @@ +using System; +using System.ComponentModel; +using Ooui.Forms.Extensions; +using Xamarin.Forms; + +namespace Ooui.Forms.Renderers +{ + public class BoxRenderer : VisualElementRenderer + { + Ooui.Color _colorToRenderer; + + protected override void OnElementChanged (ElementChangedEventArgs e) + { + base.OnElementChanged (e); + + if (Element != null) + SetBackgroundColor (Element.BackgroundColor); + } + + protected override void OnElementPropertyChanged (object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged (sender, e); + if (e.PropertyName == BoxView.ColorProperty.PropertyName) + SetBackgroundColor (Element.BackgroundColor); + } + + protected override void SetBackgroundColor (Xamarin.Forms.Color color) + { + if (Element == null) + return; + + var elementColor = Element.Color; + if (!elementColor.IsDefault) + _colorToRenderer = elementColor.ToOouiColor (); + else + _colorToRenderer = Colors.Clear; + + Style.BackgroundColor = _colorToRenderer; + } + } +} diff --git a/Ooui.Forms/Renderers/ButtonRenderer.cs b/Ooui.Forms/Renderers/ButtonRenderer.cs index 751e6c7..727d835 100644 --- a/Ooui.Forms/Renderers/ButtonRenderer.cs +++ b/Ooui.Forms/Renderers/ButtonRenderer.cs @@ -12,6 +12,13 @@ namespace Ooui.Forms.Renderers Ooui.Color _buttonTextColorDefaultHighlighted; Ooui.Color _buttonTextColorDefaultNormal; + public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) + { + var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); + size = new Size (size.Width, size.Height * 1.428 + 14); + return new SizeRequest (size, size); + } + protected override void Dispose (bool disposing) { if (Control != null) { @@ -31,6 +38,8 @@ namespace Ooui.Forms.Renderers Debug.Assert (Control != null, "Control != null"); + Control.ClassName = "btn btn-primary"; + _buttonTextColorDefaultNormal = Ooui.Colors.Black; _buttonTextColorDefaultHighlighted = Ooui.Colors.Black; _buttonTextColorDefaultDisabled = Ooui.Colors.Black; @@ -98,7 +107,7 @@ namespace Ooui.Forms.Renderers void UpdateFont () { - Element.SetStyleFont (Control.Style); + Element.SetStyleFont (Element.FontFamily, Element.FontSize, Element.FontAttributes, Control.Style); } void UpdateImage () @@ -141,14 +150,10 @@ namespace Ooui.Forms.Renderers void UpdateTextColor () { if (Element.TextColor == Xamarin.Forms.Color.Default) { - Control.Style.Color = _buttonTextColorDefaultNormal; - Control.Style.Color = _buttonTextColorDefaultHighlighted; - Control.Style.Color = _buttonTextColorDefaultDisabled; + Control.Style.Color = null; } else { Control.Style.Color = Element.TextColor.ToOouiColor (); - Control.Style.Color = Element.TextColor.ToOouiColor (); - Control.Style.Color = _buttonTextColorDefaultDisabled; } } } diff --git a/Ooui.Forms/Renderers/EntryRenderer.cs b/Ooui.Forms/Renderers/EntryRenderer.cs new file mode 100644 index 0000000..e72843d --- /dev/null +++ b/Ooui.Forms/Renderers/EntryRenderer.cs @@ -0,0 +1,167 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using Ooui.Forms.Extensions; +using Xamarin.Forms; + +namespace Ooui.Forms.Renderers +{ + public class EntryRenderer : ViewRenderer + { + Ooui.Color _defaultTextColor; + bool _disposed; + + static Size initialSize = Size.Zero; + + public EntryRenderer () + { + } + + public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) + { + return base.GetDesiredSize (widthConstraint, heightConstraint); + } + + IElementController ElementController => Element as IElementController; + + protected override void Dispose (bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) { + if (Control != null) { + //Control.Inputted -= OnEditingBegan; + Control.Inputted -= OnEditingChanged; + Control.Changed -= OnEditingEnded; + } + } + + base.Dispose (disposing); + } + + protected override void OnElementChanged (ElementChangedEventArgs e) + { + base.OnElementChanged (e); + + if (e.NewElement == null) + return; + + if (Control == null) { + var textField = new Ooui.Input (InputType.Text); + SetNativeControl (textField); + + _defaultTextColor = Colors.Black; + + textField.Inputted += OnEditingChanged; + + //textField.EditingDidBegin += OnEditingBegan; + textField.Changed += OnEditingEnded; + } + + UpdatePlaceholder (); + UpdatePassword (); + UpdateText (); + UpdateColor (); + UpdateFont (); + UpdateKeyboard (); + UpdateAlignment (); + } + + protected override void OnElementPropertyChanged (object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Entry.PlaceholderProperty.PropertyName || e.PropertyName == Entry.PlaceholderColorProperty.PropertyName) + UpdatePlaceholder (); + else if (e.PropertyName == Entry.IsPasswordProperty.PropertyName) + UpdatePassword (); + else if (e.PropertyName == Entry.TextProperty.PropertyName) + UpdateText (); + else if (e.PropertyName == Entry.TextColorProperty.PropertyName) + UpdateColor (); + else if (e.PropertyName == Xamarin.Forms.InputView.KeyboardProperty.PropertyName) + UpdateKeyboard (); + else if (e.PropertyName == Entry.HorizontalTextAlignmentProperty.PropertyName) + UpdateAlignment (); + else if (e.PropertyName == Entry.FontAttributesProperty.PropertyName) + UpdateFont (); + else if (e.PropertyName == Entry.FontFamilyProperty.PropertyName) + UpdateFont (); + else if (e.PropertyName == Entry.FontSizeProperty.PropertyName) + UpdateFont (); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) { + UpdateColor (); + UpdatePlaceholder (); + } + + base.OnElementPropertyChanged (sender, e); + } + + void OnEditingBegan (object sender, EventArgs e) + { + ElementController.SetValueFromRenderer (VisualElement.IsFocusedPropertyKey, true); + } + + void OnEditingChanged (object sender, EventArgs eventArgs) + { + ElementController.SetValueFromRenderer (Entry.TextProperty, Control.Value); + } + + 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); + } + + ElementController.SetValueFromRenderer (VisualElement.IsFocusedPropertyKey, false); + } + + void UpdateAlignment () + { + Control.Style.TextAlign = Element.HorizontalTextAlignment.ToOouiTextAlign (); + } + + void UpdateColor () + { + var textColor = Element.TextColor; + + if (textColor.IsDefault || !Element.IsEnabled) + Control.Style.Color = _defaultTextColor; + else + Control.Style.Color = textColor.ToOouiColor (); + } + + void UpdateFont () + { + if (initialSize == Size.Zero) { + var testString = "Tj"; + initialSize = testString.MeasureSize (Control.Style); + } + + Element.SetStyleFont (Element.FontFamily, Element.FontSize, Element.FontAttributes, Control.Style); + } + + void UpdateKeyboard () + { + } + + void UpdatePassword () + { + Control.Type = Element.IsPassword ? InputType.Password : InputType.Text; + } + + void UpdatePlaceholder () + { + Control.Placeholder = Element.Placeholder; + } + + void UpdateText () + { + // ReSharper disable once RedundantCheckBeforeAssignment + if (Control.Text != Element.Text) + Control.Text = Element.Text; + } + } +} diff --git a/Ooui.Forms/Renderers/LabelRenderer.cs b/Ooui.Forms/Renderers/LabelRenderer.cs index 402563e..6a6104d 100644 --- a/Ooui.Forms/Renderers/LabelRenderer.cs +++ b/Ooui.Forms/Renderers/LabelRenderer.cs @@ -17,8 +17,8 @@ namespace Ooui.Forms.Renderers public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) { if (!_perfectSizeValid) { - _perfectSize = base.GetDesiredSize (double.PositiveInfinity, double.PositiveInfinity); - _perfectSize.Minimum = new Size (Math.Min (10, _perfectSize.Request.Width), _perfectSize.Request.Height); + var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); + _perfectSize = new SizeRequest (size, size); _perfectSizeValid = true; } @@ -157,7 +157,7 @@ namespace Ooui.Forms.Renderers return; _perfectSizeValid = false; - Element.SetStyleFont (Control.Style); + Element.SetStyleFont (Element.FontFamily, Element.FontSize, Element.FontAttributes, Control.Style); } void UpdateTextColor () diff --git a/Ooui.Forms/VisualElementTracker.cs b/Ooui.Forms/VisualElementTracker.cs index 6b1fdd7..1e570ee 100644 --- a/Ooui.Forms/VisualElementTracker.cs +++ b/Ooui.Forms/VisualElementTracker.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Threading; using Xamarin.Forms; @@ -141,10 +142,10 @@ namespace Ooui.Forms bool shouldUpdate = width > 0 && height > 0 && parent != null && (boundsChanged || parentBoundsChanged); if (shouldUpdate) { uiview.Style.Position = "absolute"; - uiview.Style.Left = x + "px"; - uiview.Style.Top = y + "px"; - uiview.Style.Width = width + "px"; - uiview.Style.Height = height + "px"; + uiview.Style.Left = x; + uiview.Style.Top = y; + uiview.Style.Width = width; + uiview.Style.Height = height; Renderer.SetControlSize (new Size (width, height)); } else if (width <= 0 || height <= 0) { @@ -157,33 +158,33 @@ namespace Ooui.Forms uiview.Style.Opacity = opacity; } - //var transform = 0; - //const double epsilon = 0.001; - //caLayer.AnchorPoint = new PointF (anchorX - 0.5f, anchorY - 0.5f); + var transforms = ""; + var transformOrigin = default (string); + const double epsilon = 0.001; - //// position is relative to anchor point - //if (Math.Abs (anchorX - .5) > epsilon) - // transform = transform.Translate ((anchorX - .5f) * width, 0, 0); - //if (Math.Abs (anchorY - .5) > epsilon) - // transform = transform.Translate (0, (anchorY - .5f) * height, 0); + var icult = System.Globalization.CultureInfo.InvariantCulture; - //if (Math.Abs (translationX) > epsilon || Math.Abs (translationY) > epsilon) - // transform = transform.Translate (translationX, translationY, 0); + // position is relative to anchor point + if ((Math.Abs (anchorX - 0.5) > epsilon) || (Math.Abs (anchorY - 0.5) > epsilon)) { + transformOrigin = string.Format (icult, "{0:0.######}% {1:0.######}%", anchorX*100, anchorY*100); + } - //if (Math.Abs (scale - 1) > epsilon) - // transform = transform.Scale (scale); + if (Math.Abs (translationX) > epsilon || Math.Abs (translationY) > epsilon) + transforms = string.Format (icult, "{0} translate({1:0.######}px,{2:0.######}px)", transforms, translationX, translationY); - //// not just an optimization, iOS will not "pixel align" a view which has m34 set - //if (Math.Abs (rotationY % 180) > epsilon || Math.Abs (rotationX % 180) > epsilon) - // transform.m34 = 1.0f / -400f; + if (Math.Abs (scale - 1) > epsilon) + transforms = string.Format (icult, "{0} scale({1:0.######},{1:0.######})", transforms, scale); //if (Math.Abs (rotationX % 360) > epsilon) - // transform = transform.Rotate (rotationX * (float)Math.PI / 180.0f, 1.0f, 0.0f, 0.0f); + // RotateX (rotationX); //if (Math.Abs (rotationY % 360) > epsilon) - // transform = transform.Rotate (rotationY * (float)Math.PI / 180.0f, 0.0f, 1.0f, 0.0f); + //RotateY (rotationY); - //transform = transform.Rotate (rotation * (float)Math.PI / 180.0f, 0.0f, 0.0f, 1.0f); - //caLayer.Transform = transform; + if (Math.Abs (rotation % 360) > epsilon) + transforms = string.Format (icult, "{0} rotate({1:0.######}deg)", transforms, rotation); + + uiview.Style.Transform = transforms.Length > 0 ? transforms : null; + uiview.Style.TransformOrigin = transforms.Length > 0 ? transformOrigin : null; _lastBounds = view.Bounds; _lastParentBounds = viewParent?.Bounds ?? Rectangle.Zero; diff --git a/Ooui/Client.js b/Ooui/Client.js index bff2f13..108fc35 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -18,10 +18,32 @@ const mouseEvents = { wheel: true, }; +// Try to close the socket gracefully +window.onbeforeunload = function() { + if (socket != null) { + socket.close (1001, "Unloading page"); + socket = null; + console.log ("Web socket closed"); + } + return null; +} + +function getSize () { + return { + height: window.innerHeight, + width: window.innerWidth + }; +} + +// Main entrypoint function ooui (rootElementPath) { var opened = false; - socket = new WebSocket ("ws://" + document.location.host + rootElementPath, "ooui"); + var initialSize = getSize (); + var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + + "w=" + initialSize.width + "&h=" + initialSize.height; + + socket = new WebSocket ("ws://" + document.location.host + rootElementPath + wsArgs, "ooui"); socket.addEventListener ("open", function (event) { console.log ("Web socket opened"); @@ -52,6 +74,34 @@ function ooui (rootElementPath) { }); console.log("Web socket created"); + + // Throttled window resize event + (function() { + window.addEventListener("resize", resizeThrottler, false); + + var resizeTimeout; + function resizeThrottler() { + if (!resizeTimeout) { + resizeTimeout = setTimeout(function() { + resizeTimeout = null; + actualResizeHandler(); + }, 100); + } + } + + function resizeHandler() { + const em = { + m: "event", + id: 42, + k: "window.resize", + v: getSize (), + }; + const ems = JSON.stringify (em); + if (socket != null) + socket.send (ems); + if (debug) console.log ("Event", em); + } + }()); } function getNode (id) { @@ -135,7 +185,8 @@ function msgListen (m) { }; } const ems = JSON.stringify (em); - socket.send (ems); + if (socket != null) + socket.send (ems); if (debug) console.log ("Event", em); if (em.k === "submit") e.preventDefault (); diff --git a/Ooui/Color.cs b/Ooui/Color.cs index 7b914ad..476e1cd 100644 --- a/Ooui/Color.cs +++ b/Ooui/Color.cs @@ -1,20 +1,21 @@ using System; - +using Newtonsoft.Json; using StyleValue = System.Object; namespace Ooui { - public struct Color + [Newtonsoft.Json.JsonConverter (typeof (ColorJsonConverter))] + public struct Color : IEquatable { public byte R, G, B, A; public Color (byte r, byte g, byte b, byte a) - { - R = r; - G = g; - B = b; - A = a; - } + { + R = r; + G = g; + B = b; + A = a; + } public double Red { get => R / 255.0; @@ -33,9 +34,70 @@ namespace Ooui set => A = value >= 1.0 ? (byte)255 : ((value <= 0.0) ? (byte)0 : (byte)(value * 255.0 + 0.5)); } - public static Color FromStyleValue (StyleValue styleColor) - { + public override bool Equals (object obj) + { + if (obj is Color other) + return R == other.R && G == other.G && B == other.B && A == other.A; + return false; + } + + public bool Equals (Color other) => R == other.R && G == other.G && B == other.B && A == other.A; + + public override int GetHashCode () => R.GetHashCode () + G.GetHashCode () * 2 + B.GetHashCode () * 3 + A.GetHashCode () * 5; + + public static Color FromStyleValue (StyleValue styleColor) + { + if (styleColor is Color c) + return c; + if (styleColor is string s) + return Parse (s); return Colors.Clear; - } - } + } + + public static Color Parse (string styleValue) + { + if (string.IsNullOrWhiteSpace (styleValue) || styleValue.Length < 4) + throw new ArgumentException ("Cannot parse empty strings", nameof (styleValue)); + + if (styleValue.Length > 32) + throw new ArgumentException ("Color string is too long", nameof (styleValue)); + + if (styleValue == "inherit") + return Colors.Clear; + + //if (styleValue[0] == '#' && styleValue.Length == 4) { + //} + + //if (styleValue[0] == '#' && styleValue.Length == 7) { + //} + + throw new ArgumentException ($"Cannot parse color string `{styleValue}`", nameof (styleValue)); + } + + public override string ToString () + { + if (A == 255) + return string.Format ("#{0:x2}{1:x2}{2:x2}", R, G, B); + return string.Format ("rgba({0},{1},{2},{3})", R, G, B, A / 255.0); + } + } + + class ColorJsonConverter : Newtonsoft.Json.JsonConverter + { + public override bool CanConvert (Type objectType) + { + return objectType == typeof (Color); + } + + public override object ReadJson (JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var str = reader.ReadAsString (); + return Color.Parse (str); + } + + public override void WriteJson (JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue (value.ToString ()); + } + } } diff --git a/Ooui/Element.cs b/Ooui/Element.cs index 253f57d..8ba85e9 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -90,6 +90,12 @@ namespace Ooui remove => RemoveEventListener ("wheel", value); } + /// + /// A signal to Ooui that this element should take up the + /// entire browser window. + /// + public virtual bool WantsFullScreen => false; + protected Element (string tagName) : base (tagName) { diff --git a/Ooui/Input.cs b/Ooui/Input.cs index e24abe9..9a2d2f2 100644 --- a/Ooui/Input.cs +++ b/Ooui/Input.cs @@ -40,6 +40,12 @@ namespace Ooui remove => RemoveEventListener ("input", value); } + string placeholder = ""; + public string Placeholder { + get => placeholder; + set => SetProperty (ref placeholder, value, "placeholder"); + } + bool isChecked = false; public bool IsChecked { get => isChecked; diff --git a/Ooui/Platform.cs b/Ooui/Platform.cs index 123f8cc..287bfa4 100644 --- a/Ooui/Platform.cs +++ b/Ooui/Platform.cs @@ -7,165 +7,131 @@ 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; + static readonly Assembly iosAssembly; + static readonly Type iosUIViewControllerType; + static readonly Type iosUIApplicationType; + static readonly Type iosUIWebViewType; + static readonly Type iosNSUrl; + static readonly Type iosNSUrlRequest; - static readonly Assembly androidAssembly; - static readonly Type androidActivityType; - static readonly Type androidWebViewType; + static readonly Assembly androidAssembly; + static readonly Type androidActivityType; + static readonly Type androidWebViewType; - static Platform() - { - var asms = AppDomain.CurrentDomain.GetAssemblies().ToDictionary( - x => x.GetName().Name); + static Platform () + { + var asms = AppDomain.CurrentDomain.GetAssemblies ().ToDictionary ( + x => x.GetName ().Name); - 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"); - } + 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"); + } - asms.TryGetValue("Mono.Android", out androidAssembly); - if (androidAssembly != null) - { - androidActivityType = androidAssembly.GetType("Android.App.Activity"); - androidWebViewType = androidAssembly.GetType("Android.Webkit.WebView"); - } - } + asms.TryGetValue ("Mono.Android", out androidAssembly); + if (androidAssembly != null) { + androidActivityType = androidAssembly.GetType ("Android.App.Activity"); + androidWebViewType = androidAssembly.GetType ("Android.Webkit.WebView"); + } + } - public static void OpenBrowser(string url, object presenter) - { - if (iosAssembly != null) - { - OpenBrowserOniOS(url, presenter); - } - else if (androidAssembly != null) - { - OpenBrowserOnAndroid(url, presenter); - } - else - { - StartBrowserProcess(url); - } - } + 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 void OpenBrowserOnAndroid(string url, object presenter) - { - var presenterType = GetObjectType(presenter); + static void OpenBrowserOnAndroid (string url, object presenter) + { + var presenterType = GetObjectType (presenter); - object presenterWebView = null; - if (presenter != null && androidWebViewType.IsAssignableFrom(presenterType)) - { - presenterWebView = presenter; - } + object presenterWebView = null; + if (presenter != null && androidWebViewType.IsAssignableFrom (presenterType)) { + presenterWebView = presenter; + } - if (presenterWebView == null) - { - throw new ArgumentException("Presenter must be a WebView", nameof(presenter)); - } + if (presenterWebView == null) { + throw new ArgumentException ("Presenter must be a WebView", nameof(presenter)); + } - var m = androidWebViewType.GetMethod("LoadUrl", BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof(string) }, null); - m.Invoke(presenterWebView, new object[] { 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 OpenBrowserOniOS(string url, object presenter) - { - var presenterType = GetObjectType(presenter); + static void OpenBrowserOniOS (string url, object presenter) + { + var presenterType = GetObjectType (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; - } + // + // 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 (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; - } - } - } + 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; + } + } + } - if (presenterViewController == null) - { - throw new InvalidOperationException("Cannot find a view controller from which to present"); - } + if (presenterViewController == null) { + throw new InvalidOperationException ("Cannot find a view controller from which to present"); + } - // - // Create the browser - // - var browserVC = Activator.CreateInstance(iosUIViewControllerType); - var browserV = Activator.CreateInstance(iosUIWebViewType); + // + // Create the browser + // + var browserVC = Activator.CreateInstance (iosUIViewControllerType); + var browserV = Activator.CreateInstance (iosUIWebViewType); - 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); + 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); - var m = iosUIViewControllerType.GetMethod("PresentViewController"); + var m = iosUIViewControllerType.GetMethod ("PresentViewController"); - // Console.WriteLine (presenterViewController); - // Console.WriteLine (browserVC); - m.Invoke(presenterViewController, new object[] { browserVC, false, null }); - } + // 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 Process StartBrowserProcess(string url) - { - var cmd = url; - var args = string.Empty; - - var osv = Environment.OSVersion; - - if (osv.Platform == PlatformID.Unix) - { - cmd = "open"; - args = url; - } - - var platform = (int)Environment.OSVersion.Platform; - var isWindows = ((platform != 4) && (platform != 6) && (platform != 128)); - - if (isWindows) - { - cmd = "explorer.exe"; - args = url; - } + 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 Process StartBrowserProcess (string url) + { // var vs = Environment.GetEnvironmentVariables (); // foreach (System.Collections.DictionaryEntry kv in vs) { // System.Console.WriteLine($"K={kv.Key}, V={kv.Value}"); @@ -173,8 +139,9 @@ namespace Ooui // Console.WriteLine ($"Process.Start {cmd} {args}"); - return Process.Start(cmd, args); - + return Environment.OSVersion.Platform == PlatformID.Unix + ? Process.Start ("open", url) + : Process.Start (new ProcessStartInfo (url) { UseShellExecute = true }); } } } diff --git a/Ooui/Style.cs b/Ooui/Style.cs index a92f6d3..06b6149 100644 --- a/Ooui/Style.cs +++ b/Ooui/Style.cs @@ -11,6 +11,10 @@ namespace Ooui readonly Dictionary properties = new Dictionary (); + static readonly private char[] numberChars = new char[] { + '0','1','2','3','4','5','6','7','8','9', + '.','-','+', + }; public Value AlignSelf { get => this["align-self"]; @@ -126,7 +130,7 @@ namespace Ooui public Value Bottom { get => this["bottom"]; - set => this["bottom"] = value; + set => this["bottom"] = AddNumberUnits (value, "px"); } public Value Clear { @@ -176,7 +180,7 @@ namespace Ooui public Value FontSize { get => this["font-size"]; - set => this["font-size"] = value; + set => this["font-size"] = AddNumberUnits (value, "px"); } public Value FontStyle { @@ -196,12 +200,12 @@ namespace Ooui public Value Height { get => this["height"]; - set => this["height"] = value; + set => this["height"] = AddNumberUnits (value, "px"); } public Value Left { get => this["left"]; - set => this["left"] = value; + set => this["left"] = AddNumberUnits (value, "px"); } public Value LineHeight { @@ -301,7 +305,17 @@ namespace Ooui public Value Top { get => this["top"]; - set => this["top"] = value; + set => this["top"] = AddNumberUnits (value, "px"); + } + + public Value Transform { + get => this["transform"]; + set => this["transform"] = value; + } + + public Value TransformOrigin { + get => this["transform-origin"]; + set => this["transform-origin"] = value; } public Value VerticalAlign { @@ -316,7 +330,7 @@ namespace Ooui public Value Width { get => this["width"]; - set => this["width"] = value; + set => this["width"] = AddNumberUnits (value, "px"); } public Value ZIndex { @@ -386,5 +400,34 @@ namespace Ooui } return o.ToString (); } + + static string AddNumberUnits (object val, string units) + { + if (val is string s) + return s; + if (val is IConvertible c) + return c.ToString (System.Globalization.CultureInfo.InvariantCulture) + units; + return val.ToString (); + } + + public double GetNumberWithUnits (string key, string units, double baseValue) + { + var v = this[key]; + if (v == null) + return 0; + + if (v is string s) { + var lastIndex = s.LastIndexOfAny (numberChars); + if (lastIndex < 0) + return 0; + var num = double.Parse (s.Substring (0, lastIndex + 1), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + return num; + } + + if (v is IConvertible c) + return c.ToDouble (System.Globalization.CultureInfo.InvariantCulture); + + return 0; + } } } diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 1e4bd24..b3a3ac9 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -34,10 +34,11 @@ namespace Ooui @Title + -
+
@@ -299,9 +300,9 @@ namespace Ooui } } - public static string RenderTemplate (string webSocketPath) + public static string RenderTemplate (string webSocketPath, string title = "") { - return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()); + return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title); } class DataHandler : RequestHandler @@ -435,7 +436,7 @@ namespace Ooui // Create a new session and let it handle everything from here // try { - var session = new Session (webSocket, element, serverToken); + var session = new Session (webSocket, element, 1024, 768, serverToken); await session.RunAsync ().ConfigureAwait (false); } catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { @@ -472,11 +473,15 @@ namespace Ooui readonly System.Timers.Timer sendThrottle; DateTime lastTransmitTime = DateTime.MinValue; readonly TimeSpan throttleInterval = TimeSpan.FromSeconds (1.0 / 30); // 30 FPS max + readonly double initialWidth; + readonly double initialHeight; - public Session (WebSocket webSocket, Element element, CancellationToken serverToken) + 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 @@ -524,6 +529,10 @@ namespace Ooui // // Add it to the document body // + if (element.WantsFullScreen) { + element.Style.Width = initialWidth; + element.Style.Height = initialHeight; + } QueueMessage (Message.Call ("document.body", "appendChild", element)); // @@ -570,34 +579,39 @@ namespace Ooui } } - void QueueStateMessages (EventTarget target) + void QueueStateMessagesLocked (EventTarget target) { if (target == null) return; + var created = false; foreach (var m in target.StateMessages) { - QueueMessage (m); + if (m.MessageType == MessageType.Create) { + createdIds.Add (m.TargetId); + created = true; + } + if (created) { + QueueMessageLocked (m); + } } } - void QueueMessage (Message message) + void QueueMessageLocked (Message message) { // // Make sure all the referenced objects have been created // - if (message.MessageType == MessageType.Create) { - createdIds.Add (message.TargetId); + if (!createdIds.Contains (message.TargetId)) { + QueueStateMessagesLocked (element.GetElementById (message.TargetId)); } - else { - if (!createdIds.Contains (message.TargetId)) { - createdIds.Add (message.TargetId); - QueueStateMessages (element.GetElementById (message.TargetId)); + if (message.Value is EventTarget ve) { + if (!createdIds.Contains (ve.Id)) { + QueueStateMessagesLocked (ve); } - 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)) { - createdIds.Add (e.Id); - QueueStateMessages (e); - } + } + 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); } } } @@ -605,7 +619,14 @@ namespace Ooui // // Add it to the queue // - lock (queuedMessages) queuedMessages.Add (message); + queuedMessages.Add (message); + } + + void QueueMessage (Message message) + { + lock (queuedMessages) { + QueueMessageLocked (message); + } sendThrottle.Enabled = true; } diff --git a/PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj b/PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj index 1e9ec6c..4e05f3e 100644 --- a/PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj +++ b/PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj @@ -9,6 +9,7 @@ + @@ -19,5 +20,6 @@ + diff --git a/PlatformSamples/AspNetCoreMvc/Controllers/HomeController.cs b/PlatformSamples/AspNetCoreMvc/Controllers/HomeController.cs index 11687ab..d8433eb 100644 --- a/PlatformSamples/AspNetCoreMvc/Controllers/HomeController.cs +++ b/PlatformSamples/AspNetCoreMvc/Controllers/HomeController.cs @@ -18,33 +18,16 @@ namespace AspNetCoreMvc.Controllers return View (); } - public IActionResult Clicker () - { - var count = 0; - var head = new Heading { Text = "Click away!" }; - var label = new Label { Text = "0" }; - var btn = new Button { Text = "Increase" }; - btn.Clicked += (sender, e) => { - count++; - label.Text = count.ToString (); - }; - var div = new Div (); - div.AppendChild (head); - div.AppendChild (label); - div.AppendChild (btn); - return new ElementResult (div); - } - public IActionResult About () { - ViewData["Message"] = "Your application description page."; + ViewData["Message"] = "Ooui is a mini web framework to make programming interactive UIs easy."; return View (); } public IActionResult Contact () { - ViewData["Message"] = "Your contact page."; + ViewData["Message"] = "Find us on github."; return View (); } diff --git a/PlatformSamples/AspNetCoreMvc/Controllers/SamplesController.cs b/PlatformSamples/AspNetCoreMvc/Controllers/SamplesController.cs index 640f9b5..cf4aa7a 100644 --- a/PlatformSamples/AspNetCoreMvc/Controllers/SamplesController.cs +++ b/PlatformSamples/AspNetCoreMvc/Controllers/SamplesController.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Mvc; using AspNetCoreMvc.Models; using Ooui; using Ooui.AspNetCore; +using Samples; +using System.Collections.Concurrent; namespace AspNetCoreMvc.Controllers { @@ -35,23 +37,47 @@ namespace AspNetCoreMvc.Controllers var sampleType = typeof (Samples.ISample); var asm = sampleType.Assembly; var sampleTypes = asm.GetTypes ().Where (x => x.Name.EndsWith ("Sample", StringComparison.Ordinal) && x != sampleType); - var samples = from t in sampleTypes let s = Activator.CreateInstance (t) as Samples.ISample where s != null select s; + var samples = from t in sampleTypes + let s = Activator.CreateInstance (t) as Samples.ISample + where s != null + orderby s.Title + select s; return samples.ToList (); }), true); + static readonly ConcurrentDictionary sharedSamples = + new ConcurrentDictionary (); + public static List Samples => lazySamples.Value; - [Route("/Samples/Run/{name}")] - public IActionResult Run (string name) + [Route ("/Samples/Run/{name}")] + public IActionResult Run (string name, bool shared) { if (string.IsNullOrWhiteSpace (name) || name.Length > 32) return BadRequest (); - + var s = Samples.FirstOrDefault (x => x.Title == name); if (s == null) return NotFound (); - return new ElementResult (s.CreateElement ()); + var element = shared ? GetSharedSample (s) : s.CreateElement (); + + return new ElementResult (element, title: s.Title + " - Ooui Samples"); + } + + private Element GetSharedSample (ISample s) + { + if (sharedSamples.TryGetValue (s.Title, out var e)) + return e; + e = s.CreateElement (); + sharedSamples[s.Title] = e; + return e; + } + + [Route ("/shared-button")] + public IActionResult SharedButton () + { + return Run ("Button Counter", true); } } } diff --git a/PlatformSamples/AspNetCoreMvc/Program.cs b/PlatformSamples/AspNetCoreMvc/Program.cs index c6632a9..ac9502b 100644 --- a/PlatformSamples/AspNetCoreMvc/Program.cs +++ b/PlatformSamples/AspNetCoreMvc/Program.cs @@ -12,14 +12,15 @@ namespace AspNetCoreMvc { public class Program { - public static void Main(string[] args) + public static void Main (string[] args) { - BuildWebHost(args).Run(); + BuildWebHost (args).Run (); } - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .Build(); + public static IWebHost BuildWebHost (string[] args) => + WebHost.CreateDefaultBuilder (args) + .UseConfiguration (new ConfigurationBuilder ().AddCommandLine (args).Build ()) + .UseStartup () + .Build (); } } diff --git a/PlatformSamples/AspNetCoreMvc/Startup.cs b/PlatformSamples/AspNetCoreMvc/Startup.cs index cc5ff26..babc056 100644 --- a/PlatformSamples/AspNetCoreMvc/Startup.cs +++ b/PlatformSamples/AspNetCoreMvc/Startup.cs @@ -39,6 +39,8 @@ namespace AspNetCoreMvc app.UseOoui (); + Xamarin.Forms.Forms.Init (); + app.UseMvc (routes => { routes.MapRoute ( name: "default", diff --git a/PlatformSamples/AspNetCoreMvc/Views/Home/About.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Home/About.cshtml index a443e8b..c9f4798 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Home/About.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Home/About.cshtml @@ -4,4 +4,4 @@

@ViewData["Title"].

@ViewData["Message"]

-

Use this area to provide additional information.

+

Find out more on github

diff --git a/PlatformSamples/AspNetCoreMvc/Views/Home/Contact.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Home/Contact.cshtml index 9d10bf9..d1ef9c9 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Home/Contact.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Home/Contact.cshtml @@ -4,14 +4,3 @@

@ViewData["Title"].

@ViewData["Message"]

-
- One Microsoft Way
- Redmond, WA 98052-6399
- P: - 425.555.0100 -
- -
- Support: Support@example.com
- Marketing: Marketing@example.com -
diff --git a/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml index 53b984e..6fb9f7f 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml @@ -8,16 +8,19 @@

Ooui

-

Write interactive web apps in C#

+

Write interactive web apps in C# and F#

-
+

Samples

diff --git a/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml index 25513d3..ae68cea 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml @@ -32,7 +32,6 @@
diff --git a/README.md b/README.md index 4599264..31e7782 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ It presents a classic object-oriented UI API that controls a dumb browser. With ## Try it Online -Head on over to [http://ooui.mecha.parts:8080/shared-button](http://ooui.mecha.parts:8080/shared-button) and give it a click! +Head on over to [http://ooui.mecha.parts](http://ooui.mecha.parts) to tryout the samples. ## Try the Samples Locally diff --git a/Samples/BoxViewClockSample.cs b/Samples/BoxViewClockSample.cs new file mode 100644 index 0000000..f576061 --- /dev/null +++ b/Samples/BoxViewClockSample.cs @@ -0,0 +1,138 @@ +using System; +using Xamarin.Forms; + +namespace Samples +{ + public class BoxViewClockSample : ISample + { + public string Title => "Xamarin.Forms BoxViewClock"; + + BoxViewClockPage page; + + public Ooui.Element CreateElement () + { + // + // Always return the same page because the code never stops the timer + // and we don't want to create an unlimited number of them. + // + if (page == null) + page = new BoxViewClockPage (); + return page.GetOouiElement (); + } + + class BoxViewClockPage : ContentPage + { + // Structure for storing information about the three hands. + struct HandParams + { + public HandParams (double width, double height, double offset) : this () + { + Width = width; + Height = height; + Offset = offset; + } + + public double Width { private set; get; } // fraction of radius + public double Height { private set; get; } // ditto + public double Offset { private set; get; } // relative to center pivot + } + + static readonly HandParams secondParams = new HandParams (0.02, 1.1, 0.85); + static readonly HandParams minuteParams = new HandParams (0.05, 0.8, 0.9); + static readonly HandParams hourParams = new HandParams (0.125, 0.65, 0.9); + + BoxView[] tickMarks = new BoxView[60]; + BoxView secondHand, minuteHand, hourHand; + + public BoxViewClockPage () + { + AbsoluteLayout absoluteLayout = new AbsoluteLayout (); + + // Create the tick marks (to be sized and positioned later) + for (int i = 0; i < tickMarks.Length; i++) { + tickMarks[i] = new BoxView { + Color = Color.Accent + }; + absoluteLayout.Children.Add (tickMarks[i]); + } + + // Create the three hands. + absoluteLayout.Children.Add (hourHand = + new BoxView { + Color = Color.Accent + }); + absoluteLayout.Children.Add (minuteHand = + new BoxView { + Color = Color.Accent + }); + absoluteLayout.Children.Add (secondHand = + new BoxView { + Color = Color.Accent + }); + + Content = absoluteLayout; + + // Attach a couple event handlers. + Device.StartTimer (TimeSpan.FromMilliseconds (16), OnTimerTick); + SizeChanged += OnPageSizeChanged; + } + + void OnPageSizeChanged (object sender, EventArgs args) + { + // Size and position the 12 tick marks. + Point center = new Point (this.Width / 2, this.Height / 2); + double radius = 0.45 * Math.Min (this.Width, this.Height); + + for (int i = 0; i < tickMarks.Length; i++) { + double size = radius / (i % 5 == 0 ? 15 : 30); + double radians = i * 2 * Math.PI / tickMarks.Length; + double x = center.X + radius * Math.Sin (radians) - size / 2; + double y = center.Y - radius * Math.Cos (radians) - size / 2; + AbsoluteLayout.SetLayoutBounds (tickMarks[i], new Rectangle (x, y, size, size)); + + tickMarks[i].AnchorX = 0.51; // Anchor settings necessary for Android + tickMarks[i].AnchorY = 0.51; + tickMarks[i].Rotation = 180 * radians / Math.PI; + } + + // Function for positioning and sizing hands. + Action Layout = (boxView, handParams) => { + double width = handParams.Width * radius; + double height = handParams.Height * radius; + double offset = handParams.Offset; + + AbsoluteLayout.SetLayoutBounds (boxView, + new Rectangle (center.X - 0.5 * width, + center.Y - offset * height, + width, height)); + + boxView.AnchorX = 0.51; + boxView.AnchorY = handParams.Offset; + }; + + Layout (secondHand, secondParams); + Layout (minuteHand, minuteParams); + Layout (hourHand, hourParams); + } + + bool OnTimerTick () + { + // Set rotation angles for hour and minute hands. + DateTime dateTime = DateTime.Now; + hourHand.Rotation = 30 * (dateTime.Hour % 12) + 0.5 * dateTime.Minute; + minuteHand.Rotation = 6 * dateTime.Minute + 0.1 * dateTime.Second; + + // Do an animation for the second hand. + double t = dateTime.Millisecond / 1000.0; + if (t < 0.5) { + t = 0.5 * Easing.SpringIn.Ease (t / 0.5); + } + else { + t = 0.5 * (1 + Easing.SpringOut.Ease ((t - 0.5) / 0.5)); + } + secondHand.Rotation = 6 * (dateTime.Second + t); + return true; + } + } + } +} diff --git a/Samples/ButtonSample.cs b/Samples/ButtonSample.cs index 2a2d685..7d8a08f 100644 --- a/Samples/ButtonSample.cs +++ b/Samples/ButtonSample.cs @@ -5,11 +5,14 @@ namespace Samples { public class ButtonSample : ISample { - public string Title => "Button that count clicks"; + public string Title => "Button Counter"; Button MakeButton () { - var button = new Button ("Click me!"); + var button = new Button ("Click me!") { + ClassName = "btn btn-primary", // Some bootstrap styling + }; + button.Style.MarginTop = "2em"; var count = 0; button.Clicked += (s, e) => { count++; diff --git a/Samples/ButtonXamlPage.xaml b/Samples/ButtonXamlPage.xaml new file mode 100644 index 0000000..9fafa5f --- /dev/null +++ b/Samples/ButtonXamlPage.xaml @@ -0,0 +1,12 @@ + + + + +