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"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <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> <TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup> </PropertyGroup>

View File

@ -25,19 +25,29 @@ namespace Microsoft.AspNetCore.Builder
}; };
app.UseWebSockets (webSocketOptions); app.UseWebSockets (webSocketOptions);
Ooui.UI.ServerEnabled = false;
app.Use (async (context, next) => app.Use (async (context, next) =>
{ {
if (context.Request.Path == jsPath) {
var response = context.Response; var response = context.Response;
if (context.Request.Path == jsPath) {
var clientJsBytes = Ooui.UI.ClientJsBytes; var clientJsBytes = Ooui.UI.ClientJsBytes;
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.StatusCode = 200;
response.ContentLength = clientJsBytes.Length; response.ContentLength = clientJsBytes.Length;
response.ContentType = "application/javascript; charset=utf-8"; response.ContentType = "application/javascript; charset=utf-8";
response.Headers.Add ("Cache-Control", "public, max-age=3600"); response.Headers.Add ("Cache-Control", "public, max-age=60");
response.Headers.Add ("Etag", clientJsEtag);
using (var s = response.Body) { using (var s = response.Body) {
await s.WriteAsync (clientJsBytes, 0, clientJsBytes.Length).ConfigureAwait (false); await s.WriteAsync (clientJsBytes, 0, clientJsBytes.Length).ConfigureAwait (false);
} }
} }
}
else if (context.Request.Path == WebSocketHandler.WebSocketPath) { else if (context.Request.Path == WebSocketHandler.WebSocketPath) {
if (context.WebSockets.IsWebSocketRequest) { if (context.WebSockets.IsWebSocketRequest) {
await WebSocketHandler.HandleWebSocketRequestAsync (context).ConfigureAwait (false); await WebSocketHandler.HandleWebSocketRequestAsync (context).ConfigureAwait (false);
@ -46,6 +56,21 @@ namespace Microsoft.AspNetCore.Builder
context.Response.StatusCode = 400; 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 { else {
await next ().ConfigureAwait (false); await next ().ConfigureAwait (false);
} }

View File

@ -86,9 +86,10 @@ namespace Ooui.AspNetCore
BadRequest ("Missing `h`"); BadRequest ("Missing `h`");
return; 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; 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; h = 480;
// //

View File

@ -59,7 +59,7 @@ namespace Ooui.Forms
{ {
ClassName = "btn btn-default" ClassName = "btn btn-default"
}; };
_cancelButton.Clicked += (s, e) => SetResult(false); _cancelButton.Click += (s, e) => SetResult(false);
footer.AppendChild(_cancelButton); footer.AppendChild(_cancelButton);
@ -70,7 +70,7 @@ namespace Ooui.Forms
ClassName = "btn btn-default" ClassName = "btn btn-default"
}; };
_acceptButton.Clicked += (s, e) => SetResult(true); _acceptButton.Click += (s, e) => SetResult(true);
footer.AppendChild(_acceptButton); footer.AppendChild(_acceptButton);
} }
@ -90,23 +90,23 @@ namespace Ooui.Forms
{ {
add add
{ {
_closeButton.Clicked += value; _closeButton.Click += value;
if(_cancelButton != null) if(_cancelButton != null)
_cancelButton.Clicked += value; _cancelButton.Click += value;
if(_acceptButton != null) if(_acceptButton != null)
_acceptButton.Clicked += value; _acceptButton.Click += value;
} }
remove remove
{ {
_closeButton.Clicked -= value; _closeButton.Click -= value;
if (_cancelButton != null) if (_cancelButton != null)
_cancelButton.Clicked -= value; _cancelButton.Click -= value;
if (_acceptButton != null) if (_acceptButton != null)
_acceptButton.Clicked -= value; _acceptButton.Click -= value;
} }
} }
public Element Element { get; private set; } 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 (DatePicker), typeof (DatePickerRenderer))]
[assembly: ExportRenderer (typeof (Editor), typeof (EditorRenderer))] [assembly: ExportRenderer (typeof (Editor), typeof (EditorRenderer))]
[assembly: ExportRenderer (typeof (Entry), typeof (EntryRenderer))] [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 (Label), typeof (LabelRenderer))]
[assembly: ExportRenderer (typeof (ProgressBar), typeof (ProgressBarRenderer))] [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 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")) { if (self.Style.Width.Equals ("inherit")) {
s = self.Text.MeasureSize (self.Style); s = self.Text.MeasureSize (self.Style);
measured = true; measured = true;
rw = double.IsPositiveInfinity (s.Width) ? double.PositiveInfinity : s.Width; rw = double.IsPositiveInfinity (s.Width) ? double.PositiveInfinity : Math.Ceiling (s.Width);
} }
else { else {
rw = self.Style.GetNumberWithUnits ("width", "px", 640); rw = self.Style.GetNumberWithUnits ("width", "px", 640);
@ -27,7 +27,7 @@ namespace Ooui.Forms.Extensions
s = self.Text.MeasureSize (self.Style); s = self.Text.MeasureSize (self.Style);
measured = true; 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 { else {
rh = self.Style.GetNumberWithUnits ("height", "px", 480); rh = self.Style.GetNumberWithUnits ("height", "px", 480);

View File

@ -44,9 +44,23 @@ namespace Ooui.Forms.Extensions
return Size.Zero; return Size.Zero;
var fontHeight = fontSize; 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); 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,12 +26,12 @@ namespace Xamarin.Forms
Device.SetIdiom (TargetIdiom.Desktop); Device.SetIdiom (TargetIdiom.Desktop);
Device.PlatformServices = new OouiPlatformServices (); Device.PlatformServices = new OouiPlatformServices ();
Device.Info = new OouiDeviceInfo (); Device.Info = new OouiDeviceInfo ();
Color.SetAccent (Color.FromHex ("#0000EE")); // Safari Blue Color.SetAccent (Color.FromHex ("#337ab7")); // Bootstrap Blue
Registrar.RegisterAll (new[] { Registrar.RegisterAll (new[] {
typeof(ExportRendererAttribute), typeof(ExportRendererAttribute),
//typeof(ExportCellAttribute), //typeof(ExportCellAttribute),
//typeof(ExportImageSourceHandlerAttribute), typeof(ExportImageSourceHandlerAttribute),
}); });
} }
@ -62,11 +62,6 @@ namespace Xamarin.Forms
Task.Run (action); Task.Run (action);
} }
public Ticker CreateTicker ()
{
throw new NotImplementedException ();
}
public Assembly[] GetAssemblies () public Assembly[] GetAssemblies ()
{ {
return AppDomain.CurrentDomain.GetAssemblies (); return AppDomain.CurrentDomain.GetAssemblies ();
@ -119,6 +114,31 @@ namespace Xamarin.Forms
} }
}), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds); }), 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 public class ViewInitializedEventArgs

View File

@ -1,6 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <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> <TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup> </PropertyGroup>

View File

@ -57,6 +57,10 @@ namespace Ooui.Forms
MessagingCenter.Unsubscribe<Page, ActionSheetArguments> (this, Page.ActionSheetSignalName); MessagingCenter.Unsubscribe<Page, ActionSheetArguments> (this, Page.ActionSheetSignalName);
MessagingCenter.Unsubscribe<Page, AlertArguments> (this, Page.AlertSignalName); MessagingCenter.Unsubscribe<Page, AlertArguments> (this, Page.AlertSignalName);
MessagingCenter.Unsubscribe<Page, bool> (this, Page.BusySetSignalName); MessagingCenter.Unsubscribe<Page, bool> (this, Page.BusySetSignalName);
DisposeModelAndChildrenRenderers (Page);
//foreach (var modal in _modals)
//DisposeModelAndChildrenRenderers (modal);
} }
public static IVisualElementRenderer CreateRenderer (VisualElement element) public static IVisualElementRenderer CreateRenderer (VisualElement element)
@ -110,7 +114,29 @@ namespace Ooui.Forms
void HandleChildRemoved (object sender, ElementEventArgs e) 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) void AddChild (VisualElement view)

View File

@ -15,5 +15,19 @@ namespace Ooui.Forms
{ {
this.platform = platform; 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) protected override void Dispose (bool disposing)
{ {
if (Control != null) { if (Control != null) {
Control.Clicked -= OnButtonTouchUpInside; Control.Click -= OnButtonTouchUpInside;
} }
base.Dispose (disposing); base.Dispose (disposing);
@ -44,7 +44,7 @@ namespace Ooui.Forms.Renderers
_buttonTextColorDefaultHighlighted = Ooui.Colors.Black; _buttonTextColorDefaultHighlighted = Ooui.Colors.Black;
_buttonTextColorDefaultDisabled = Ooui.Colors.Black; _buttonTextColorDefaultDisabled = Ooui.Colors.Black;
Control.Clicked += OnButtonTouchUpInside; Control.Click += OnButtonTouchUpInside;
} }
UpdateText (); UpdateText ();

View File

@ -31,8 +31,8 @@ namespace Ooui.Forms.Renderers
Type = InputType.Date, Type = InputType.Date,
}; };
entry.Inputted += OnStarted; //entry.Input += OnStarted;
entry.Changed += OnEnded; entry.Change += OnEnded;
SetNativeControl (entry); SetNativeControl (entry);
} }
@ -100,8 +100,8 @@ namespace Ooui.Forms.Renderers
if (disposing) { if (disposing) {
if (Control != null) { if (Control != null) {
Control.Inputted -= OnStarted; //Control.Input -= OnStarted;
Control.Changed -= OnEnded; Control.Change -= OnEnded;
} }
} }

View File

@ -10,6 +10,12 @@ namespace Ooui.Forms.Renderers
bool _disposed; bool _disposed;
IEditorController ElementController => Element; 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) protected override void Dispose (bool disposing)
{ {
if (_disposed) if (_disposed)
@ -19,9 +25,9 @@ namespace Ooui.Forms.Renderers
if (disposing) { if (disposing) {
if (Control != null) { if (Control != null) {
Control.Changed -= HandleChanged; Control.Input -= HandleChanged;
//Control.Started -= OnStarted; //Control.Started -= OnStarted;
//Control.Ended -= OnEnded; Control.Change -= OnEnded;
} }
} }
@ -40,9 +46,9 @@ namespace Ooui.Forms.Renderers
ClassName = "form-control" ClassName = "form-control"
}); });
Control.Changed += HandleChanged; Control.Input += HandleChanged;
//Control.Started += OnStarted; //Control.Started += OnStarted;
//Control.Ended += OnEnded; Control.Change += OnEnded;
} }
UpdateText (); UpdateText ();
@ -75,13 +81,13 @@ namespace Ooui.Forms.Renderers
void HandleChanged (object sender, EventArgs e) void HandleChanged (object sender, EventArgs e)
{ {
ElementController.SetValueFromRenderer (Editor.TextProperty, Control.Text); ElementController.SetValueFromRenderer (Editor.TextProperty, Control.Value);
} }
void OnEnded (object sender, EventArgs eventArgs) void OnEnded (object sender, EventArgs eventArgs)
{ {
if (Control.Text != Element.Text) if (Control.Value != Element.Text)
ElementController.SetValueFromRenderer (Editor.TextProperty, Control.Text); ElementController.SetValueFromRenderer (Editor.TextProperty, Control.Value);
Element.SetValue (VisualElement.IsFocusedPropertyKey, false); Element.SetValue (VisualElement.IsFocusedPropertyKey, false);
ElementController.SendCompleted (); ElementController.SendCompleted ();
@ -108,8 +114,8 @@ namespace Ooui.Forms.Renderers
void UpdateText () void UpdateText ()
{ {
if (Control.Text != Element.Text) if (Control.Value != Element.Text)
Control.Text = Element.Text; Control.Value = Element.Text;
} }
void UpdateTextAlignment () void UpdateTextAlignment ()

View File

@ -6,7 +6,7 @@ using Xamarin.Forms;
namespace Ooui.Forms.Renderers namespace Ooui.Forms.Renderers
{ {
public class EntryRenderer : ViewRenderer<Entry, Ooui.Input> public class EntryRenderer : ViewRenderer<Entry, Ooui.TextInput>
{ {
Ooui.Color _defaultTextColor; Ooui.Color _defaultTextColor;
bool _disposed; bool _disposed;
@ -17,7 +17,17 @@ namespace Ooui.Forms.Renderers
public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) 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); size = new Size (size.Width, size.Height * 1.428 + 14);
return new SizeRequest (size, size); return new SizeRequest (size, size);
} }
@ -32,8 +42,8 @@ namespace Ooui.Forms.Renderers
if (disposing) { if (disposing) {
if (Control != null) { if (Control != null) {
//Control.Inputted -= OnEditingBegan; //Control.Inputted -= OnEditingBegan;
Control.Inputted -= OnEditingChanged; Control.Input -= OnEditingChanged;
Control.Changed -= OnEditingEnded; Control.Change -= OnEditingEnded;
} }
} }
@ -48,7 +58,7 @@ namespace Ooui.Forms.Renderers
return; return;
if (Control == null) { if (Control == null) {
var textField = new Ooui.Input (InputType.Text); var textField = new Ooui.TextInput ();
SetNativeControl (textField); SetNativeControl (textField);
Debug.Assert (Control != null, "Control != null"); Debug.Assert (Control != null, "Control != null");
@ -57,10 +67,10 @@ namespace Ooui.Forms.Renderers
_defaultTextColor = Colors.Black; _defaultTextColor = Colors.Black;
textField.Inputted += OnEditingChanged; textField.Input += OnEditingChanged;
//textField.EditingDidBegin += OnEditingBegan; //textField.EditingDidBegin += OnEditingBegan;
textField.Changed += OnEditingEnded; textField.Change += OnEditingEnded;
} }
UpdatePlaceholder (); UpdatePlaceholder ();
@ -113,8 +123,8 @@ namespace Ooui.Forms.Renderers
void OnEditingEnded (object sender, EventArgs e) void OnEditingEnded (object sender, EventArgs e)
{ {
// Typing aid changes don't always raise EditingChanged event // Typing aid changes don't always raise EditingChanged event
if (Control.Text != Element.Text) { if (Control.Value != Element.Text) {
ElementController.SetValueFromRenderer (Entry.TextProperty, Control.Text); ElementController.SetValueFromRenderer (Entry.TextProperty, Control.Value);
} }
ElementController.SetValueFromRenderer (VisualElement.IsFocusedPropertyKey, false); 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) { if (!_perfectSizeValid) {
var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); 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); _perfectSize = new SizeRequest (size, size);
_perfectSizeValid = true; _perfectSizeValid = true;
} }
@ -72,6 +74,9 @@ namespace Ooui.Forms.Renderers
{ {
base.OnElementPropertyChanged (sender, e); base.OnElementPropertyChanged (sender, e);
if (Control == null)
return;
if (e.PropertyName == Xamarin.Forms.Label.HorizontalTextAlignmentProperty.PropertyName) if (e.PropertyName == Xamarin.Forms.Label.HorizontalTextAlignmentProperty.PropertyName)
UpdateAlignment (); UpdateAlignment ();
else if (e.PropertyName == Xamarin.Forms.Label.VerticalTextAlignmentProperty.PropertyName) else if (e.PropertyName == Xamarin.Forms.Label.VerticalTextAlignmentProperty.PropertyName)
@ -98,8 +103,10 @@ namespace Ooui.Forms.Renderers
void UpdateAlignment () void UpdateAlignment ()
{ {
Control.Style.TextAlign = Element.HorizontalTextAlignment.ToOouiTextAlign (); this.Style.Display = "table";
Control.Style.VerticalAlign = Element.VerticalTextAlignment.ToOouiTextAlign (); Control.Style.Display = "table-cell";
this.Style.TextAlign = Element.HorizontalTextAlignment.ToOouiTextAlign ();
Control.Style.VerticalAlign = Element.VerticalTextAlignment.ToOouiVerticalAlign ();
} }
void UpdateLineBreakMode () 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; VisualElementRendererFlags _flags = VisualElementRendererFlags.AutoPackage | VisualElementRendererFlags.AutoTrack;
EventTracker _events;
VisualElementPackager _packager; VisualElementPackager _packager;
VisualElementTracker _tracker; VisualElementTracker _tracker;
@ -107,10 +108,10 @@ namespace Ooui.Forms
_packager.Load (); _packager.Load ();
} }
//if (AutoTrack && _events == null) { if (AutoTrack && _events == null) {
// _events = new EventTracker (this); _events = new EventTracker (this);
// _events.LoadEvents (this); _events.LoadEvents (this);
//} }
element.PropertyChanged += _propertyChangedHandler; element.PropertyChanged += _propertyChangedHandler;
} }

View File

@ -81,97 +81,97 @@ namespace Ooui
public void Save () public void Save ()
{ {
SendCall ("save"); Call ("save");
} }
public void Restore () public void Restore ()
{ {
SendCall ("restore"); Call ("restore");
} }
public void ClearRect (double x, double y, double w, double h) 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) 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) 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 () public void BeginPath ()
{ {
SendCall ("beginPath"); Call ("beginPath");
} }
public void ClosePath () public void ClosePath ()
{ {
SendCall ("closePath"); Call ("closePath");
} }
public void MoveTo (double x, double y) public void MoveTo (double x, double y)
{ {
SendCall ("moveTo", x, y); Call ("moveTo", x, y);
} }
public void LineTo (double x, double 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) 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) 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) 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) 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) 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 () public void Fill ()
{ {
SendCall ("fill"); Call ("fill");
} }
public void Stroke () public void Stroke ()
{ {
SendCall ("stroke"); Call ("stroke");
} }
public void Clip () public void Clip ()
{ {
SendCall ("clip"); Call ("clip");
} }
public void FillText (string text, double x, double y, double? maxWidth) 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) 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) { socket.addEventListener ("close", function (event) {
console.error ("Web socket close", event); console.error ("Web socket close", event);
if (opened) { 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); const messages = JSON.parse (event.data);
if (debug) console.log("Messages", messages); if (debug) console.log("Messages", messages);
if (Array.isArray (messages)) { if (Array.isArray (messages)) {
const jqs = []
messages.forEach (function (m) { messages.forEach (function (m) {
// console.log('Raw value from server', m.v); // console.log('Raw value from server', m.v);
m.v = fixupValue (m.v); m.v = fixupValue (m.v);
if (m.k.startsWith ("$.")) {
jqs.push (m);
}
else {
processMessage (m); 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() { function resizeHandler() {
const em = { const em = {
m: "event", m: "event",
id: 42, id: "window",
k: "window.resize", k: "resize",
v: getSize (), v: getSize (),
}; };
const ems = JSON.stringify (em); const ems = JSON.stringify (em);
@ -163,9 +172,11 @@ function msgCall (m) {
console.error ("Unknown node id", m); console.error ("Unknown node id", m);
return; 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); 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) { if (typeof m.rid === 'string' || m.rid instanceof String) {
nodes[m.rid] = r; nodes[m.rid] = r;
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace Ooui namespace Ooui
{ {
@ -8,5 +9,21 @@ namespace Ooui
: base ("div") : 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"); set => SetProperty (ref hidden, value, "hidden");
} }
public event TargetEventHandler Clicked { public event TargetEventHandler Click {
add => AddEventListener ("click", value); add => AddEventListener ("click", value);
remove => RemoveEventListener ("click", value); remove => RemoveEventListener ("click", value);
} }
public event TargetEventHandler DoubleClicked { public event TargetEventHandler DoubleClick {
add => AddEventListener ("dblclick", value); add => AddEventListener ("dblclick", value);
remove => RemoveEventListener ("dblclick", value); remove => RemoveEventListener ("dblclick", value);
} }
@ -40,7 +40,7 @@ namespace Ooui
remove => RemoveEventListener ("keydown", value); remove => RemoveEventListener ("keydown", value);
} }
public event TargetEventHandler KeyPressed { public event TargetEventHandler KeyPress {
add => AddEventListener ("keypress", value); add => AddEventListener ("keypress", value);
remove => RemoveEventListener ("keypress", value); remove => RemoveEventListener ("keypress", value);
} }
@ -55,17 +55,17 @@ namespace Ooui
remove => RemoveEventListener ("mousedown", value); remove => RemoveEventListener ("mousedown", value);
} }
public event TargetEventHandler MouseEntered { public event TargetEventHandler MouseEnter {
add => AddEventListener ("mouseenter", value); add => AddEventListener ("mouseenter", value);
remove => RemoveEventListener ("mouseenter", value); remove => RemoveEventListener ("mouseenter", value);
} }
public event TargetEventHandler MouseLeft { public event TargetEventHandler MouseLeave {
add => AddEventListener ("mouseleave", value); add => AddEventListener ("mouseleave", value);
remove => RemoveEventListener ("mouseleave", value); remove => RemoveEventListener ("mouseleave", value);
} }
public event TargetEventHandler MouseMoved { public event TargetEventHandler MouseMove {
add => AddEventListener ("mousemove", value); add => AddEventListener ("mousemove", value);
remove => RemoveEventListener ("mousemove", value); remove => RemoveEventListener ("mousemove", value);
} }
@ -85,7 +85,7 @@ namespace Ooui
remove => RemoveEventListener ("mouseup", value); remove => RemoveEventListener ("mouseup", value);
} }
public event TargetEventHandler Wheeled { public event TargetEventHandler Wheel {
add => AddEventListener ("wheel", value); add => AddEventListener ("wheel", value);
remove => RemoveEventListener ("wheel", value); remove => RemoveEventListener ("wheel", value);
} }
@ -116,5 +116,19 @@ namespace Ooui
{ {
SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]); 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); 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)); Send (Message.Call (Id, methodName, args));
} }

View File

@ -22,7 +22,7 @@ namespace Ooui
set => SetProperty (ref enctype, value ?? "", "enctype"); set => SetProperty (ref enctype, value ?? "", "enctype");
} }
public event TargetEventHandler Submitted { public event TargetEventHandler Submit {
add => AddEventListener ("submit", value); add => AddEventListener ("submit", value);
remove => RemoveEventListener ("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); add => AddEventListener ("change", value);
remove => RemoveEventListener ("change", value); remove => RemoveEventListener ("change", value);
} }
public event TargetEventHandler Inputted {
add => AddEventListener ("input", value);
remove => RemoveEventListener ("input", value);
}
string placeholder = ""; string placeholder = "";
public string Placeholder { public string Placeholder {
get => placeholder; get => placeholder;
@ -77,7 +72,7 @@ namespace Ooui
: base ("input") : base ("input")
{ {
// Subscribe to the change event so we always get up-to-date values // Subscribe to the change event so we always get up-to-date values
Changed += (s, e) => {}; Change += (s, e) => {};
} }
public Input (InputType type) public Input (InputType type)
@ -88,7 +83,7 @@ namespace Ooui
protected override bool TriggerEventFromMessage (Message message) 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 // Don't need to notify here because the base implementation will fire the event
if (Type == InputType.Checkbox) { if (Type == InputType.Checkbox) {
isChecked = message.Value != null ? Convert.ToBoolean (message.Value) : false; isChecked = message.Value != null ? Convert.ToBoolean (message.Value) : false;

View File

@ -73,7 +73,8 @@ namespace Ooui
} }
} }
newChild.MessageSent += HandleChildMessageSent; newChild.MessageSent += HandleChildMessageSent;
SendCall ("insertBefore", newChild, referenceChild); Call ("insertBefore", newChild, referenceChild);
OnChildInsertedBefore (newChild, referenceChild);
return newChild; return newChild;
} }
@ -87,10 +88,19 @@ namespace Ooui
} }
} }
child.MessageSent -= HandleChildMessageSent; child.MessageSent -= HandleChildMessageSent;
SendCall ("removeChild", child); Call ("removeChild", child);
OnChildRemoved (child);
return child; return child;
} }
protected virtual void OnChildInsertedBefore (Node newChild, Node referenceChild)
{
}
protected virtual void OnChildRemoved (Node child)
{
}
protected void ReplaceAll (Node newNode) protected void ReplaceAll (Node newNode)
{ {
var toRemove = new List<Node> (); var toRemove = new List<Node> ();
@ -100,7 +110,7 @@ namespace Ooui
} }
foreach (var child in toRemove) { foreach (var child in toRemove) {
child.MessageSent -= HandleChildMessageSent; child.MessageSent -= HandleChildMessageSent;
SendCall ("removeChild", child); Call ("removeChild", child);
} }
InsertBefore (newNode, null); InsertBefore (newNode, null);
} }

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<Version>0.2.0</Version> <Version>1.0.0</Version>
<Authors>praeclarum</Authors> <Authors>praeclarum</Authors>
<Description>Small cross-platform UI library for .NET that uses web technologies.</Description> <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> <PackageIconUrl>https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl> <PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl> <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"); set => SetProperty (ref val, value ?? "", "value");
} }
public event TargetEventHandler Changed { public event TargetEventHandler Change {
add => AddEventListener ("change", value); add => AddEventListener ("change", value);
remove => RemoveEventListener ("change", value); remove => RemoveEventListener ("change", value);
} }
public event TargetEventHandler Input {
add => AddEventListener ("input", value);
remove => RemoveEventListener ("input", value);
}
public Select () public Select ()
: base ("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 { public Value BorderTopWidth {
get => this["border-top-width"]; get => this["border-top-width"];
set => this["border-top-width"] = value; set => this["border-top-width"] = AddNumberUnits (value, "px");
} }
public Value BorderRightWidth { public Value BorderRightWidth {
get => this["border-right-width"]; get => this["border-right-width"];
set => this["border-right-width"] = value; set => this["border-right-width"] = AddNumberUnits (value, "px");
} }
public Value BorderBottomWidth { public Value BorderBottomWidth {
get => this["border-bottom-width"]; get => this["border-bottom-width"];
set => this["border-bottom-width"] = value; set => this["border-bottom-width"] = AddNumberUnits (value, "px");
} }
public Value BorderLeftWidth { public Value BorderLeftWidth {
get => this["border-left-width"]; get => this["border-left-width"];
set => this["border-left-width"] = value; set => this["border-left-width"] = AddNumberUnits (value, "px");
} }
public Value BorderRadius { public Value BorderRadius {
get => this["border-radius"]; get => this["border-radius"];
set { set {
this["border-radius"] = value; this["border-radius"] = AddNumberUnits (value, "px");
} }
} }
public Value BorderWidth { public Value BorderWidth {
get => this["border-top-width"]; get => this["border-top-width"];
set { set {
this["border-top-width"] = value; this["border-top-width"] = AddNumberUnits (value, "px");
this["border-right-width"] = value; this["border-right-width"] = AddNumberUnits (value, "px");
this["border-bottom-width"] = value; this["border-bottom-width"] = AddNumberUnits (value, "px");
this["border-left-width"] = value; this["border-left-width"] = AddNumberUnits (value, "px");
} }
} }
@ -253,6 +253,11 @@ namespace Ooui
set => this["order"] = value; set => this["order"] = value;
} }
public Value Overflow {
get => this["overflow"];
set => this["overflow"] = value;
}
public Value PaddingTop { public Value PaddingTop {
get => this["padding-top"]; get => this["padding-top"];
set => this["padding-top"] = value; set => this["padding-top"] = value;
@ -403,6 +408,8 @@ namespace Ooui
static string AddNumberUnits (object val, string units) static string AddNumberUnits (object val, string units)
{ {
if (val == null)
return null;
if (val is string s) if (val is string s)
return s; return s;
if (val is IConvertible c) if (val is IConvertible c)

View File

@ -4,12 +4,12 @@ namespace Ooui
{ {
public class TextArea : FormControl public class TextArea : FormControl
{ {
public event TargetEventHandler Changed { public event TargetEventHandler Change {
add => AddEventListener ("change", value); add => AddEventListener ("change", value);
remove => RemoveEventListener ("change", value); remove => RemoveEventListener ("change", value);
} }
public event TargetEventHandler Inputted { public event TargetEventHandler Input {
add => AddEventListener ("input", value); add => AddEventListener ("input", value);
remove => RemoveEventListener ("input", value); remove => RemoveEventListener ("input", value);
} }
@ -36,18 +36,18 @@ namespace Ooui
: base ("textarea") : base ("textarea")
{ {
// Subscribe to the change event so we always get up-to-date values // Subscribe to the change event so we always get up-to-date values
Changed += (s, e) => {}; Change += (s, e) => {};
} }
public TextArea (string text) public TextArea (string text)
: this () : this ()
{ {
Text = text; Value = text;
} }
protected override bool TriggerEventFromMessage (Message message) 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 // Don't need to notify here because the base implementation will fire the event
val = message.Value != null ? Convert.ToString (message.Value) : ""; 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); static readonly ManualResetEvent started = new ManualResetEvent (false);
[ThreadStatic]
static System.Security.Cryptography.SHA256 sha256;
static CancellationTokenSource serverCts; static CancellationTokenSource serverCts;
static readonly Dictionary<string, RequestHandler> publishedPaths = static readonly Dictionary<string, RequestHandler> publishedPaths =
@ -26,8 +29,10 @@ namespace Ooui
public static StyleSelectors Styles => rules; public static StyleSelectors Styles => rules;
static readonly byte[] clientJsBytes; static readonly byte[] clientJsBytes;
static readonly string clientJsEtag;
public static byte[] ClientJsBytes => clientJsBytes; public static byte[] ClientJsBytes => clientJsBytes;
public static string ClientJsEtag => clientJsEtag;
public static string Template { get; set; } = $@"<!DOCTYPE html> public static string Template { get; set; } = $@"<!DOCTYPE html>
<html> <html>
@ -35,10 +40,14 @@ namespace Ooui
<title>@Title</title> <title>@Title</title>
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" /> <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://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> <style>@Styles</style>
</head> </head>
<body> <body>
<div id=""ooui-body"" class=""container-fluid""></div> <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 src=""/ooui.js""></script>
<script>ooui(""@WebSocketPath"");</script> <script>ooui(""@WebSocketPath"");</script>
</body> </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 () static UI ()
{ {
@ -79,6 +101,22 @@ namespace Ooui
clientJsBytes = Encoding.UTF8.GetBytes (r.ReadToEnd ()); 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) static void Publish (string path, RequestHandler handler)
@ -110,7 +148,47 @@ namespace Ooui
if (contentType == null) { if (contentType == null) {
contentType = GuessContentType (path, filePath); 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) public static void PublishJson (string path, Func<object> ctor)
@ -121,7 +199,8 @@ namespace Ooui
public static void PublishJson (string path, object value) public static void PublishJson (string path, object value)
{ {
var data = JsonHandler.GetData (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) public static void PublishCustomResponse (string path, Action<HttpListenerContext, CancellationToken> responder)
@ -153,6 +232,7 @@ namespace Ooui
static void Start () static void Start ()
{ {
if (!serverEnabled) return;
if (serverCts != null) return; if (serverCts != null) return;
serverCts = new CancellationTokenSource (); serverCts = new CancellationTokenSource ();
var token = serverCts.Token; var token = serverCts.Token;
@ -232,13 +312,23 @@ namespace Ooui
var response = listenerContext.Response; var response = listenerContext.Response;
if (path == "/ooui.js") { if (path == "/ooui.js") {
var inm = listenerContext.Request.Headers.Get ("If-None-Match");
if (string.IsNullOrEmpty (inm) || inm != clientJsEtag) {
response.StatusCode = 200;
response.ContentLength64 = clientJsBytes.LongLength; response.ContentLength64 = clientJsBytes.LongLength;
response.ContentType = "application/javascript"; response.ContentType = "application/javascript";
response.ContentEncoding = Encoding.UTF8; response.ContentEncoding = Encoding.UTF8;
response.AddHeader ("Cache-Control", "public, max-age=3600"); response.AddHeader ("Cache-Control", "public, max-age=60");
response.AddHeader ("Etag", clientJsEtag);
using (var s = response.OutputStream) { using (var s = response.OutputStream) {
s.Write (clientJsBytes, 0, clientJsBytes.Length); s.Write (clientJsBytes, 0, clientJsBytes.Length);
} }
response.Close ();
}
else {
response.StatusCode = 304;
response.Close ();
}
} }
else { else {
var found = false; var found = false;
@ -308,11 +398,17 @@ namespace Ooui
class DataHandler : RequestHandler class DataHandler : RequestHandler
{ {
readonly byte[] data; readonly byte[] data;
readonly string etag;
readonly string contentType; 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.data = data;
this.etag = etag;
this.contentType = contentType; this.contentType = contentType;
} }
@ -322,7 +418,13 @@ namespace Ooui
var path = url.LocalPath; var path = url.LocalPath;
var response = listenerContext.Response; var response = listenerContext.Response;
var inm = listenerContext.Request.Headers.Get ("If-None-Match");
if (!string.IsNullOrEmpty (inm) && inm == etag) {
response.StatusCode = 304;
}
else {
response.StatusCode = 200; response.StatusCode = 200;
response.AddHeader ("Etag", etag);
if (!string.IsNullOrEmpty (contentType)) if (!string.IsNullOrEmpty (contentType))
response.ContentType = contentType; response.ContentType = contentType;
response.ContentLength64 = data.LongLength; response.ContentLength64 = data.LongLength;
@ -330,6 +432,7 @@ namespace Ooui
using (var s = response.OutputStream) { using (var s = response.OutputStream) {
s.Write (data, 0, data.Length); s.Write (data, 0, data.Length);
} }
}
response.Close (); response.Close ();
} }
} }
@ -432,11 +535,32 @@ namespace Ooui
return; 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 // Create a new session and let it handle everything from here
// //
try { try {
var session = new Session (webSocket, element, 1024, 768, serverToken); var session = new Session (webSocket, element, w, h, serverToken);
await session.RunAsync ().ConfigureAwait (false); await session.RunAsync ().ConfigureAwait (false);
} }
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) {
@ -470,9 +594,11 @@ namespace Ooui
readonly HashSet<string> createdIds; readonly HashSet<string> createdIds;
readonly List<Message> queuedMessages = new List<Message> (); readonly List<Message> queuedMessages = new List<Message> ();
public const int MaxFps = 30;
readonly System.Timers.Timer sendThrottle; readonly System.Timers.Timer sendThrottle;
DateTime lastTransmitTime = DateTime.MinValue; 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 initialWidth;
readonly double initialHeight; readonly double initialHeight;
@ -619,6 +745,7 @@ namespace Ooui
// //
// Add it to the queue // Add it to the queue
// //
//Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}");
queuedMessages.Add (message); queuedMessages.Add (message);
} }
@ -637,19 +764,24 @@ namespace Ooui
// Dequeue as many messages as we can // Dequeue as many messages as we can
// //
var messagesToSend = new List<Message> (); var messagesToSend = new List<Message> ();
System.Runtime.CompilerServices.ConfiguredTaskAwaitable task;
lock (queuedMessages) { lock (queuedMessages) {
messagesToSend.AddRange (queuedMessages); messagesToSend.AddRange (queuedMessages);
queuedMessages.Clear (); queuedMessages.Clear ();
}
if (messagesToSend.Count == 0) if (messagesToSend.Count == 0)
return; return;
// //
// Now actually send this message // 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 json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend);
var outputBuffer = new ArraySegment<byte> (Encoding.UTF8.GetBytes (json)); var outputBuffer = new ArraySegment<byte> (Encoding.UTF8.GetBytes (json));
await webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false); //Console.WriteLine ("TRANSMIT " + json);
task = webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false);
}
await task;
} }
catch (Exception ex) { catch (Exception ex) {
Error ("Failed to send queued messages, aborting session", 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 head = new Heading { Text = "Click away!" };
var label = new Label { Text = "0" }; var label = new Label { Text = "0" };
var btn = new Button { Text = "Increase" }; var btn = new Button { Text = "Increase" };
btn.Clicked += (sender, e) => { btn.Click += (sender, e) => {
count++; count++;
label.Text = count.ToString (); 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. 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 // Add some logic to it
var count = 0; var count = 0;
button.Clicked += (s, e) => { button.Click += (s, e) => {
count++; count++;
button.Text = $"Clicked {count} times"; button.Text = $"Clicked {count} times";
}; };
@ -77,7 +81,7 @@ Button MakeButton()
{ {
var button = new Button("Click me!"); var button = new Button("Click me!");
var count = 0; var count = 0;
button.Clicked += (s, e) => { button.Click += (s, e) => {
count++; count++;
button.Text = $"Clicked {count} times"; 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"; button.Style.MarginTop = "2em";
var count = 0; var count = 0;
button.Clicked += (s, e) => { button.Click += (s, e) => {
count++; count++;
button.Text = $"Clicked {count} times"; button.Text = $"Clicked {count} times";
}; };

View File

@ -4,10 +4,15 @@
x:Class="Samples.DisplayAlertPage"> x:Class="Samples.DisplayAlertPage">
<ContentPage.Content> <ContentPage.Content>
<StackLayout> <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" /> <ActivityIndicator x:Name="activity" />
<ProgressBar x:Name="progress" /> <ProgressBar x:Name="progress" />
<DatePicker x:Name="datePicker" /> <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" <Button Text="Tap to Display Alert"
Clicked="OnButtonClicked" /> Clicked="OnButtonClicked" />
</StackLayout> </StackLayout>

View File

@ -18,15 +18,24 @@ namespace Samples
{ {
var heading = new Heading ("Draw"); var heading = new Heading ("Draw");
var subtitle = new Paragraph ("Click to draw a masterpiece"); 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 { var canvas = new Canvas {
Width = 320, Width = 320,
Height = 240, Height = 240,
}; };
var context = canvas.GetContext2D (); var context = canvas.GetContext2D ();
canvas.Clicked += (s, e) => { canvas.Click += (s, e) => {
var radius = 10;
context.BeginPath (); 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 (); context.Fill ();
}; };
canvas.Style.Cursor = "pointer"; canvas.Style.Cursor = "pointer";
@ -38,7 +47,7 @@ namespace Samples
Type = ButtonType.Submit, Type = ButtonType.Submit,
ClassName = "btn btn-danger", ClassName = "btn btn-danger",
}; };
clearbtn.Clicked += (s, e) => { clearbtn.Click += (s, e) => {
context.ClearRect (0, 0, canvas.Width, canvas.Height); context.ClearRect (0, 0, canvas.Width, canvas.Height);
}; };
clearbtn.Style.Display = "block"; clearbtn.Style.Display = "block";
@ -46,6 +55,7 @@ namespace Samples
var app = new Div (); var app = new Div ();
app.AppendChild (heading); app.AppendChild (heading);
app.AppendChild (subtitle); app.AppendChild (subtitle);
app.AppendChild (new Div (toolSel));
app.AppendChild (canvas); app.AppendChild (canvas);
app.AppendChild (clearbtn); app.AppendChild (clearbtn);
return app; return app;

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
<ContentPage.Content> <ContentPage.Content>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -10,8 +11,12 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Editor x:Name="editor" FontFamily="monospace" Grid.Row="0" Grid.Column="0" /> <StackLayout Orientation="Vertical" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<ContentView x:Name="results" Grid.Row="0" Grid.Column="1" BackgroundColor="White" /> <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> </Grid>
</ContentPage.Content> </ContentPage.Content>
</ContentPage> </ContentPage>

View File

@ -1,6 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Xamarin.Forms; using Xamarin.Forms;
namespace Samples namespace Samples
@ -23,24 +23,50 @@ namespace Samples
<ColumnDefinition Width=""*"" /> <ColumnDefinition Width=""*"" />
<ColumnDefinition Width=""*"" /> <ColumnDefinition Width=""*"" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Label Text=""Top Left"" Grid.Row=""0"" Grid.Column=""0"" /> <StackLayout Grid.Row=""0"" Grid.Column=""0"">
<Label Text=""Top Right"" Grid.Row=""0"" Grid.Column=""1"" /> <Label Text=""Top Left"" />
<Label Text=""Bottom Left"" Grid.Row=""1"" Grid.Column=""0"" /> <Entry Placeholder=""I'm ready for some text"" />
<Label Text=""Bottom Right"" Grid.Row=""1"" Grid.Column=""1"" /> <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> </Grid>
</ContentView>"; </ContentView>";
editor.TextChanged += (sender, e) => DisplayXaml ();
DisplayXaml (); DisplayXaml ();
} }
CancellationTokenSource lastCts = null;
public void DisplayXaml () public void DisplayXaml ()
{ {
try {
var cts = new CancellationTokenSource ();
var token = cts.Token;
lastCts?.Cancel ();
lastCts = cts;
var asm = typeof (Xamarin.Forms.Xaml.Internals.XamlTypeResolver).Assembly; var asm = typeof (Xamarin.Forms.Xaml.Internals.XamlTypeResolver).Assembly;
var xamlLoaderType = asm.GetType ("Xamarin.Forms.Xaml.XamlLoader"); var xamlLoaderType = asm.GetType ("Xamarin.Forms.Xaml.XamlLoader");
var loadArgTypes = new[] { typeof (object), typeof (string) }; 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 loadMethod = xamlLoaderType.GetMethod ("Load", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, null, System.Reflection.CallingConventions.Any, loadArgTypes, null);
var contentView = new ContentView (); var contentView = new ContentView ();
loadMethod.Invoke (null, new object[] { contentView, editor.Text }); loadMethod.Invoke (null, new object[] { contentView, editor.Text });
if (!token.IsCancellationRequested) {
results.Content = contentView; 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); listened = listened || (m.MessageType == MessageType.Listen);
}; };
Assert.IsFalse (listened); Assert.IsFalse (listened);
b.Clicked += (s, e) => { b.Click += (s, e) => {
clicked = true; clicked = true;
}; };
Assert.IsTrue (listened); Assert.IsTrue (listened);

View File

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

View File

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

View File

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