diff --git a/Cairo/Perspex.Cairo/Media/StreamGeometryContextImpl.cs b/Cairo/Perspex.Cairo/Media/StreamGeometryContextImpl.cs
index 12e15ad503..e49c7ab450 100644
--- a/Cairo/Perspex.Cairo/Media/StreamGeometryContextImpl.cs
+++ b/Cairo/Perspex.Cairo/Media/StreamGeometryContextImpl.cs
@@ -6,6 +6,7 @@
namespace Perspex.Cairo.Media
{
+ using Perspex.Media;
using Perspex.Platform;
public class StreamGeometryContextImpl : IStreamGeometryContextImpl
@@ -15,6 +16,11 @@ namespace Perspex.Cairo.Media
// TODO: Implement
}
+ public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+ {
+ // TODO: Implement
+ }
+
public void BeginFigure(Point startPoint, bool isFilled)
{
// TODO: Implement
diff --git a/Perspex.Controls/Image.cs b/Perspex.Controls/Image.cs
index 051e47cf1f..d63a0958d8 100644
--- a/Perspex.Controls/Image.cs
+++ b/Perspex.Controls/Image.cs
@@ -47,10 +47,10 @@ namespace Perspex.Controls
Vector scale = CalculateScaling(this.ActualSize, sourceSize, this.Stretch);
Size scaledSize = sourceSize * scale;
Rect destRect = viewPort
- .Center(new Rect(scaledSize))
+ .CenterIn(new Rect(scaledSize))
.Intersect(viewPort);
Rect sourceRect = new Rect(sourceSize)
- .Center(new Rect(destRect.Size / scale));
+ .CenterIn(new Rect(destRect.Size / scale));
drawingContext.DrawImage(source, 1, sourceRect, destRect);
}
diff --git a/Perspex.Controls/Perspex.Controls.csproj b/Perspex.Controls/Perspex.Controls.csproj
index 8c3511e128..d4aa3f3254 100644
--- a/Perspex.Controls/Perspex.Controls.csproj
+++ b/Perspex.Controls/Perspex.Controls.csproj
@@ -36,6 +36,7 @@
+
@@ -75,6 +76,7 @@
+
diff --git a/Perspex.Controls/Primitives/ToggleButton.cs b/Perspex.Controls/Primitives/ToggleButton.cs
index 101127be71..adaa5f0bcb 100644
--- a/Perspex.Controls/Primitives/ToggleButton.cs
+++ b/Perspex.Controls/Primitives/ToggleButton.cs
@@ -20,7 +20,7 @@ namespace Perspex.Controls.Primitives
public ToggleButton()
{
- this.Click += (s, e) => this.IsChecked = !this.IsChecked;
+ this.Click += (s, e) => this.Toggle();
}
public bool IsChecked
@@ -28,5 +28,10 @@ namespace Perspex.Controls.Primitives
get { return this.GetValue(IsCheckedProperty); }
set { this.SetValue(IsCheckedProperty, value); }
}
+
+ protected virtual void Toggle()
+ {
+ this.IsChecked = !this.IsChecked;
+ }
}
}
diff --git a/Perspex.Controls/RadioButton.cs b/Perspex.Controls/RadioButton.cs
new file mode 100644
index 0000000000..33c9bfa298
--- /dev/null
+++ b/Perspex.Controls/RadioButton.cs
@@ -0,0 +1,46 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2014 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Controls
+{
+ using System;
+ using System.Linq;
+ using Perspex.Controls.Primitives;
+
+ public class RadioButton : ToggleButton
+ {
+ public RadioButton()
+ {
+ this.GetObservable(IsCheckedProperty).Subscribe(this.IsCheckedChanged);
+ }
+
+ protected override void Toggle()
+ {
+ if (!this.IsChecked)
+ {
+ this.IsChecked = true;
+ }
+ }
+
+ private void IsCheckedChanged(bool value)
+ {
+ var parent = this.GetVisualParent();
+
+ if (value && parent != null)
+ {
+ var siblings = parent
+ .GetVisualChildren()
+ .OfType()
+ .Where(x => x != this);
+
+ foreach (var sibling in siblings)
+ {
+ sibling.IsChecked = false;
+ }
+ }
+ }
+ }
+}
diff --git a/Perspex.Controls/Shapes/Ellipse.cs b/Perspex.Controls/Shapes/Ellipse.cs
new file mode 100644
index 0000000000..00098f3991
--- /dev/null
+++ b/Perspex.Controls/Shapes/Ellipse.cs
@@ -0,0 +1,38 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2014 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Controls.Shapes
+{
+ using Perspex.Media;
+
+ public class Ellipse : Shape
+ {
+ private Geometry geometry;
+
+ private Size geometrySize;
+
+ public override Geometry DefiningGeometry
+ {
+ get
+ {
+ if (this.geometry == null || this.geometrySize != this.ActualSize)
+ {
+ var rect = new Rect(0, 0, this.ActualSize.Width, this.ActualSize.Height);
+ rect = rect.Deflate(this.StrokeThickness / 2);
+ this.geometry = new EllipseGeometry(rect);
+ this.geometrySize = this.ActualSize;
+ }
+
+ return this.geometry;
+ }
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ return new Size(this.StrokeThickness, this.StrokeThickness);
+ }
+ }
+}
diff --git a/Perspex.SceneGraph/Media/EllipseGeometry.cs b/Perspex.SceneGraph/Media/EllipseGeometry.cs
new file mode 100644
index 0000000000..90610ce574
--- /dev/null
+++ b/Perspex.SceneGraph/Media/EllipseGeometry.cs
@@ -0,0 +1,59 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2014 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Media
+{
+ using System;
+ using Perspex.Platform;
+ using Splat;
+
+ public class EllipseGeometry : Geometry
+ {
+ public EllipseGeometry(Rect rect)
+ {
+ IPlatformRenderInterface factory = Locator.Current.GetService();
+ IStreamGeometryImpl impl = factory.CreateStreamGeometry();
+
+ using (IStreamGeometryContextImpl ctx = impl.Open())
+ {
+ double ControlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3;
+ var center = rect.Center;
+ var radius = new Vector(rect.Width / 2, rect.Height / 2);
+
+ var x0 = center.X - radius.X;
+ var x1 = center.X - radius.X * ControlPointRatio;
+ var x2 = center.X;
+ var x3 = center.X + radius.X * ControlPointRatio;
+ var x4 = center.X + radius.X;
+
+ var y0 = center.Y - radius.Y;
+ var y1 = center.Y - radius.Y * ControlPointRatio;
+ var y2 = center.Y;
+ var y3 = center.Y + radius.Y * ControlPointRatio;
+ var y4 = center.Y + radius.Y;
+
+ ctx.BeginFigure(new Point(x2, y0), true);
+ ctx.BezierTo(new Point(x3, y0), new Point(x4, y1), new Point(x4, y2));
+ ctx.BezierTo(new Point(x4, y3), new Point(x3, y4), new Point(x2, y4));
+ ctx.BezierTo(new Point(x1, y4), new Point(x0, y3), new Point(x0, y2));
+ ctx.BezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0));
+ ctx.EndFigure(true);
+ }
+
+ this.PlatformImpl = impl;
+ }
+
+ public override Rect Bounds
+ {
+ get { return this.PlatformImpl.Bounds; }
+ }
+
+ public override Geometry Clone()
+ {
+ return new EllipseGeometry(this.Bounds);
+ }
+ }
+}
diff --git a/Perspex.SceneGraph/Media/StreamGeometryContext.cs b/Perspex.SceneGraph/Media/StreamGeometryContext.cs
index 84677cb066..62d24b3d7e 100644
--- a/Perspex.SceneGraph/Media/StreamGeometryContext.cs
+++ b/Perspex.SceneGraph/Media/StreamGeometryContext.cs
@@ -18,6 +18,11 @@ namespace Perspex.Media
this.impl = impl;
}
+ public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+ {
+ this.impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
+ }
+
public void BeginFigure(Point startPoint, bool isFilled)
{
this.impl.BeginFigure(startPoint, isFilled);
diff --git a/Perspex.SceneGraph/Media/SweepDirection.cs b/Perspex.SceneGraph/Media/SweepDirection.cs
new file mode 100644
index 0000000000..14cd7d2f3c
--- /dev/null
+++ b/Perspex.SceneGraph/Media/SweepDirection.cs
@@ -0,0 +1,14 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2013 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Media
+{
+ public enum SweepDirection
+ {
+ CounterClockwise,
+ Clockwise,
+ }
+}
diff --git a/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/Perspex.SceneGraph/Perspex.SceneGraph.csproj
index d82ece6791..40b9cd6492 100644
--- a/Perspex.SceneGraph/Perspex.SceneGraph.csproj
+++ b/Perspex.SceneGraph/Perspex.SceneGraph.csproj
@@ -58,6 +58,7 @@
+
@@ -65,6 +66,7 @@
+
diff --git a/Perspex.SceneGraph/Platform/IStreamGeometryContextImpl.cs b/Perspex.SceneGraph/Platform/IStreamGeometryContextImpl.cs
index dc783491e1..aacd8801a8 100644
--- a/Perspex.SceneGraph/Platform/IStreamGeometryContextImpl.cs
+++ b/Perspex.SceneGraph/Platform/IStreamGeometryContextImpl.cs
@@ -7,9 +7,12 @@
namespace Perspex.Platform
{
using System;
+ using Perspex.Media;
public interface IStreamGeometryContextImpl : IDisposable
{
+ void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
+
void BeginFigure(Point startPoint, bool isFilled);
void BezierTo(Point point1, Point point2, Point point3);
diff --git a/Perspex.SceneGraph/Rect.cs b/Perspex.SceneGraph/Rect.cs
index 82df33050c..c9933353f1 100644
--- a/Perspex.SceneGraph/Rect.cs
+++ b/Perspex.SceneGraph/Rect.cs
@@ -183,6 +183,14 @@ namespace Perspex
get { return new Point(this.Right, this.Bottom); }
}
+ ///
+ /// Gets the center point of the rectangle.
+ ///
+ public Point Center
+ {
+ get { return new Point(this.x + (this.width / 2), this.y + (this.height / 2)); }
+ }
+
///
/// Gets a value that indicates whether the rectangle is empty.
///
@@ -255,7 +263,7 @@ namespace Perspex
p.Y >= this.y && p.Y < this.y + this.height;
}
- public Rect Center(Rect rect)
+ public Rect CenterIn(Rect rect)
{
return new Rect(
this.x + ((this.width - rect.width) / 2),
diff --git a/Perspex.Themes.Default/DefaultTheme.cs b/Perspex.Themes.Default/DefaultTheme.cs
index 51d915a43d..5d7ea5b1a5 100644
--- a/Perspex.Themes.Default/DefaultTheme.cs
+++ b/Perspex.Themes.Default/DefaultTheme.cs
@@ -19,6 +19,7 @@ namespace Perspex.Themes.Default
this.Add(new ItemsControlStyle());
this.Add(new ListBoxStyle());
this.Add(new ListBoxItemStyle());
+ this.Add(new RadioButtonStyle());
this.Add(new ScrollBarStyle());
this.Add(new ScrollViewerStyle());
this.Add(new TabControlStyle());
diff --git a/Perspex.Themes.Default/Perspex.Themes.Default.csproj b/Perspex.Themes.Default/Perspex.Themes.Default.csproj
index e9c253b038..807f66800d 100644
--- a/Perspex.Themes.Default/Perspex.Themes.Default.csproj
+++ b/Perspex.Themes.Default/Perspex.Themes.Default.csproj
@@ -66,6 +66,7 @@
+
diff --git a/Perspex.Themes.Default/RadioButtonStyle.cs b/Perspex.Themes.Default/RadioButtonStyle.cs
new file mode 100644
index 0000000000..1f374df041
--- /dev/null
+++ b/Perspex.Themes.Default/RadioButtonStyle.cs
@@ -0,0 +1,97 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2014 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Themes.Default
+{
+ using System;
+ using System.Linq;
+ using Perspex.Controls;
+ using Perspex.Layout;
+ using Perspex.Media;
+ using Perspex.Controls.Shapes;
+ using Perspex.Styling;
+ using Perspex.Controls.Presenters;
+
+ public class RadioButtonStyle : Styles
+ {
+ public RadioButtonStyle()
+ {
+ this.AddRange(new[]
+ {
+ new Style(x => x.OfType())
+ {
+ Setters = new[]
+ {
+ new Setter(Button.TemplateProperty, ControlTemplate.Create(this.Template)),
+ },
+ },
+ new Style(x => x.OfType().Template().Id("checkMark"))
+ {
+ Setters = new[]
+ {
+ new Setter(Shape.IsVisibleProperty, false),
+ },
+ },
+ new Style(x => x.OfType().Class(":checked").Template().Id("checkMark"))
+ {
+ Setters = new[]
+ {
+ new Setter(Shape.IsVisibleProperty, true),
+ },
+ },
+ });
+ }
+
+ private Control Template(RadioButton control)
+ {
+ Border result = new Border
+ {
+ [~Border.BackgroundProperty] = control[~RadioButton.BackgroundProperty],
+ Content = new Grid
+ {
+ ColumnDefinitions = new ColumnDefinitions
+ {
+ new ColumnDefinition(GridLength.Auto),
+ new ColumnDefinition(new GridLength(1, GridUnitType.Star)),
+ },
+ Children = new Controls
+ {
+ new Ellipse
+ {
+ Id = "checkBorder",
+ Stroke = Brushes.Black,
+ StrokeThickness = 2,
+ Width = 18,
+ Height = 18,
+ VerticalAlignment = VerticalAlignment.Center,
+ [Grid.ColumnProperty] = 0,
+ },
+ new Ellipse
+ {
+ Id = "checkMark",
+ Fill = Brushes.Black,
+ Width = 10,
+ Height = 10,
+ Stretch = Stretch.Uniform,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ [Grid.ColumnProperty] = 0,
+ },
+ new ContentPresenter
+ {
+ Margin = new Thickness(4, 0, 0, 0),
+ VerticalAlignment = VerticalAlignment.Center,
+ [~ContentPresenter.ContentProperty] = control[~RadioButton.ContentProperty],
+ [Grid.ColumnProperty] = 1,
+ },
+ },
+ },
+ };
+
+ return result;
+ }
+ }
+}
diff --git a/TestApplication/Program.cs b/TestApplication/Program.cs
index a3e520117d..a5f402aa37 100644
--- a/TestApplication/Program.cs
+++ b/TestApplication/Program.cs
@@ -198,6 +198,15 @@ namespace TestApplication
{
Content = "Checkbox",
},
+ new RadioButton
+ {
+ Content = "RadioButton 1",
+ IsChecked = true,
+ },
+ new RadioButton
+ {
+ Content = "RadioButton 2",
+ },
}
},
};
diff --git a/TestFiles/Direct2D1/Shapes/Ellipse/Circle_1px_Stroke.expected.png b/TestFiles/Direct2D1/Shapes/Ellipse/Circle_1px_Stroke.expected.png
new file mode 100644
index 0000000000..e637006545
Binary files /dev/null and b/TestFiles/Direct2D1/Shapes/Ellipse/Circle_1px_Stroke.expected.png differ
diff --git a/Windows/Perspex.Direct2D1.RenderTests/Perspex.Direct2D1.RenderTests.csproj b/Windows/Perspex.Direct2D1.RenderTests/Perspex.Direct2D1.RenderTests.csproj
index bace0f0688..d85dc70820 100644
--- a/Windows/Perspex.Direct2D1.RenderTests/Perspex.Direct2D1.RenderTests.csproj
+++ b/Windows/Perspex.Direct2D1.RenderTests/Perspex.Direct2D1.RenderTests.csproj
@@ -62,6 +62,7 @@
+
diff --git a/Windows/Perspex.Direct2D1.RenderTests/Shapes/EllipseTests.cs b/Windows/Perspex.Direct2D1.RenderTests/Shapes/EllipseTests.cs
new file mode 100644
index 0000000000..00874c20b7
--- /dev/null
+++ b/Windows/Perspex.Direct2D1.RenderTests/Shapes/EllipseTests.cs
@@ -0,0 +1,41 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2014 MIT Licence. See licence.md for more information.
+//
+// -----------------------------------------------------------------------
+
+namespace Perspex.Direct2D1.RenderTests.Shapes
+{
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+ using Perspex.Controls;
+ using Perspex.Media;
+ using Perspex.Controls.Shapes;
+
+ [TestClass]
+ public class EllipseTests : TestBase
+ {
+ public EllipseTests()
+ : base(@"Shapes\Ellipse")
+ {
+ }
+
+ [TestMethod]
+ public void Circle_1px_Stroke()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Content = new Ellipse
+ {
+ Stroke = Brushes.Black,
+ StrokeThickness = 1,
+ }
+ };
+
+ this.RenderToFile(target);
+ this.CompareImages();
+ }
+ }
+}
diff --git a/Windows/Perspex.Direct2D1/Media/StreamGeometryContextImpl.cs b/Windows/Perspex.Direct2D1/Media/StreamGeometryContextImpl.cs
index 53f98a24da..15fb88d7d0 100644
--- a/Windows/Perspex.Direct2D1/Media/StreamGeometryContextImpl.cs
+++ b/Windows/Perspex.Direct2D1/Media/StreamGeometryContextImpl.cs
@@ -18,6 +18,23 @@ namespace Perspex.Direct2D1.Media
this.sink = sink;
}
+ public void ArcTo(
+ Point point,
+ Size size,
+ double rotationAngle,
+ bool isLargeArc,
+ Perspex.Media.SweepDirection sweepDirection)
+ {
+ this.sink.AddArc(new ArcSegment
+ {
+ Point = point.ToSharpDX(),
+ Size = size.ToSharpDX(),
+ RotationAngle = (float)rotationAngle,
+ ArcSize = isLargeArc ? ArcSize.Large : ArcSize.Small,
+ SweepDirection = (SweepDirection)sweepDirection,
+ });
+ }
+
public void BeginFigure(Point startPoint, bool isFilled)
{
this.sink.BeginFigure(startPoint.ToSharpDX(), isFilled ? FigureBegin.Filled : FigureBegin.Hollow);
diff --git a/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs b/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs
index d18f8cb1e7..c2512b7edb 100644
--- a/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs
+++ b/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs
@@ -26,6 +26,11 @@ namespace Perspex.Direct2D1
return new Vector2((float)p.X, (float)p.Y);
}
+ public static Size2F ToSharpDX(this Perspex.Size p)
+ {
+ return new Size2F((float)p.Width, (float)p.Height);
+ }
+
///
/// Converts a brush to Direct2D.
///