|
|
|
@ -1,10 +1,13 @@ |
|
|
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
|
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
|
|
// Portions of this source file are adapted from the Windows Presentation Foundation project.
|
|
|
|
// (https://github.com/dotnet/wpf/)
|
|
|
|
//
|
|
|
|
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|
|
|
|
|
|
|
using System; |
|
|
|
using Avalonia.Input; |
|
|
|
using Avalonia.Layout; |
|
|
|
using Avalonia.Metadata; |
|
|
|
using Avalonia.Utilities; |
|
|
|
|
|
|
|
namespace Avalonia.Controls.Primitives |
|
|
|
{ |
|
|
|
@ -34,6 +37,9 @@ namespace Avalonia.Controls.Primitives |
|
|
|
public static readonly StyledProperty<Button> DecreaseButtonProperty = |
|
|
|
AvaloniaProperty.Register<Track, Button>(nameof(DecreaseButton)); |
|
|
|
|
|
|
|
public static readonly StyledProperty<bool> IsDirectionReversedProperty = |
|
|
|
AvaloniaProperty.Register<Track, bool>(nameof(IsDirectionReversed)); |
|
|
|
|
|
|
|
private double _minimum; |
|
|
|
private double _maximum = 100.0; |
|
|
|
private double _value; |
|
|
|
@ -97,110 +103,268 @@ namespace Avalonia.Controls.Primitives |
|
|
|
set { SetValue(DecreaseButtonProperty, value); } |
|
|
|
} |
|
|
|
|
|
|
|
public bool IsDirectionReversed |
|
|
|
{ |
|
|
|
get { return GetValue(IsDirectionReversedProperty); } |
|
|
|
set { SetValue(IsDirectionReversedProperty, value); } |
|
|
|
} |
|
|
|
|
|
|
|
private double ThumbCenterOffset { get; set; } |
|
|
|
private double Density { get; set; } |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Calculates the distance along the <see cref="Thumb"/> of a specified point along the
|
|
|
|
/// track.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="point">The specified point.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// The distance between the Thumb and the specified pt value.
|
|
|
|
/// </returns>
|
|
|
|
public virtual double ValueFromPoint(Point point) |
|
|
|
{ |
|
|
|
double val; |
|
|
|
|
|
|
|
// Find distance from center of thumb to given point.
|
|
|
|
if (Orientation == Orientation.Horizontal) |
|
|
|
{ |
|
|
|
val = Value + ValueFromDistance(point.X - ThumbCenterOffset, point.Y - (Bounds.Height * 0.5)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
val = Value + ValueFromDistance(point.X - (Bounds.Width * 0.5), point.Y - ThumbCenterOffset); |
|
|
|
} |
|
|
|
|
|
|
|
return Math.Max(Minimum, Math.Min(Maximum, val)); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Calculates the change in the <see cref="Value"/> of the <see cref="Track"/> when the
|
|
|
|
/// <see cref="Thumb"/> moves.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="horizontal">The horizontal displacement of the thumb.</param>
|
|
|
|
/// <param name="vertical">The vertical displacement of the thumb.</param>
|
|
|
|
public virtual double ValueFromDistance(double horizontal, double vertical) |
|
|
|
{ |
|
|
|
double scale = IsDirectionReversed ? -1 : 1; |
|
|
|
|
|
|
|
if (Orientation == Orientation.Horizontal) |
|
|
|
{ |
|
|
|
return scale * horizontal * Density; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Increases in y cause decreases in Sliders value
|
|
|
|
return -1 * scale * vertical * Density; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected override Size MeasureOverride(Size availableSize) |
|
|
|
{ |
|
|
|
var thumb = Thumb; |
|
|
|
Size desiredSize = new Size(0.0, 0.0); |
|
|
|
|
|
|
|
if (thumb != null) |
|
|
|
// Only measure thumb.
|
|
|
|
// Repeat buttons will be sized based on thumb
|
|
|
|
if (Thumb != null) |
|
|
|
{ |
|
|
|
thumb.Measure(availableSize); |
|
|
|
Thumb.Measure(availableSize); |
|
|
|
desiredSize = Thumb.DesiredSize; |
|
|
|
} |
|
|
|
|
|
|
|
if (Orientation == Orientation.Horizontal) |
|
|
|
{ |
|
|
|
return new Size(0, thumb.DesiredSize.Height); |
|
|
|
} |
|
|
|
if (!double.IsNaN(ViewportSize)) |
|
|
|
{ |
|
|
|
// ScrollBar can shrink to 0 in the direction of scrolling
|
|
|
|
if (Orientation == Orientation.Vertical) |
|
|
|
desiredSize = desiredSize.WithHeight(0.0); |
|
|
|
else |
|
|
|
{ |
|
|
|
return new Size(thumb.DesiredSize.Width, 0); |
|
|
|
} |
|
|
|
desiredSize = desiredSize.WithWidth(0.0); |
|
|
|
} |
|
|
|
|
|
|
|
return base.MeasureOverride(availableSize); |
|
|
|
return desiredSize; |
|
|
|
} |
|
|
|
|
|
|
|
protected override Size ArrangeOverride(Size finalSize) |
|
|
|
protected override Size ArrangeOverride(Size arrangeSize) |
|
|
|
{ |
|
|
|
var thumb = Thumb; |
|
|
|
var increaseButton = IncreaseButton; |
|
|
|
var decreaseButton = DecreaseButton; |
|
|
|
|
|
|
|
var range = Maximum - Minimum; |
|
|
|
var offset = Math.Min(Value - Minimum, range); |
|
|
|
var viewportSize = ViewportSize; |
|
|
|
var extent = range + viewportSize; |
|
|
|
double decreaseButtonLength, thumbLength, increaseButtonLength; |
|
|
|
var isVertical = Orientation == Orientation.Vertical; |
|
|
|
var viewportSize = Math.Max(0.0, ViewportSize); |
|
|
|
|
|
|
|
if (Orientation == Orientation.Horizontal) |
|
|
|
// If viewport is NaN, compute thumb's size based on its desired size,
|
|
|
|
// otherwise compute the thumb base on the viewport and extent properties
|
|
|
|
if (double.IsNaN(ViewportSize)) |
|
|
|
{ |
|
|
|
double thumbWidth = 0; |
|
|
|
|
|
|
|
if (double.IsNaN(viewportSize)) |
|
|
|
{ |
|
|
|
thumbWidth = thumb?.DesiredSize.Width ?? 0; |
|
|
|
} |
|
|
|
else if (extent > 0) |
|
|
|
ComputeSliderLengths(arrangeSize, isVertical, out decreaseButtonLength, out thumbLength, out increaseButtonLength); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Don't arrange if there's not enough content or the track is too small
|
|
|
|
if (!ComputeScrollBarLengths(arrangeSize, viewportSize, isVertical, out decreaseButtonLength, out thumbLength, out increaseButtonLength)) |
|
|
|
{ |
|
|
|
thumbWidth = finalSize.Width * viewportSize / extent; |
|
|
|
return arrangeSize; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var remaining = finalSize.Width - thumbWidth; |
|
|
|
var firstWidth = range <= 0 ? 0 : remaining * offset / range; |
|
|
|
// Layout the pieces of track
|
|
|
|
var offset = new Point(); |
|
|
|
var pieceSize = arrangeSize; |
|
|
|
var isDirectionReversed = IsDirectionReversed; |
|
|
|
|
|
|
|
if (decreaseButton != null) |
|
|
|
if (isVertical) |
|
|
|
{ |
|
|
|
CoerceLength(ref decreaseButtonLength, arrangeSize.Height); |
|
|
|
CoerceLength(ref increaseButtonLength, arrangeSize.Height); |
|
|
|
CoerceLength(ref thumbLength, arrangeSize.Height); |
|
|
|
|
|
|
|
offset = offset.WithY(isDirectionReversed ? decreaseButtonLength + thumbLength : 0.0); |
|
|
|
pieceSize = pieceSize.WithHeight(increaseButtonLength); |
|
|
|
|
|
|
|
if (IncreaseButton != null) |
|
|
|
{ |
|
|
|
decreaseButton.Arrange(new Rect(0, 0, firstWidth, finalSize.Height)); |
|
|
|
IncreaseButton.Arrange(new Rect(offset, pieceSize)); |
|
|
|
} |
|
|
|
|
|
|
|
if (thumb != null) |
|
|
|
offset = offset.WithY(isDirectionReversed ? 0.0 : increaseButtonLength + thumbLength); |
|
|
|
pieceSize = pieceSize.WithHeight(decreaseButtonLength); |
|
|
|
|
|
|
|
if (DecreaseButton != null) |
|
|
|
{ |
|
|
|
thumb.Arrange(new Rect(firstWidth, 0, thumbWidth, finalSize.Height)); |
|
|
|
DecreaseButton.Arrange(new Rect(offset, pieceSize)); |
|
|
|
} |
|
|
|
|
|
|
|
if (increaseButton != null) |
|
|
|
offset = offset.WithY(isDirectionReversed ? decreaseButtonLength : increaseButtonLength); |
|
|
|
pieceSize = pieceSize.WithHeight(thumbLength); |
|
|
|
|
|
|
|
if (Thumb != null) |
|
|
|
{ |
|
|
|
increaseButton.Arrange(new Rect( |
|
|
|
firstWidth + thumbWidth, |
|
|
|
0, |
|
|
|
Math.Max(0, remaining - firstWidth), |
|
|
|
finalSize.Height)); |
|
|
|
Thumb.Arrange(new Rect(offset, pieceSize)); |
|
|
|
} |
|
|
|
|
|
|
|
ThumbCenterOffset = offset.Y + (thumbLength * 0.5); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
double thumbHeight = 0; |
|
|
|
CoerceLength(ref decreaseButtonLength, arrangeSize.Width); |
|
|
|
CoerceLength(ref increaseButtonLength, arrangeSize.Width); |
|
|
|
CoerceLength(ref thumbLength, arrangeSize.Width); |
|
|
|
|
|
|
|
if (double.IsNaN(viewportSize)) |
|
|
|
{ |
|
|
|
thumbHeight = thumb?.DesiredSize.Height ?? 0; |
|
|
|
} |
|
|
|
else if (extent > 0) |
|
|
|
offset = offset.WithY(isDirectionReversed ? increaseButtonLength + thumbLength : 0.0); |
|
|
|
pieceSize = pieceSize.WithWidth(decreaseButtonLength); |
|
|
|
|
|
|
|
if (DecreaseButton != null) |
|
|
|
{ |
|
|
|
thumbHeight = finalSize.Height * viewportSize / extent; |
|
|
|
DecreaseButton.Arrange(new Rect(offset, pieceSize)); |
|
|
|
} |
|
|
|
|
|
|
|
var remaining = finalSize.Height - thumbHeight; |
|
|
|
var firstHeight = range <= 0 ? 0 : remaining * offset / range; |
|
|
|
offset = offset.WithX(isDirectionReversed ? 0.0 : decreaseButtonLength + thumbLength); |
|
|
|
pieceSize = pieceSize.WithWidth(increaseButtonLength); |
|
|
|
|
|
|
|
if (decreaseButton != null) |
|
|
|
if (IncreaseButton != null) |
|
|
|
{ |
|
|
|
decreaseButton.Arrange(new Rect(0, 0, finalSize.Width, firstHeight)); |
|
|
|
IncreaseButton.Arrange(new Rect(offset, pieceSize)); |
|
|
|
} |
|
|
|
|
|
|
|
if (thumb != null) |
|
|
|
{ |
|
|
|
thumb.Arrange(new Rect(0, firstHeight, finalSize.Width, thumbHeight)); |
|
|
|
} |
|
|
|
offset = offset.WithX(isDirectionReversed ? increaseButtonLength : decreaseButtonLength); |
|
|
|
pieceSize = pieceSize.WithWidth(thumbLength); |
|
|
|
|
|
|
|
if (increaseButton != null) |
|
|
|
if (Thumb != null) |
|
|
|
{ |
|
|
|
increaseButton.Arrange(new Rect( |
|
|
|
0, |
|
|
|
firstHeight + thumbHeight, |
|
|
|
finalSize.Width, |
|
|
|
Math.Max(remaining - firstHeight, 0))); |
|
|
|
Thumb.Arrange(new Rect(offset, pieceSize)); |
|
|
|
} |
|
|
|
|
|
|
|
ThumbCenterOffset = offset.X + (thumbLength * 0.5); |
|
|
|
} |
|
|
|
|
|
|
|
return finalSize; |
|
|
|
return arrangeSize; |
|
|
|
} |
|
|
|
|
|
|
|
private static void CoerceLength(ref double componentLength, double trackLength) |
|
|
|
{ |
|
|
|
if (componentLength < 0) |
|
|
|
{ |
|
|
|
componentLength = 0.0; |
|
|
|
} |
|
|
|
else if (componentLength > trackLength || double.IsNaN(componentLength)) |
|
|
|
{ |
|
|
|
componentLength = trackLength; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void ComputeSliderLengths(Size arrangeSize, bool isVertical, out double decreaseButtonLength, out double thumbLength, out double increaseButtonLength) |
|
|
|
{ |
|
|
|
double min = Minimum; |
|
|
|
double range = Math.Max(0.0, Maximum - min); |
|
|
|
double offset = Math.Min(range, Value - min); |
|
|
|
|
|
|
|
double trackLength; |
|
|
|
|
|
|
|
// Compute thumb size
|
|
|
|
if (isVertical) |
|
|
|
{ |
|
|
|
trackLength = arrangeSize.Height; |
|
|
|
thumbLength = Thumb == null ? 0 : Thumb.DesiredSize.Height; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
trackLength = arrangeSize.Width; |
|
|
|
thumbLength = Thumb == null ? 0 : Thumb.DesiredSize.Width; |
|
|
|
} |
|
|
|
|
|
|
|
CoerceLength(ref thumbLength, trackLength); |
|
|
|
|
|
|
|
double remainingTrackLength = trackLength - thumbLength; |
|
|
|
|
|
|
|
decreaseButtonLength = remainingTrackLength * offset / range; |
|
|
|
CoerceLength(ref decreaseButtonLength, remainingTrackLength); |
|
|
|
|
|
|
|
increaseButtonLength = remainingTrackLength - decreaseButtonLength; |
|
|
|
CoerceLength(ref increaseButtonLength, remainingTrackLength); |
|
|
|
|
|
|
|
Density = range / remainingTrackLength; |
|
|
|
} |
|
|
|
|
|
|
|
private bool ComputeScrollBarLengths(Size arrangeSize, double viewportSize, bool isVertical, out double decreaseButtonLength, out double thumbLength, out double increaseButtonLength) |
|
|
|
{ |
|
|
|
var min = Minimum; |
|
|
|
var range = Math.Max(0.0, Maximum - min); |
|
|
|
var offset = Math.Min(range, Value - min); |
|
|
|
var extent = Math.Max(0.0, range) + viewportSize; |
|
|
|
var trackLength = isVertical ? arrangeSize.Height : arrangeSize.Width; |
|
|
|
double thumbMinLength = 10; |
|
|
|
|
|
|
|
thumbLength = trackLength * viewportSize / extent; |
|
|
|
CoerceLength(ref thumbLength, trackLength); |
|
|
|
thumbLength = Math.Max(thumbMinLength, thumbLength); |
|
|
|
|
|
|
|
// If we don't have enough content to scroll, disable the track.
|
|
|
|
var notEnoughContentToScroll = MathUtilities.LessThanOrClose(range, 0.0); |
|
|
|
var thumbLongerThanTrack = thumbLength > trackLength; |
|
|
|
|
|
|
|
// if there's not enough content or the thumb is longer than the track,
|
|
|
|
// hide the track and don't arrange the pieces
|
|
|
|
if (notEnoughContentToScroll || thumbLongerThanTrack) |
|
|
|
{ |
|
|
|
ShowChildren(false); |
|
|
|
ThumbCenterOffset = Double.NaN; |
|
|
|
Density = Double.NaN; |
|
|
|
decreaseButtonLength = 0.0; |
|
|
|
increaseButtonLength = 0.0; |
|
|
|
return false; // don't arrange
|
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
ShowChildren(true); |
|
|
|
} |
|
|
|
|
|
|
|
// Compute lengths of increase and decrease button
|
|
|
|
double remainingTrackLength = trackLength - thumbLength; |
|
|
|
decreaseButtonLength = remainingTrackLength * offset / range; |
|
|
|
CoerceLength(ref decreaseButtonLength, remainingTrackLength); |
|
|
|
|
|
|
|
increaseButtonLength = remainingTrackLength - decreaseButtonLength; |
|
|
|
CoerceLength(ref increaseButtonLength, remainingTrackLength); |
|
|
|
|
|
|
|
Density = range / remainingTrackLength; |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
private void ThumbChanged(AvaloniaPropertyChangedEventArgs e) |
|
|
|
@ -244,25 +408,29 @@ namespace Avalonia.Controls.Primitives |
|
|
|
|
|
|
|
private void ThumbDragged(object sender, VectorEventArgs e) |
|
|
|
{ |
|
|
|
double range = Maximum - Minimum; |
|
|
|
double value = Value; |
|
|
|
double offset; |
|
|
|
Value = MathUtilities.Clamp( |
|
|
|
Value + ValueFromDistance(e.Vector.X, e.Vector.Y), |
|
|
|
Minimum, |
|
|
|
Maximum); |
|
|
|
} |
|
|
|
|
|
|
|
if (Orientation == Orientation.Horizontal) |
|
|
|
private void ShowChildren(bool visible) |
|
|
|
{ |
|
|
|
// WPF sets Visible = Hidden here but we don't have that, and setting IsVisible = false
|
|
|
|
// will cause us to stop being laid out. Instead show/hide the child controls.
|
|
|
|
if (Thumb != null) |
|
|
|
{ |
|
|
|
offset = e.Vector.X / ((Bounds.Size.Width - Thumb.Bounds.Size.Width) / range); |
|
|
|
Thumb.IsVisible = visible; |
|
|
|
} |
|
|
|
else |
|
|
|
|
|
|
|
if (IncreaseButton != null) |
|
|
|
{ |
|
|
|
offset = e.Vector.Y * (range / (Bounds.Size.Height - Thumb.Bounds.Size.Height)); |
|
|
|
IncreaseButton.IsVisible = visible; |
|
|
|
} |
|
|
|
|
|
|
|
if (!double.IsNaN(offset) && !double.IsInfinity(offset)) |
|
|
|
if (DecreaseButton != null) |
|
|
|
{ |
|
|
|
value += offset; |
|
|
|
value = Math.Max(value, Minimum); |
|
|
|
value = Math.Min(value, Maximum); |
|
|
|
Value = value; |
|
|
|
DecreaseButton.IsVisible = visible; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|