Merge remote-tracking branch 'refs/remotes/praeclarum/master'

This commit is contained in:
Javier Suárez Ruiz 2017-12-12 19:41:19 +01:00
commit dcec2428d5
59 changed files with 1975 additions and 183 deletions

View File

@ -0,0 +1,56 @@
<html>
<body>
<textarea id="result" cols="200" rows="32"></textarea>
<canvas id="canvas" width="320" height="200"></canvas>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var r = "static readonly double[] CharacterProportions = {\n ";
var head = "";
let size = 16;
ctx.font = "bold " + size + "px \"Helvetica Neue\"";
var mmm = ctx.measureText("MM");
let sp = 0;
let np = 0;
let mw = 0;
for (let i = 0; i < 128; i++) {
if (i > 0 && i % 8 == 0) {
head = ",\n ";
}
else if (i > 0) {
head = ", ";
}
let c = String.fromCharCode(i);
let s = "M" + c + "M";
let m = ctx.measureText(s);
let w = m.width - mmm.width;
let p = w / size;
if (p > 1e-4) {
sp += p;
np++;
}
if (c == "M") {
mw = w;
}
r += head + p;
console.log (c + " = " + w);
}
let ap = sp / np;
let padding = (mmm.width - mw*2)/size;
r += "\n};\nconst double AverageCharProportion = " + ap + ";";
r += "\nconst double StringWidthPaddingProportion = " + padding + ";";
console.log(r);
document.getElementById("result").innerText = r;
</script>
</body>
</html>

View File

@ -1,6 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
<Authors>praeclarum</Authors>
<Description>ASP.NET Core MVC extensions to make working with Ooui easy.</Description>
<PackageTags>Ooui;UI;CrossPlatform;ASP.NET</PackageTags>
<PackageIconUrl>https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

View File

@ -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);
}

View File

@ -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;
//

View File

@ -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; }

184
Ooui.Forms/EventTracker.cs Normal file
View File

@ -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<IGestureRecognizer, NativeGestureRecognizer> _gestureRecognizers = new Dictionary<IGestureRecognizer, NativeGestureRecognizer> ();
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<IGestureRecognizer> ElementGestureRecognizers {
get {
if (_renderer?.Element is View)
return ((View)_renderer.Element).GestureRecognizers as ObservableCollection<IGestureRecognizer>;
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<IGestureRecognizer>)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 ()
{
}
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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<ViewInitializedEventArgs> 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

View File

@ -1,6 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
<Authors>praeclarum</Authors>
<Description>Xamarin.Forms backend for the web using Ooui technologies.</Description>
<PackageTags>Ooui;UI;CrossPlatform;Xamarin.Forms</PackageTags>
<PackageIconUrl>https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

View File

@ -57,6 +57,10 @@ namespace Ooui.Forms
MessagingCenter.Unsubscribe<Page, ActionSheetArguments> (this, Page.ActionSheetSignalName);
MessagingCenter.Unsubscribe<Page, AlertArguments> (this, Page.AlertSignalName);
MessagingCenter.Unsubscribe<Page, bool> (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)

View File

@ -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);
}
}
}
}

View File

@ -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 ();

View File

@ -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;
}
}

View File

@ -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 ()

View File

@ -6,7 +6,7 @@ using Xamarin.Forms;
namespace Ooui.Forms.Renderers
{
public class EntryRenderer : ViewRenderer<Entry, Ooui.Input>
public class EntryRenderer : ViewRenderer<Entry, Ooui.TextInput>
{
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);

View File

@ -0,0 +1,64 @@
using System;
using System.ComponentModel;
using Xamarin.Forms;
using Ooui.Forms.Extensions;
namespace Ooui.Forms.Renderers
{
public class FrameRenderer : VisualElementRenderer<Frame>
{
protected override void OnElementChanged (ElementChangedEventArgs<Frame> 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";
}
}
}
}

View File

@ -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<Xamarin.Forms.Image, Ooui.Image>
{
bool _isDisposed;
protected override void Dispose (bool disposing)
{
if (_isDisposed)
return;
if (disposing) {
}
_isDisposed = true;
base.Dispose (disposing);
}
protected override async void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.Image> 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<IImageSourceHandler> (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<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1);
}
public sealed class FileImageSourceHandler : IImageSourceHandler
{
public async Task<string> 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<string> 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<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
{
var imageLoader = imagesource as UriImageSource;
return Task.FromResult (imageLoader?.Uri.ToString () ?? "");
}
}
}

View File

@ -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 ()

View File

@ -0,0 +1,53 @@
using System;
using Xamarin.Forms;
namespace Ooui.Forms.Renderers
{
public class SwitchRenderer : ViewRenderer<Switch, Input>
{
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<Switch> 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;
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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<Element> children)
: this ()
{
foreach (var c in children) {
AppendChild (c);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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));
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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<Node> ();
@ -100,7 +110,7 @@ namespace Ooui
}
foreach (var child in toRemove) {
child.MessageSent -= HandleChildMessageSent;
SendCall ("removeChild", child);
Call ("removeChild", child);
}
InsertBefore (newNode, null);
}

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>0.2.0</Version>
<Version>1.0.0</Version>
<Authors>praeclarum</Authors>
<Description>Small cross-platform UI library for .NET that uses web technologies.</Description>
<PackageTags>UI;CrossPlatform</PackageTags>
<PackageTags>Ooui;UI;CrossPlatform</PackageTags>
<PackageIconUrl>https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>

30
Ooui/Option.cs Normal file
View File

@ -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")
{
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)

View File

@ -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) : "";
}

17
Ooui/TextInput.cs Normal file
View File

@ -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)
{
}
}
}

View File

@ -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<string, RequestHandler> 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; } = $@"<!DOCTYPE html>
<html>
@ -35,10 +40,14 @@ namespace Ooui
<title>@Title</title>
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />
<link rel=""stylesheet"" href=""https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css"" />
<style>@Styles</style>
</head>
<body>
<div id=""ooui-body"" class=""container-fluid""></div>
<script type=""text/javascript"" src=""https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js""></script>
<script type=""text/javascript"" src=""https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js""></script>
<script src=""/ooui.js""></script>
<script>ooui(""@WebSocketPath"");</script>
</body>
@ -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<object> 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<HttpListenerContext, CancellationToken> 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<string> createdIds;
readonly List<Message> queuedMessages = new List<Message> ();
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<Message> ();
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<byte> (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<byte> (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);

View File

@ -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 ();
};

View File

@ -1,6 +1,10 @@
# Ooui
# Ooui Web Framework <img src="https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png" height="32"> [![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";
};

13
Samples/BugSweeper/App.cs Executable file
View File

@ -0,0 +1,13 @@
using System;
using Xamarin.Forms;
namespace BugSweeper
{
public class App : Application
{
public App ()
{
MainPage = new BugSweeperPage();
}
}
}

235
Samples/BugSweeper/Board.cs Executable file
View File

@ -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<bool> 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<int, int> 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;
}
}
}
}

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:BugSweeper"
x:Class="BugSweeper.BugSweeperPage"
Title="BugSweeper">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness" iOS="0, 20, 0, 0" Android="0, 0, 0, 0" WinPhone="0, 0, 0, 0" />
</ContentPage.Padding>
<ContentView SizeChanged="OnMainContentViewSizeChanged">
<Grid x:Name="mainGrid" ColumnSpacing="0" RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="7*" />
<RowDefinition Height="4*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout x:Name="textStack" Grid.Row="0" Grid.Column="1" Spacing="0">
<StackLayout HorizontalOptions="Center" Spacing="0">
<Label Text="BugSweeper" Font="Bold, Large" TextColor="Accent" />
<BoxView Color="Accent" HeightRequest="3" />
</StackLayout>
<Label Text="Tap to flag/unflag a potential bug." VerticalOptions="CenterAndExpand" HorizontalTextAlignment="Center" />
<Label Text="Double-tap if you're sure it's not a bug.&#xA;The first double-tap is always safe!" VerticalOptions="CenterAndExpand" HorizontalTextAlignment="Center" />
<StackLayout Orientation="Horizontal" Spacing="0" VerticalOptions="CenterAndExpand" HorizontalOptions="Center">
<Label BindingContext="{x:Reference board}" Text="{Binding FlaggedTileCount, StringFormat='Flagged {0} '}" />
<Label BindingContext="{x:Reference board}" Text="{Binding BugCount, StringFormat=' out of {0} bugs.'}" />
</StackLayout>
<!-- Make this a binding??? -->
<Label x:Name="timeLabel" Text="0:00" VerticalOptions="CenterAndExpand" HorizontalTextAlignment="Center" />
</StackLayout>
<ContentView Grid.Row="1" Grid.Column="1" SizeChanged="OnBoardContentViewSizeChanged">
<!-- Single-cell Grid for Board and overlays. -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:Board x:Name="board" />
<StackLayout x:Name="congratulationsText" Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="Center" Spacing="0">
<Label Text="C" TextColor="Red" />
<Label Text="O" TextColor="Red" />
<Label Text="N" TextColor="Red" />
<Label Text="G" TextColor="Red" />
<Label Text="R" TextColor="Red" />
<Label Text="A" TextColor="Red" />
<Label Text="T" TextColor="Red" />
<Label Text="U" TextColor="Red" />
<Label Text="L" TextColor="Red" />
<Label Text="A" TextColor="Red" />
<Label Text="T" TextColor="Red" />
<Label Text="I" TextColor="Red" />
<Label Text="O" TextColor="Red" />
<Label Text="N" TextColor="Red" />
<Label Text="S" TextColor="Red" />
<Label Text="!" TextColor="Red" />
</StackLayout>
<StackLayout x:Name="consolationText" Orientation="Horizontal" Spacing="0" HorizontalOptions="Center" VerticalOptions="Center">
<Label Text="T" TextColor="Red" />
<Label Text="O" TextColor="Red" />
<Label Text="O" TextColor="Red" />
<Label Text=" " TextColor="Red" />
<Label Text="B" TextColor="Red" />
<Label Text="A" TextColor="Red" />
<Label Text="D" TextColor="Red" />
<Label Text="!" TextColor="Red" />
</StackLayout>
<Button x:Name="playAgainButton" Text=" Play Another Game? " HorizontalOptions="Center" VerticalOptions="Center" Clicked="OnplayAgainButtonClicked"
BorderColor="Black" BorderWidth="2" BackgroundColor="White" TextColor="Black" />
</Grid>
</ContentView>
</Grid>
</ContentView>
</ContentPage>

View File

@ -0,0 +1,174 @@
#define FIX_WINPHONE_BUTTON // IsEnabled = false doesn't disable button
#pragma warning disable 4014 // for non-await'ed async call
using System;
using System.Linq;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace BugSweeper
{
public partial class BugSweeperPage : ContentPage
{
const string timeFormat = @"%m\:ss";
bool isGameInProgress;
DateTime gameStartTime;
public BugSweeperPage()
{
InitializeComponent();
board.GameStarted += (sender, args) =>
{
isGameInProgress = true;
gameStartTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
timeLabel.Text = (DateTime.Now - gameStartTime).ToString(timeFormat);
return isGameInProgress;
});
};
board.GameEnded += (sender, hasWon) =>
{
isGameInProgress = false;
if (hasWon)
{
DisplayWonAnimation();
}
else
{
DisplayLostAnimation();
}
};
PrepareForNewGame();
}
void PrepareForNewGame()
{
board.NewGameInitialize();
congratulationsText.IsVisible = false;
consolationText.IsVisible = false;
playAgainButton.IsVisible = false;
playAgainButton.IsEnabled = false;
timeLabel.Text = new TimeSpan().ToString(timeFormat);
isGameInProgress = false;
}
void OnMainContentViewSizeChanged(object sender, EventArgs args)
{
ContentView contentView = (ContentView)sender;
double width = contentView.Width;
double height = contentView.Height;
bool isLandscape = width > height;
if (isLandscape)
{
mainGrid.RowDefinitions[0].Height = 0;
mainGrid.RowDefinitions[1].Height = new GridLength(1, GridUnitType.Star);
mainGrid.ColumnDefinitions[0].Width = new GridLength(1, GridUnitType.Star);
mainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
Grid.SetRow(textStack, 1);
Grid.SetColumn(textStack, 0);
}
else // portrait
{
mainGrid.RowDefinitions[0].Height = new GridLength(3, GridUnitType.Star);
mainGrid.RowDefinitions[1].Height = new GridLength(5, GridUnitType.Star);
mainGrid.ColumnDefinitions[0].Width = 0;
mainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
Grid.SetRow(textStack, 0);
Grid.SetColumn(textStack, 1);
}
}
// Maintains a square aspect ratio for the board.
void OnBoardContentViewSizeChanged(object sender, EventArgs args)
{
ContentView contentView = (ContentView)sender;
double width = contentView.Width;
double height = contentView.Height;
double dimension = Math.Min(width, height);
double horzPadding = (width - dimension) / 2;
double vertPadding = (height - dimension) / 2;
contentView.Padding = new Thickness(horzPadding, vertPadding);
}
async void DisplayWonAnimation()
{
congratulationsText.Scale = 0;
congratulationsText.IsVisible = true;
// Because IsVisible has been false, the text might not have a size yet,
// in which case Measure will return a size.
double congratulationsTextWidth = congratulationsText.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
congratulationsText.Rotation = 0;
congratulationsText.RotateTo(3 * 360, 1000, Easing.CubicOut);
double maxScale = 0.9 * board.Width / congratulationsTextWidth;
await congratulationsText.ScaleTo(maxScale, 1000);
foreach (View view in congratulationsText.Children)
{
view.Rotation = 0;
view.RotateTo(180);
await view.ScaleTo(3, 100);
view.RotateTo(360);
await view.ScaleTo(1, 100);
}
await DisplayPlayAgainButton();
}
async void DisplayLostAnimation()
{
consolationText.Scale = 0;
consolationText.IsVisible = true;
// (See above for rationale)
double consolationTextWidth = consolationText.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
double maxScale = 0.9 * board.Width / consolationTextWidth;
await consolationText.ScaleTo(maxScale, 1000);
await Task.Delay(1000);
await DisplayPlayAgainButton();
}
async Task DisplayPlayAgainButton()
{
playAgainButton.Scale = 0;
playAgainButton.IsVisible = true;
playAgainButton.IsEnabled = true;
// (See above for rationale)
double playAgainButtonWidth = playAgainButton.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
double maxScale = board.Width / playAgainButtonWidth;
await playAgainButton.ScaleTo(maxScale, 1000, Easing.SpringOut);
}
void OnplayAgainButtonClicked(object sender, object EventArgs)
{
#if FIX_WINPHONE_BUTTON
if (Device.OS == TargetPlatform.WinPhone && !((Button)sender).IsEnabled)
return;
#endif
PrepareForNewGame();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

191
Samples/BugSweeper/Tile.cs Executable file
View File

@ -0,0 +1,191 @@
#define FIX_WINDOWS_DOUBLE_TAPS // Double-taps don't work well on Windows Runtime as of 2.3.0
#define FIX_WINDOWS_PHONE_NULL_CONTENT // Set Content of Frame to null doesn't work in Windows as of 2.3.0
using System;
using Xamarin.Forms;
namespace BugSweeper
{
enum TileStatus
{
Hidden,
Flagged,
Exposed
}
class Tile : Frame
{
TileStatus tileStatus = TileStatus.Hidden;
Label label;
Image flagImage, bugImage;
static ImageSource flagImageSource;
static ImageSource bugImageSource;
bool doNotFireEvent;
public event EventHandler<TileStatus> TileStatusChanged;
static Tile()
{
flagImageSource = ImageSource.FromResource("Samples.BugSweeper.Images.Xamarin120.png", System.Reflection.Assembly.GetCallingAssembly());
bugImageSource = ImageSource.FromResource("Samples.BugSweeper.Images.RedBug.png", System.Reflection.Assembly.GetCallingAssembly ());
}
public Tile(int row, int col)
{
this.Row = row;
this.Col = col;
this.BackgroundColor = Color.Yellow;
this.OutlineColor = Color.Blue;
this.Padding = 2;
label = new Label {
Text = " ",
TextColor = Color.Yellow,
BackgroundColor = Color.Blue,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
};
flagImage = new Image {
Source = flagImageSource,
};
bugImage = new Image {
Source = bugImageSource
};
TapGestureRecognizer singleTap = new TapGestureRecognizer {
NumberOfTapsRequired = 1
};
singleTap.Tapped += OnSingleTap;
this.GestureRecognizers.Add(singleTap);
#if FIX_WINDOWS_DOUBLE_TAPS
if (Device.OS != TargetPlatform.Windows && Device.OS != TargetPlatform.WinPhone) {
#endif
TapGestureRecognizer doubleTap = new TapGestureRecognizer {
NumberOfTapsRequired = 2
};
doubleTap.Tapped += OnDoubleTap;
this.GestureRecognizers.Add(doubleTap);
#if FIX_WINDOWS_DOUBLE_TAPS
}
#endif
}
public int Row { private set; get; }
public int Col { private set; get; }
public bool IsBug { set; get; }
public int SurroundingBugCount { set; get; }
public TileStatus Status {
set {
if (tileStatus != value) {
tileStatus = value;
switch (tileStatus) {
case TileStatus.Hidden:
this.Content = null;
#if FIX_WINDOWS_PHONE_NULL_CONTENT
if (Device.OS == TargetPlatform.WinPhone || Device.OS == TargetPlatform.Windows) {
this.Content = new Label { Text = " " };
}
#endif
break;
case TileStatus.Flagged:
this.Content = flagImage;
break;
case TileStatus.Exposed:
if (this.IsBug) {
this.Content = bugImage;
} else {
this.Content = label;
label.Text =
(this.SurroundingBugCount > 0) ?
this.SurroundingBugCount.ToString() : " ";
}
break;
}
if (!doNotFireEvent && TileStatusChanged != null) {
TileStatusChanged(this, tileStatus);
}
}
}
get {
return tileStatus;
}
}
// Does not fire TileStatusChanged events.
public void Initialize()
{
doNotFireEvent = true;
this.Status = TileStatus.Hidden;
this.IsBug = false;
this.SurroundingBugCount = 0;
doNotFireEvent = false;
}
#if FIX_WINDOWS_DOUBLE_TAPS
bool lastTapSingle;
DateTime lastTapTime;
#endif
void OnSingleTap(object sender, object args)
{
#if FIX_WINDOWS_DOUBLE_TAPS
if (Device.OS == TargetPlatform.Windows || Device.OS == TargetPlatform.WinPhone) {
if (lastTapSingle && DateTime.Now - lastTapTime < TimeSpan.FromMilliseconds (500)) {
OnDoubleTap (sender, args);
lastTapSingle = false;
} else {
lastTapTime = DateTime.Now;
lastTapSingle = true;
}
}
#endif
switch (this.Status) {
case TileStatus.Hidden:
this.Status = TileStatus.Flagged;
break;
case TileStatus.Flagged:
this.Status = TileStatus.Hidden;
break;
case TileStatus.Exposed:
// Do nothing
break;
}
}
void OnDoubleTap (object sender, object args)
{
this.Status = TileStatus.Exposed;
}
}
}

View File

@ -0,0 +1,16 @@
using Ooui;
using Xamarin.Forms;
namespace Samples
{
public class BugSweeperSample : ISample
{
public string Title => "Xamarin.Forms BugSweeper";
public Ooui.Element CreateElement ()
{
var page = new BugSweeper.BugSweeperPage ();
return page.GetOouiElement ();
}
}
}

View File

@ -14,7 +14,7 @@ namespace Samples
};
button.Style.MarginTop = "2em";
var count = 0;
button.Clicked += (s, e) => {
button.Click += (s, e) => {
count++;
button.Text = $"Clicked {count} times";
};

View File

@ -4,10 +4,15 @@
x:Class="Samples.DisplayAlertPage">
<ContentPage.Content>
<StackLayout>
<Label Text="Welcome to DisplayAlert Sample!" FontSize="32" FontAttributes="Bold" Margin="10,10,10,50" />
<Label Text="Welcome to DisplayAlert Sample!" FontSize="32" FontAttributes="Bold" Margin="10,10,10,50"
HorizontalOptions="Center"/>
<ActivityIndicator x:Name="activity" />
<ProgressBar x:Name="progress" />
<DatePicker x:Name="datePicker" />
<StackLayout Orientation="Horizontal">
<Switch x:Name="switch1" IsToggled="true" />
<Switch x:Name="switch2" IsToggled="false" />
</StackLayout>
<Button Text="Tap to Display Alert"
Clicked="OnButtonClicked" />
</StackLayout>

View File

@ -18,15 +18,24 @@ namespace Samples
{
var heading = new Heading ("Draw");
var subtitle = new Paragraph ("Click to draw a masterpiece");
var toolSel = new Select ();
toolSel.AppendChild (new Option { Label = "Boxes", Value = "box" });
toolSel.AddOption ("Circles", "circle");
var canvas = new Canvas {
Width = 320,
Height = 240,
};
var context = canvas.GetContext2D ();
canvas.Clicked += (s, e) => {
canvas.Click += (s, e) => {
var radius = 10;
context.BeginPath ();
context.Rect (e.OffsetX - 5, e.OffsetY - 5, 10, 10);
if (toolSel.Value == "box") {
context.Rect (e.OffsetX - radius, e.OffsetY - radius, 2*radius, 2*radius);
}
else {
context.Arc (e.OffsetX, e.OffsetY, radius, 0, 2 * Math.PI, true);
}
context.Fill ();
};
canvas.Style.Cursor = "pointer";
@ -38,7 +47,7 @@ namespace Samples
Type = ButtonType.Submit,
ClassName = "btn btn-danger",
};
clearbtn.Clicked += (s, e) => {
clearbtn.Click += (s, e) => {
context.ClearRect (0, 0, canvas.Width, canvas.Height);
};
clearbtn.Style.Display = "block";
@ -46,6 +55,7 @@ namespace Samples
var app = new Div ();
app.AppendChild (heading);
app.AppendChild (subtitle);
app.AppendChild (new Div (toolSel));
app.AppendChild (canvas);
app.AppendChild (clearbtn);
return app;

View File

@ -5,7 +5,7 @@ namespace Samples
{
public class EditorSample : ISample
{
public string Title => "Editor Sample";
public string Title => "Xamarin.Forms Editor Sample";
public Ooui.Element CreateElement()
{
@ -13,7 +13,9 @@ namespace Samples
var titleLabel = new Xamarin.Forms.Label
{
Text = "Editor"
Text = "Editor",
FontSize = 24,
FontAttributes = FontAttributes.Bold,
};
panel.Children.Add(titleLabel);

View File

@ -15,6 +15,8 @@
<ItemGroup>
<EmbeddedResource Include="**/*.xaml" />
<EmbeddedResource Include="BugSweeper\Images\RedBug.png" />
<EmbeddedResource Include="BugSweeper\Images\Xamarin120.png" />
</ItemGroup>
<ItemGroup>
@ -41,6 +43,10 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Remove="BugSweeper\Images\RedBug.png" />
<None Remove="BugSweeper\Images\Xamarin120.png" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>

View File

@ -66,19 +66,19 @@ namespace Samples
if (string.IsNullOrWhiteSpace (input.Value))
return;
var item = new Item (input.Value);
item.Clicked += (s, e) => {
item.Click += (s, e) => {
item.IsDone = !item.IsDone;
};
items.InsertBefore (item, items.FirstChild);
input.Value = "";
}
addbtn.Clicked += (s, e) => {
addbtn.Click += (s, e) => {
AddItem ();
};
inputForm.Submitted += (s, e) => {
inputForm.Submit += (s, e) => {
AddItem ();
};
clearbtn.Clicked += (s, e) => {
clearbtn.Click += (s, e) => {
var toremove = new List<Node> ();
foreach (Item i in items.Children) {
if (i.IsDone) toremove.Add (i);

View File

@ -3,6 +3,7 @@
<ContentPage.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
@ -10,8 +11,12 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Editor x:Name="editor" FontFamily="monospace" Grid.Row="0" Grid.Column="0" />
<ContentView x:Name="results" Grid.Row="0" Grid.Column="1" BackgroundColor="White" />
<StackLayout Orientation="Vertical" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<Label Text="Xamarin.Forms XAML Editor" FontSize="24" FontAttributes="Bold" Margin="8,8,8,0" />
<Label Text="Edit the XAML below to see a live preview on the right" Margin="8,0,8,8" />
</StackLayout>
<Editor x:Name="editor" FontFamily="monospace" FontSize="12" Grid.Row="1" Grid.Column="0" />
<ContentView x:Name="results" Grid.Row="1" Grid.Column="1" BackgroundColor="White" />
</Grid>
</ContentPage.Content>
</ContentPage>

View File

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Xamarin.Forms;
namespace Samples
@ -23,24 +23,50 @@ namespace Samples
<ColumnDefinition Width=""*"" />
<ColumnDefinition Width=""*"" />
</Grid.ColumnDefinitions>
<Label Text=""Top Left"" Grid.Row=""0"" Grid.Column=""0"" />
<Label Text=""Top Right"" Grid.Row=""0"" Grid.Column=""1"" />
<Label Text=""Bottom Left"" Grid.Row=""1"" Grid.Column=""0"" />
<Label Text=""Bottom Right"" Grid.Row=""1"" Grid.Column=""1"" />
<StackLayout Grid.Row=""0"" Grid.Column=""0"">
<Label Text=""Top Left"" />
<Entry Placeholder=""I'm ready for some text"" />
<Button Text=""I'm a button, but I don't do anything"" />
</StackLayout>
<Label Text=""Top Right"" Grid.Row=""0"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#c5000b"" />
<Label Text=""Bottom Left"" Grid.Row=""1"" Grid.Column=""0"" TextColor=""Black"" BackgroundColor=""#ffd320"" />
<Label Text=""Bottom Right"" Grid.Row=""1"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#008000"" />
</Grid>
</ContentView>";
editor.TextChanged += (sender, e) => DisplayXaml ();
DisplayXaml ();
}
CancellationTokenSource lastCts = null;
public void DisplayXaml ()
{
var asm = typeof (Xamarin.Forms.Xaml.Internals.XamlTypeResolver).Assembly;
var xamlLoaderType = asm.GetType ("Xamarin.Forms.Xaml.XamlLoader");
var loadArgTypes = new[] { typeof (object), typeof (string) };
var loadMethod = xamlLoaderType.GetMethod ("Load", System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.Public, null, System.Reflection.CallingConventions.Any, loadArgTypes, null);
var contentView = new ContentView ();
loadMethod.Invoke (null, new object[] { contentView, editor.Text });
results.Content = contentView;
try {
var cts = new CancellationTokenSource ();
var token = cts.Token;
lastCts?.Cancel ();
lastCts = cts;
var asm = typeof (Xamarin.Forms.Xaml.Internals.XamlTypeResolver).Assembly;
var xamlLoaderType = asm.GetType ("Xamarin.Forms.Xaml.XamlLoader");
var loadArgTypes = new[] { typeof (object), typeof (string) };
var loadMethod = xamlLoaderType.GetMethod ("Load", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, null, System.Reflection.CallingConventions.Any, loadArgTypes, null);
var contentView = new ContentView ();
loadMethod.Invoke (null, new object[] { contentView, editor.Text });
if (!token.IsCancellationRequested) {
results.Content = contentView;
}
}
catch (OperationCanceledException) {
}
catch (Exception ex) {
results.Content = new Label {
TextColor = Color.DarkRed,
FontSize = 12,
Text = ex.ToString (),
};
}
}
}
}

View File

@ -42,7 +42,7 @@ namespace Tests
listened = listened || (m.MessageType == MessageType.Listen);
};
Assert.IsFalse (listened);
b.Clicked += (s, e) => {
b.Click += (s, e) => {
clicked = true;
};
Assert.IsTrue (listened);

View File

@ -52,7 +52,7 @@ namespace Tests
var b = new Button ();
p.AppendChild (b);
var clicked = false;
b.Clicked += (s, e) => {
b.Click += (s, e) => {
clicked = true;
};
p.Receive (Message.Event (b.Id, "click"));

View File

@ -75,7 +75,7 @@ namespace Tests
{
var s = new Style ();
s.BorderLeftWidth = 3.142;
Assert.AreEqual ("border-left-width:3.142", s.ToString ());
Assert.AreEqual ("border-left-width:3.142px", s.ToString ());
}
[TestMethod]

View File

@ -7,7 +7,7 @@ Type=simple
Environment=HOME=/home/ubuntu
WorkingDirectory=/home/ubuntu/Ooui/PlatformSamples/AspNetCoreMvc
ExecStart=/usr/bin/dotnet run --no-build --server.urls=http://0.0.0.0:80/
Restart=on-abort
Restart=always
[Install]
WantedBy=multi-user.target