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. ///