Browse Source

Ported Track layout code from WPF.

To fix problem with scrollbar layout when the scrollbar range was large.

NOTE: This commit adds `Track.IsDirectionReversed` and modifies the default direction of vertical tracks to be the reverse of before in order to match WPF.
pull/3065/head
Steven Kirk 7 years ago
parent
commit
9a9ef657e2
  1. 318
      src/Avalonia.Controls/Primitives/Track.cs
  2. 3
      src/Avalonia.Themes.Default/ScrollBar.xaml
  3. 2
      src/Avalonia.Themes.Default/Slider.xaml

318
src/Avalonia.Controls/Primitives/Track.cs

@ -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;
}
}
// Layout the pieces of track
var offset = new Point();
var pieceSize = arrangeSize;
var isDirectionReversed = IsDirectionReversed;
if (isVertical)
{
CoerceLength(ref decreaseButtonLength, arrangeSize.Height);
CoerceLength(ref increaseButtonLength, arrangeSize.Height);
CoerceLength(ref thumbLength, arrangeSize.Height);
var remaining = finalSize.Width - thumbWidth;
var firstWidth = range <= 0 ? 0 : remaining * offset / range;
offset = offset.WithY(isDirectionReversed ? decreaseButtonLength + thumbLength : 0.0);
pieceSize = pieceSize.WithHeight(increaseButtonLength);
if (decreaseButton != null)
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 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)
{
IsVisible = false;
ThumbCenterOffset = Double.NaN;
Density = Double.NaN;
decreaseButtonLength = 0.0;
increaseButtonLength = 0.0;
return false; // don't arrange
}
else
{
IsVisible = true;
}
return finalSize;
// 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,26 +408,10 @@ namespace Avalonia.Controls.Primitives
private void ThumbDragged(object sender, VectorEventArgs e)
{
double range = Maximum - Minimum;
double value = Value;
double offset;
if (Orientation == Orientation.Horizontal)
{
offset = e.Vector.X / ((Bounds.Size.Width - Thumb.Bounds.Size.Width) / range);
}
else
{
offset = e.Vector.Y * (range / (Bounds.Size.Height - Thumb.Bounds.Size.Height));
}
if (!double.IsNaN(offset) && !double.IsInfinity(offset))
{
value += offset;
value = Math.Max(value, Minimum);
value = Math.Min(value, Maximum);
Value = value;
}
Value = MathUtilities.Clamp(
Value + ValueFromDistance(e.Vector.X, e.Vector.Y),
Minimum,
Maximum);
}
}
}

3
src/Avalonia.Themes.Default/ScrollBar.xaml

@ -19,7 +19,8 @@
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
ViewportSize="{TemplateBinding ViewportSize}"
Orientation="{TemplateBinding Orientation}">
Orientation="{TemplateBinding Orientation}"
IsDirectionReversed="True">
<Track.DecreaseButton>
<RepeatButton Name="PART_PageUpButton"
Classes="repeattrack"

2
src/Avalonia.Themes.Default/Slider.xaml

@ -46,7 +46,7 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Name="TrackBackground" Grid.Column="1" Width="4" Margin="0,6" HorizontalAlignment="Center"/>
<Track Name="PART_Track" Grid.Column="1" Orientation="Vertical">
<Track Name="PART_Track" Grid.Column="1" Orientation="Vertical" IsDirectionReversed="True">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Classes="repeattrack" />

Loading…
Cancel
Save