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 @@
-
+
+
+
+
+