diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs new file mode 100644 index 0000000000..1cf98bd169 --- /dev/null +++ b/src/Avalonia.Controls/TickBar.cs @@ -0,0 +1,593 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + + /// + /// Enum which describes how to position the TickBar. + /// + public enum TickBarPlacement + { + /// + /// Position this tick at the left of target element. + /// + Left, + + /// + /// Position this tick at the top of target element. + /// + Top, + + /// + /// Position this tick at the right of target element. + /// + Right, + + /// + /// Position this tick at the bottom of target element. + /// + Bottom, + + // NOTE: if you add or remove any values in this enum, be sure to update TickBar.IsValidTickBarPlacement() + }; + + + /// + /// TickBar is an element that use for drawing Slider's Ticks. + /// + public class TickBar : TemplatedControl + { + static TickBar() + { + AffectsRender(ReservedSpaceProperty, MaximumProperty, MinimumProperty, OrientationProperty, TickFrequencyProperty); + } + + public TickBar() : base() + { + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty FillProperty = + AvaloniaProperty.Register(nameof(Fill)); + + /// + /// Brush used to fill the TickBar's Ticks. + /// + public IBrush Fill + { + get { return GetValue(FillProperty); } + set { SetValue(FillProperty, value); } + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), 0d); + + /// + /// Logical position where the Minimum Tick will be drawn + /// + public double Minimum + { + get { return GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), 0d); + + /// + /// Logical position where the Maximum Tick will be drawn + /// + public double Maximum + { + get { return GetValue(MaximumProperty); } + set { SetValue(MaximumProperty, value); } + } + +/* + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectionStartProperty = + AvaloniaProperty.Register(nameof(SelectionStart), 0d); + + /// + /// Logical position where the SelectionStart Tick will be drawn + /// + public double SelectionStart + { + get { return GetValue(SelectionStartProperty); } + set { SetValue(SelectionStartProperty, value); } + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectionEndProperty = + AvaloniaProperty.Register(nameof(SelectionEnd), 0d); + + /// + /// Logical position where the SelectionEnd Tick will be drawn + /// + public double SelectionEnd + { + get { return GetValue(SelectionEndProperty); } + set { SetValue(SelectionEndProperty, value); } + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsSelectionRangeEnabledProperty = + AvaloniaProperty.Register(nameof(IsSelectionRangeEnabled)); + + /// + /// IsSelectionRangeEnabled specifies whether to draw SelectionStart Tick and SelectionEnd Tick or not. + /// + public bool IsSelectionRangeEnabled + { + get { return GetValue(IsSelectionRangeEnabledProperty); } + set { SetValue(IsSelectionRangeEnabledProperty, value); } + } +*/ + + /// + /// Defines the property. + /// + public static readonly StyledProperty TickFrequencyProperty = + AvaloniaProperty.Register(nameof(TickFrequency), 0d); + + /// + /// TickFrequency property defines how the tick will be drawn. + /// + public double TickFrequency + { + get { return GetValue(TickFrequencyProperty); } + set { SetValue(TickFrequencyProperty, value); } + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register(nameof(Orientation)); + + /// + /// TickBar parent's orientation. + /// + public Orientation Orientation + { + get { return GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty> TicksProperty = + AvaloniaProperty.Register>(nameof(Ticks)); + + /// + /// The Ticks property contains collection of value of type Double which + /// are the logical positions use to draw the ticks. + /// The property value is a . + /// + public List Ticks + { + get { return GetValue(TicksProperty); } + set { SetValue(TicksProperty, value); } + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsDirectionReversedProperty = + AvaloniaProperty.Register(nameof(IsDirectionReversed), false); + + /// + /// The IsDirectionReversed property defines the direction of value incrementation. + /// By default, if Tick's orientation is Horizontal, ticks will be drawn from left to right. + /// (And, bottom to top for Vertical orientation). + /// If IsDirectionReversed is 'true' the direction of the drawing will be in opposite direction. + /// + public bool IsDirectionReversed + { + get { return GetValue(IsDirectionReversedProperty); } + set { SetValue(IsDirectionReversedProperty, value); } + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementProperty = + AvaloniaProperty.Register(nameof(Placement), 0d); + + + /// + /// Placement property specified how the Tick will be placed. + /// This property affects the way ticks are drawn. + /// This property has type of . + /// + public TickBarPlacement Placement + { + get { return GetValue(PlacementProperty); } + set { SetValue(PlacementProperty, value); } + } + + private static bool IsValidTickBarPlacement(object o) + { + TickBarPlacement placement = (TickBarPlacement)o; + return placement == TickBarPlacement.Left || + placement == TickBarPlacement.Top || + placement == TickBarPlacement.Right || + placement == TickBarPlacement.Bottom; + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ReservedSpaceProperty = + AvaloniaProperty.Register(nameof(ReservedSpace)); + + /// + /// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or + /// tob and bottom spacing (for vertical orienation). + /// The space on both sides of TickBar is half of specified ReservedSpace. + /// This property has type of . + /// + public Rect ReservedSpace + { + get { return GetValue(ReservedSpaceProperty); } + set { SetValue(ReservedSpaceProperty, value); } + } + + + /// + /// Draw ticks. + /// Ticks can be draw in 8 diffrent ways depends on Placment property and IsDirectionReversed property. + /// + /// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and + /// SelectionStart and SelectionEnd are valid. + /// + /// The primary ticks (for Mininum and Maximum value) height will be 100% of TickBar's render size (use Width or Height + /// depends on Placement property). + /// + /// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size. + /// + /// Brush that use to fill ticks is specified by Shape.Fill property. + /// + /// Pen that use to draw ticks is specified by Shape.Pen property. + /// + public override void Render(DrawingContext dc) + { + var size = new Size(Bounds.Width, Bounds.Height); + var range = Maximum - Minimum; + var tickLen = 0.0d; // Height for Primary Tick (for Mininum and Maximum value) + var tickLen2 = 0.0d; // Height for Secondary Tick + var logicalToPhysical = 1.0; + var progression = 1.0d; + var startPoint = new Point(); + var endPoint = new Point(); + var rSpace = Orientation == Orientation.Horizontal ? ReservedSpace.Width : ReservedSpace.Height; + + // Take Thumb size in to account + double halfReservedSpace = rSpace * 0.5; + + switch (Placement) + { + case TickBarPlacement.Top: + if (MathUtilities.GreaterThanOrClose(rSpace, size.Width)) + { + return; + } + size = new Size(size.Width - rSpace, size.Height); + tickLen = -size.Height; + startPoint = new Point(halfReservedSpace, size.Height); + endPoint = new Point(halfReservedSpace + size.Width, size.Height); + logicalToPhysical = size.Width / range; + progression = 1; + break; + + case TickBarPlacement.Bottom: + if (MathUtilities.GreaterThanOrClose(rSpace, size.Width)) + { + return; + } + size = new Size(size.Width - rSpace, size.Height); + tickLen = size.Height; + startPoint = new Point(halfReservedSpace, 0d); + endPoint = new Point(halfReservedSpace + size.Width, 0d); + logicalToPhysical = size.Width / range; + progression = 1; + break; + + case TickBarPlacement.Left: + if (MathUtilities.GreaterThanOrClose(rSpace, size.Height)) + { + return; + } + size = new Size(size.Width, size.Height - rSpace); + + tickLen = -size.Width; + startPoint = new Point(size.Width, size.Height + halfReservedSpace); + endPoint = new Point(size.Width, halfReservedSpace); + logicalToPhysical = size.Height / range * -1; + progression = -1; + break; + + case TickBarPlacement.Right: + if (MathUtilities.GreaterThanOrClose(rSpace, size.Height)) + { + return; + } + + size = new Size(size.Width, size.Height - rSpace); + + tickLen = size.Width; + startPoint = new Point(0d, size.Height + halfReservedSpace); + endPoint = new Point(0d, halfReservedSpace); + logicalToPhysical = size.Height / range * -1; + progression = -1; + break; + }; + + tickLen2 = tickLen * 0.75; + + // Invert direciton of the ticks + if (IsDirectionReversed) + { + progression *= -progression; + logicalToPhysical *= -1; + + // swap startPoint & endPoint + var pt = startPoint; + startPoint = endPoint; + endPoint = pt; + } + + var pen = new Pen(Fill, 1.0d); + bool snapsToDevicePixels = UseLayoutRounding; + // var xLines = snapsToDevicePixels ? new List() : null; + // var yLines = snapsToDevicePixels ? new List() : null; + + // Is it Vertical? + if ((Placement == TickBarPlacement.Left) || (Placement == TickBarPlacement.Right)) + { + // Reduce tick interval if it is more than would be visible on the screen + double interval = TickFrequency; + if (interval > 0.0) + { + double minInterval = (Maximum - Minimum) / size.Height; + if (interval < minInterval) + { + interval = minInterval; + } + } + + // Draw Min & Max tick + dc.DrawLine(pen, startPoint, new Point(startPoint.X + tickLen, startPoint.Y)); + dc.DrawLine(pen, new Point(startPoint.X, endPoint.Y), + new Point(startPoint.X + tickLen, endPoint.Y)); + + // if (snapsToDevicePixels) + // { + // xLines.Add(startPoint.X); + // yLines.Add(startPoint.Y - 0.5); + // xLines.Add(startPoint.X + tickLen); + // yLines.Add(endPoint.Y - 0.5); + // xLines.Add(startPoint.X + tickLen2); + // } + + // This property is rarely set so let's try to avoid the GetValue + // caching of the mutable default value + var ticks = Ticks ?? null; + + // Draw ticks using specified Ticks collection + if (ticks?.Count > 0) + { + for (int i = 0; i < ticks.Count; i++) + { + if (MathUtilities.LessThanOrClose(ticks[i],Minimum) || MathUtilities.GreaterThanOrClose(ticks[i],Maximum)) + { + continue; + } + + double adjustedTick = ticks[i] - Minimum; + + double y = adjustedTick * logicalToPhysical + startPoint.Y; + dc.DrawLine(pen, + new Point(startPoint.X, y), + new Point(startPoint.X + tickLen2, y)); + + // if (snapsToDevicePixels) + // { + // yLines.Add(y - 0.5); + // } + } + } + // Draw ticks using specified TickFrequency + else if (interval > 0.0) + { + for (double i = interval; i < range; i += interval) + { + double y = i * logicalToPhysical + startPoint.Y; + + dc.DrawLine(pen, + new Point(startPoint.X, y), + new Point(startPoint.X + tickLen2, y)); + + // if (snapsToDevicePixels) + // { + // yLines.Add(y - 0.5); + // } + } + } + + // Draw Selection Ticks + // if (IsSelectionRangeEnabled) + // { + // double y0 = (SelectionStart - Minimum) * logicalToPhysical + startPoint.Y; + // Point pt0 = new Point(startPoint.X, y0); + // Point pt1 = new Point(startPoint.X + tickLen2, y0); + // Point pt2 = new Point(startPoint.X + tickLen2, y0 + Math.Abs(tickLen2) * progression); + + // PathSegment[] segments = new PathSegment[] { + // new LineSegment(pt2, true), + // new LineSegment(pt0, true), + // }; + // PathGeometry geo = new PathGeometry(new PathFigure[] { new PathFigure(pt1, segments, true) }); + + // dc.DrawGeometry(Fill, pen, geo); + + // y0 = (SelectionEnd - Minimum) * logicalToPhysical + startPoint.Y; + // pt0 = new Point(startPoint.X, y0); + // pt1 = new Point(startPoint.X + tickLen2, y0); + // pt2 = new Point(startPoint.X + tickLen2, y0 - Math.Abs(tickLen2) * progression); + + // segments = new PathSegment[] { + // new LineSegment(pt2, true), + // new LineSegment(pt0, true), + // }; + // geo = new PathGeometry(new PathFigure[] { new PathFigure(pt1, segments, true) }); + // dc.DrawGeometry(Fill, pen, geo); + // } + } + else // Placement == Top || Placement == Bottom + { + // Reduce tick interval if it is more than would be visible on the screen + double interval = TickFrequency; + if (interval > 0.0) + { + double minInterval = (Maximum - Minimum) / size.Width; + if (interval < minInterval) + { + interval = minInterval; + } + } + + // Draw Min & Max tick + dc.DrawLine(pen, startPoint, new Point(startPoint.X, startPoint.Y + tickLen)); + dc.DrawLine(pen, new Point(endPoint.X, startPoint.Y), + new Point(endPoint.X, startPoint.Y + tickLen)); + + // if (snapsToDevicePixels) + // { + // xLines.Add(startPoint.X - 0.5); + // yLines.Add(startPoint.Y); + // xLines.Add(startPoint.X - 0.5); + // yLines.Add(endPoint.Y + tickLen); + // yLines.Add(endPoint.Y + tickLen2); + // } + + // This property is rarely set so let's try to avoid the GetValue + // caching of the mutable default value + var ticks = Ticks ?? null; + + // Draw ticks using specified Ticks collection + if (ticks?.Count > 0) + { + for (int i = 0; i < ticks.Count; i++) + { + if (MathUtilities.LessThanOrClose(ticks[i],Minimum) || MathUtilities.GreaterThanOrClose(ticks[i],Maximum)) + { + continue; + } + double adjustedTick = ticks[i] - Minimum; + + double x = adjustedTick * logicalToPhysical + startPoint.X; + dc.DrawLine(pen, + new Point(x, startPoint.Y), + new Point(x, startPoint.Y + tickLen2)); + + // if (snapsToDevicePixels) + // { + // xLines.Add(x - 0.5); + // } + } + } + // Draw ticks using specified TickFrequency + else if (interval > 0.0) + { + for (double i = interval; i < range; i += interval) + { + double x = i * logicalToPhysical + startPoint.X; + dc.DrawLine(pen, + new Point(x, startPoint.Y), + new Point(x, startPoint.Y + tickLen2)); + + // if (snapsToDevicePixels) + // { + // xLines.Add(x - 0.5); + // } + } + } + + // Draw Selection Ticks + // if (IsSelectionRangeEnabled) + // { + // double x0 = (SelectionStart - Minimum) * logicalToPhysical + startPoint.X; + // Point pt0 = new Point(x0, startPoint.Y); + // Point pt1 = new Point(x0, startPoint.Y + tickLen2); + // Point pt2 = new Point(x0 + Math.Abs(tickLen2) * progression, startPoint.Y + tickLen2); + + // PathSegment[] segments = new PathSegment[] { + // new LineSegment(pt2, true), + // new LineSegment(pt0, true), + // }; + // PathGeometry geo = new PathGeometry(new PathFigure[] { new PathFigure(pt1, segments, true) }); + + // dc.DrawGeometry(Fill, pen, geo); + + // x0 = (SelectionEnd - Minimum) * logicalToPhysical + startPoint.X; + // pt0 = new Point(x0, startPoint.Y); + // pt1 = new Point(x0, startPoint.Y + tickLen2); + // pt2 = new Point(x0 - Math.Abs(tickLen2) * progression, startPoint.Y + tickLen2); + + // segments = new PathSegment[] { + // new LineSegment(pt2, true), + // new LineSegment(pt0, true), + // }; + // geo = new PathGeometry(new PathFigure[] { new PathFigure(pt1, segments, true) }); + // dc.DrawGeometry(Fill, pen, geo); + // } + } + + // if (snapsToDevicePixels) + // { + // xLines.Add(Bounds.Width); + // yLines.Add(Bounds.Height); + // VisualXSnappingGuidelines = xLines; + // VisualYSnappingGuidelines = yLines; + // } + } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + if (TemplatedParent is Slider parent) + { + this[~TickFrequencyProperty] = parent[~Slider.TickFrequencyProperty]; + this[~OrientationProperty] = parent[~Slider.OrientationProperty]; + this[~MinimumProperty] = parent[~Slider.MinimumProperty]; + this[~MaximumProperty] = parent[~Slider.MaximumProperty]; + + // if (parent._track != null) + // { + ReservedSpace = new Rect(0, 0, 20, 20); // HACK parent._track.Thumb[~BoundsProperty]; + // } + } + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index 75f3e97367..fff8525ebd 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -48,9 +48,9 @@ - - - + + + @@ -58,11 +58,11 @@ - + + + + + @@ -109,22 +109,22 @@ - - - + + + + + + + - - + +