diff --git a/src/Perspex.Application/Application.cs b/src/Perspex.Application/Application.cs index 1b94ecaa51..40c3542d8c 100644 --- a/src/Perspex.Application/Application.cs +++ b/src/Perspex.Application/Application.cs @@ -150,6 +150,7 @@ namespace Perspex /// protected virtual void RegisterServices() { + PerspexSynchronizationContext.InstallIfNeeded(); this.FocusManager = new FocusManager(); this.InputManager = new InputManager(); diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index 654fbb3926..d5723e5a6d 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -72,6 +72,7 @@ + diff --git a/src/Perspex.Base/Threading/Dispatcher.cs b/src/Perspex.Base/Threading/Dispatcher.cs index af6f1d58af..d959151543 100644 --- a/src/Perspex.Base/Threading/Dispatcher.cs +++ b/src/Perspex.Base/Threading/Dispatcher.cs @@ -50,6 +50,14 @@ namespace Perspex.Threading this.mainLoop.Run(cancellationToken); } + /// + /// Runs continuations pushed on the loop. + /// + public void RunJobs() + { + this.mainLoop.RunJobs(); + } + /// /// Invokes a method on the dispatcher thread. /// @@ -60,5 +68,15 @@ namespace Perspex.Threading { return this.mainLoop.InvokeAsync(action, priority); } + + /// + /// Post action that will be invoked on main thread + /// + /// The method. + /// The priority with which to invoke the method. + internal void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + { + this.mainLoop.Post(action, priority); + } } } \ No newline at end of file diff --git a/src/Perspex.Base/Threading/DispatcherTimer.cs b/src/Perspex.Base/Threading/DispatcherTimer.cs index 73335ea408..0d49bb21f8 100644 --- a/src/Perspex.Base/Threading/DispatcherTimer.cs +++ b/src/Perspex.Base/Threading/DispatcherTimer.cs @@ -203,7 +203,7 @@ namespace Perspex.Threading /// private void InternalTick() { - this.Dispatcher.InvokeAsync(this.RaiseTick, this.priority); + this.Dispatcher.Post(this.RaiseTick, this.priority); } /// diff --git a/src/Perspex.Base/Threading/MainLoop.cs b/src/Perspex.Base/Threading/MainLoop.cs index e7efefacc1..3a99133d15 100644 --- a/src/Perspex.Base/Threading/MainLoop.cs +++ b/src/Perspex.Base/Threading/MainLoop.cs @@ -42,23 +42,40 @@ namespace Perspex.Win32.Threading { while (!cancellationToken.IsCancellationRequested) { - Job job = null; + RunJobs(); - while (job != null || this.queue.Count > 0) + platform.ProcessMessage(); + } + } + + /// + /// Runs continuations pushed on the loop. + /// + public void RunJobs() + { + Job job = null; + + while (job != null || this.queue.Count > 0) + { + if (job == null) { - if (job == null) + lock (this.queue) { - lock (this.queue) - { - job = this.queue.Dequeue(); - } + job = this.queue.Dequeue(); } + } - if (job.Priority < DispatcherPriority.Input && platform.HasMessages()) - { - break; - } + if (job.Priority < DispatcherPriority.Input && platform.HasMessages()) + { + break; + } + if (job.TaskCompletionSource == null) + { + job.Action(); + } + else + { try { job.Action(); @@ -68,11 +85,9 @@ namespace Perspex.Win32.Threading { job.TaskCompletionSource.SetException(e); } - - job = null; } - platform.ProcessMessage(); + job = null; } } @@ -84,15 +99,29 @@ namespace Perspex.Win32.Threading /// A task that can be used to track the method's execution. public Task InvokeAsync(Action action, DispatcherPriority priority) { - var job = new Job(action, priority); + var job = new Job(action, priority, false); + this.AddJob(job); + return job.TaskCompletionSource.Task; + } + /// + /// Post action that will be invoked on main thread + /// + /// The method. + /// + /// The priority with which to invoke the method. + internal void Post(Action action, DispatcherPriority priority) + { + this.AddJob(new Job(action, priority, true)); + } + + private void AddJob(Job job) + { lock (this.queue) { - this.queue.Add(job, priority); + this.queue.Add(job, job.Priority); } - platform.Wake(); - return job.TaskCompletionSource.Task; } /// @@ -105,11 +134,12 @@ namespace Perspex.Win32.Threading /// /// The method to call. /// The job priority. - public Job(Action action, DispatcherPriority priority) + /// Do not wrap excepption in TaskCompletionSource + public Job(Action action, DispatcherPriority priority, bool throwOnUiThread) { this.Action = action; this.Priority = priority; - this.TaskCompletionSource = new TaskCompletionSource(); + this.TaskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); } /// diff --git a/src/Perspex.Base/Threading/PerspexSynchronizationContext.cs b/src/Perspex.Base/Threading/PerspexSynchronizationContext.cs new file mode 100644 index 0000000000..40bbfc801c --- /dev/null +++ b/src/Perspex.Base/Threading/PerspexSynchronizationContext.cs @@ -0,0 +1,47 @@ +namespace Perspex.Threading +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Threading; + + + /// + /// SynchronizationContext to be used on main thread + /// + public class PerspexSynchronizationContext : SynchronizationContext + { + /// + /// Controls if SynchronizationContext should be installed in InstallIfNeeded. Used by Designer. + /// + public static bool AutoInstall { get; set; } = true; + + /// + /// Installs synchronization context in current thread + /// + public static void InstallIfNeeded() + { + if (!AutoInstall || Current is PerspexSynchronizationContext) + { + return; + } + + SetSynchronizationContext(new PerspexSynchronizationContext()); + } + + /// + public override void Post(SendOrPostCallback d, object state) + { + Dispatcher.UIThread.Post(() => d(state)); + } + + /// + public override void Send(SendOrPostCallback d, object state) + { + // TODO: Add check for being on the main thread, we should invoke the method immediately in this case + Dispatcher.UIThread.InvokeAsync(() => d(state)).Wait(); + } + } +} \ No newline at end of file diff --git a/src/Perspex.ReactiveUI/Registrations.cs b/src/Perspex.ReactiveUI/Registrations.cs index b6b09fb7f2..699d02d87f 100644 --- a/src/Perspex.ReactiveUI/Registrations.cs +++ b/src/Perspex.ReactiveUI/Registrations.cs @@ -1,4 +1,6 @@ using System; +using System.Reactive.Concurrency; +using System.Threading; namespace ReactiveUI @@ -12,7 +14,7 @@ namespace ReactiveUI { public void Register(Action, Type> registerFunction) { - + RxApp.MainThreadScheduler = new SynchronizationContextScheduler(SynchronizationContext.Current); } } } diff --git a/src/Perspex.SceneGraph/Media/Brush.cs b/src/Perspex.SceneGraph/Media/Brush.cs index 5a3b6c3493..26e5953621 100644 --- a/src/Perspex.SceneGraph/Media/Brush.cs +++ b/src/Perspex.SceneGraph/Media/Brush.cs @@ -11,5 +11,13 @@ namespace Perspex.Media /// public abstract class Brush : PerspexObject { + public static readonly PerspexProperty OpacityProperty = + PerspexProperty.Register(nameof(Opacity), 1.0); + + public double Opacity + { + get { return this.GetValue(OpacityProperty); } + set { this.SetValue(OpacityProperty, value); } + } } } diff --git a/src/Perspex.SceneGraph/Media/BrushMappingMode.cs b/src/Perspex.SceneGraph/Media/BrushMappingMode.cs new file mode 100644 index 0000000000..a453f81d85 --- /dev/null +++ b/src/Perspex.SceneGraph/Media/BrushMappingMode.cs @@ -0,0 +1,14 @@ +namespace Perspex.Media +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public enum BrushMappingMode + { + Absolute, + RelativeToBoundingBox + } +} \ No newline at end of file diff --git a/src/Perspex.SceneGraph/Media/GradientBrush.cs b/src/Perspex.SceneGraph/Media/GradientBrush.cs new file mode 100644 index 0000000000..5c63b66242 --- /dev/null +++ b/src/Perspex.SceneGraph/Media/GradientBrush.cs @@ -0,0 +1,38 @@ +namespace Perspex.Media +{ + using System.Collections.Generic; + + public abstract class GradientBrush : Brush + { + public static readonly PerspexProperty MappingModeProperty = +PerspexProperty.Register(nameof(MappingMode), BrushMappingMode.RelativeToBoundingBox); + + public static readonly PerspexProperty SpreadMethodProperty = +PerspexProperty.Register(nameof(SpreadMethod), GradientSpreadMethod.Pad); + + public static readonly PerspexProperty> GradientStopsProperty = +PerspexProperty.Register>(nameof(Opacity), new List()); + + public GradientBrush() + { + } + + public BrushMappingMode MappingMode + { + get { return this.GetValue(MappingModeProperty); } + set { this.SetValue(MappingModeProperty, value); } + } + + public GradientSpreadMethod SpreadMethod + { + get { return this.GetValue(SpreadMethodProperty); } + set { this.SetValue(SpreadMethodProperty, value); } + } + + public List GradientStops + { + get { return this.GetValue(GradientStopsProperty); } + set { this.SetValue(GradientStopsProperty, value); } + } + } +} \ No newline at end of file diff --git a/src/Perspex.SceneGraph/Media/GradientSpreadMethod.cs b/src/Perspex.SceneGraph/Media/GradientSpreadMethod.cs new file mode 100644 index 0000000000..78ba87b05a --- /dev/null +++ b/src/Perspex.SceneGraph/Media/GradientSpreadMethod.cs @@ -0,0 +1,15 @@ +namespace Perspex.Media +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public enum GradientSpreadMethod + { + Pad, + Reflect, + Repeat + } +} \ No newline at end of file diff --git a/src/Perspex.SceneGraph/Media/GradientStop.cs b/src/Perspex.SceneGraph/Media/GradientStop.cs new file mode 100644 index 0000000000..9dbbe610ef --- /dev/null +++ b/src/Perspex.SceneGraph/Media/GradientStop.cs @@ -0,0 +1,36 @@ +namespace Perspex.Media +{ + /// + /// GradientStop + /// + public sealed class GradientStop + { + /// + /// Initializes a new instance of the class. + /// + public GradientStop() { } + + /// + /// Initializes a new instance of the class. + /// + /// The color + /// The offset + public GradientStop(Color color, double offset) + { + this.Color = color; + this.Offset = offset; + } + + // TODO: Make these dependency properties. + + /// + /// The offset + /// + public double Offset { get; set; } + + /// + /// The color + /// + public Color Color { get; set; } + } +} \ No newline at end of file diff --git a/src/Perspex.SceneGraph/Media/LinearGradientBrush.cs b/src/Perspex.SceneGraph/Media/LinearGradientBrush.cs new file mode 100644 index 0000000000..3a63a39506 --- /dev/null +++ b/src/Perspex.SceneGraph/Media/LinearGradientBrush.cs @@ -0,0 +1,23 @@ +namespace Perspex.Media +{ + public class LinearGradientBrush : GradientBrush + { + public static readonly PerspexProperty StartPointProperty = +PerspexProperty.Register(nameof(StartPoint), new Point(0,0)); + + public static readonly PerspexProperty EndPointProperty = +PerspexProperty.Register(nameof(EndPoint), new Point(0, 0)); + + public Point StartPoint + { + get { return this.GetValue(StartPointProperty); } + set { this.SetValue(StartPointProperty, value); } + } + + public Point EndPoint + { + get { return this.GetValue(EndPointProperty); } + set { this.SetValue(EndPointProperty, value); } + } + } +} \ No newline at end of file diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj index ff8e0cf94e..d0a259a480 100644 --- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -61,9 +61,14 @@ + + + + + diff --git a/src/Windows/Perspex.Direct2D1/Media/BrushImpl.cs b/src/Windows/Perspex.Direct2D1/Media/BrushImpl.cs new file mode 100644 index 0000000000..e6114b3764 --- /dev/null +++ b/src/Windows/Perspex.Direct2D1/Media/BrushImpl.cs @@ -0,0 +1,19 @@ +using System; + +namespace Perspex.Direct2D1.Media +{ + public abstract class BrushImpl : IDisposable + { + public SharpDX.Direct2D1.Brush PlatformBrush { get; set; } + + public BrushImpl(Perspex.Media.Brush brush, SharpDX.Direct2D1.RenderTarget target, Size destinationSize) + { + } + + public virtual void Dispose() + { + if (this.PlatformBrush != null) + this.PlatformBrush.Dispose(); + } + } +} diff --git a/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs b/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs index 33571c34fc..639bae6049 100644 --- a/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs +++ b/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs @@ -98,7 +98,7 @@ namespace Perspex.Direct2D1.Media this.renderTarget.DrawLine( p1.ToSharpDX(), p2.ToSharpDX(), - d2dBrush, + d2dBrush.PlatformBrush, (float)pen.Thickness, d2dStroke); } @@ -118,7 +118,7 @@ namespace Perspex.Direct2D1.Media using (var d2dBrush = this.CreateBrush(brush, geometry.Bounds.Size)) { GeometryImpl impl = (GeometryImpl)geometry.PlatformImpl; - this.renderTarget.FillGeometry(impl.Geometry, d2dBrush); + this.renderTarget.FillGeometry(impl.Geometry, d2dBrush.PlatformBrush); } } @@ -128,7 +128,7 @@ namespace Perspex.Direct2D1.Media using (var d2dStroke = pen.ToDirect2DStrokeStyle(this.renderTarget)) { GeometryImpl impl = (GeometryImpl)geometry.PlatformImpl; - this.renderTarget.DrawGeometry(impl.Geometry, d2dBrush, (float)pen.Thickness, d2dStroke); + this.renderTarget.DrawGeometry(impl.Geometry, d2dBrush.PlatformBrush, (float)pen.Thickness, d2dStroke); } } } @@ -146,7 +146,7 @@ namespace Perspex.Direct2D1.Media { this.renderTarget.DrawRoundedRectangle( new RoundedRectangle { Rect = rect.ToDirect2D(), RadiusX = cornerRadius, RadiusY = cornerRadius }, - brush, + brush.PlatformBrush, (float)pen.Thickness, d2dStroke); } @@ -165,7 +165,7 @@ namespace Perspex.Direct2D1.Media var impl = (FormattedTextImpl)text.PlatformImpl; using (var brush = this.CreateBrush(foreground, impl.Measure())) - using (var renderer = new PerspexTextRenderer(this, this.renderTarget, brush)) + using (var renderer = new PerspexTextRenderer(this, this.renderTarget, brush.PlatformBrush)) { impl.TextLayout.Draw(renderer, (float)origin.X, (float)origin.Y); } @@ -193,7 +193,7 @@ namespace Perspex.Direct2D1.Media RadiusX = cornerRadius, RadiusY = cornerRadius }, - b); + b.PlatformBrush); } } @@ -261,67 +261,22 @@ namespace Perspex.Direct2D1.Media }); } - /// - /// Creates a Direct2D brush from a Perspex brush. - /// - /// The perspex brush. - /// The size of the brush's target area. - /// The Direct2D brush. - public Disposable CreateBrush(Perspex.Media.Brush brush, Size destinationSize) + public BrushImpl CreateBrush(Perspex.Media.Brush brush, Size destinationSize) { - var solidColorBrush = brush as Perspex.Media.SolidColorBrush; - var visualBrush = brush as Perspex.Media.VisualBrush; + Perspex.Media.SolidColorBrush solidColorBrush = brush as Perspex.Media.SolidColorBrush; + Perspex.Media.LinearGradientBrush linearGradientBrush = brush as Perspex.Media.LinearGradientBrush; if (solidColorBrush != null) { - return new Disposable( - new SharpDX.Direct2D1.SolidColorBrush( - this.renderTarget, - solidColorBrush.Color.ToDirect2D())); + return new SolidColorBrushImpl(solidColorBrush, this.renderTarget, destinationSize); } - else if (visualBrush != null) + else if (linearGradientBrush != null) { - return this.CreateBrush(visualBrush, destinationSize); + return new LinearGradientBrushImpl(linearGradientBrush, this.renderTarget, destinationSize); } else { - // TODO: Implement other brushes. - return new Disposable( - new SharpDX.Direct2D1.SolidColorBrush(this.renderTarget, new Color4())); - } - } - - /// - /// Creates a Direct2D from a Perspex . - /// - /// The perspex brush. - /// The size of the brush's target area. - /// The Direct2D brush. - private Disposable CreateBrush(VisualBrush brush, Size destinationSize) - { - var visual = brush.Visual; - var layoutable = visual as ILayoutable; - - if (layoutable?.IsArrangeValid == false) - { - layoutable.Measure(Size.Infinity); - layoutable.Arrange(new Rect(layoutable.DesiredSize)); - } - - var sourceSize = layoutable.Bounds.Size; - var destinationRect = brush.DestinationRect.ToPixels(destinationSize); - var scale = brush.Stretch.CalculateScaling(destinationRect.Size, sourceSize); - - using (var target = new BitmapRenderTarget( - this.renderTarget, - CompatibleRenderTargetOptions.None, - destinationRect.Size.ToSharpDX())) - { - var renderer = new Renderer(target); - renderer.Render(visual, null, Matrix.Identity, Matrix.CreateScale(scale)); - - var result = new BitmapBrush(this.renderTarget, target.Bitmap); - return new Disposable(result, target); + return new SolidColorBrushImpl(null, this.renderTarget, destinationSize); } } } diff --git a/src/Windows/Perspex.Direct2D1/Media/LinearGradientBrushImpl.cs b/src/Windows/Perspex.Direct2D1/Media/LinearGradientBrushImpl.cs new file mode 100644 index 0000000000..81242c9e0b --- /dev/null +++ b/src/Windows/Perspex.Direct2D1/Media/LinearGradientBrushImpl.cs @@ -0,0 +1,43 @@ +namespace Perspex.Direct2D1.Media +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public class LinearGradientBrushImpl : BrushImpl + { + public LinearGradientBrushImpl(Perspex.Media.LinearGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, Size destinationSize) + : base(brush, target, destinationSize) + { + if (brush != null) + { + var gradientStops = brush.GradientStops.Select(s => new SharpDX.Direct2D1.GradientStop { Color = s.Color.ToDirect2D(), Position = (float)s.Offset }).ToArray(); + + Point startPoint = new Point(0, 0); + Point endPoint = new Point(0, 0); + + switch (brush.MappingMode) + { + case Perspex.Media.BrushMappingMode.Absolute: + // TODO: + + break; + case Perspex.Media.BrushMappingMode.RelativeToBoundingBox: + startPoint = new Point(brush.StartPoint.X * destinationSize.Width, brush.StartPoint.Y * destinationSize.Height); + endPoint = new Point(brush.EndPoint.X * destinationSize.Width, brush.EndPoint.Y * destinationSize.Height); + + break; + } + + this.PlatformBrush = new SharpDX.Direct2D1.LinearGradientBrush( + target, + new SharpDX.Direct2D1.LinearGradientBrushProperties { StartPoint = startPoint.ToSharpDX(), EndPoint = endPoint.ToSharpDX() }, + new SharpDX.Direct2D1.BrushProperties { Opacity = (float)brush.Opacity, Transform = target.Transform }, + new SharpDX.Direct2D1.GradientStopCollection(target, gradientStops, brush.SpreadMethod.ToDirect2D()) + ); + } + } + } +} diff --git a/src/Windows/Perspex.Direct2D1/Media/PerspexTextRenderer.cs b/src/Windows/Perspex.Direct2D1/Media/PerspexTextRenderer.cs index 9319c86328..264b5d45c0 100644 --- a/src/Windows/Perspex.Direct2D1/Media/PerspexTextRenderer.cs +++ b/src/Windows/Perspex.Direct2D1/Media/PerspexTextRenderer.cs @@ -53,7 +53,7 @@ namespace Perspex.Direct2D1.Media // TODO: Work out how to get the size below rather than passing new Size(). var brush = (wrapper == null) ? this.foreground : - this.context.CreateBrush(wrapper.Brush, new Size()); + this.context.CreateBrush(wrapper.Brush, new Size()).PlatformBrush; this.renderTarget.DrawGlyphRun( new Vector2(baselineOriginX, baselineOriginY), diff --git a/src/Windows/Perspex.Direct2D1/Media/SolidColorBrushImpl.cs b/src/Windows/Perspex.Direct2D1/Media/SolidColorBrushImpl.cs new file mode 100644 index 0000000000..949b8e72be --- /dev/null +++ b/src/Windows/Perspex.Direct2D1/Media/SolidColorBrushImpl.cs @@ -0,0 +1,17 @@ +namespace Perspex.Direct2D1.Media +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public class SolidColorBrushImpl : BrushImpl + { + public SolidColorBrushImpl(Perspex.Media.SolidColorBrush brush, SharpDX.Direct2D1.RenderTarget target, Size destinationSize) + : base(brush, target, destinationSize) + { + this.PlatformBrush = new SharpDX.Direct2D1.SolidColorBrush(target, brush?.Color.ToDirect2D() ?? new SharpDX.Color4()); + } + } +} diff --git a/src/Windows/Perspex.Direct2D1/Perspex.Direct2D1.csproj b/src/Windows/Perspex.Direct2D1/Perspex.Direct2D1.csproj index 99e3b2e643..11053fdc40 100644 --- a/src/Windows/Perspex.Direct2D1/Perspex.Direct2D1.csproj +++ b/src/Windows/Perspex.Direct2D1/Perspex.Direct2D1.csproj @@ -76,11 +76,14 @@ + + + diff --git a/src/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs index 54c0da9c06..abebb62a3c 100644 --- a/src/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs @@ -34,6 +34,15 @@ namespace Perspex.Direct2D1 return new Size2F((float)p.Width, (float)p.Height); } + public static SharpDX.Direct2D1.ExtendMode ToDirect2D(this Perspex.Media.GradientSpreadMethod spreadMethod) + { + if (spreadMethod == Perspex.Media.GradientSpreadMethod.Pad) + return ExtendMode.Clamp; + else if (spreadMethod == Perspex.Media.GradientSpreadMethod.Reflect) + return ExtendMode.Mirror; + else + return ExtendMode.Wrap; + } /// /// Converts a pen to a Direct2D stroke style. ///