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. // Portions of this source file are adapted from the Windows Presentation Foundation project.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // (https://github.com/dotnet/wpf/)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -34,6 +37,9 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<Button> DecreaseButtonProperty = public static readonly StyledProperty<Button> DecreaseButtonProperty =
AvaloniaProperty.Register<Track, Button>(nameof(DecreaseButton)); AvaloniaProperty.Register<Track, Button>(nameof(DecreaseButton));
public static readonly StyledProperty<bool> IsDirectionReversedProperty =
AvaloniaProperty.Register<Track, bool>(nameof(IsDirectionReversed));
private double _minimum; private double _minimum;
private double _maximum = 100.0; private double _maximum = 100.0;
private double _value; private double _value;
@ -97,110 +103,268 @@ namespace Avalonia.Controls.Primitives
set { SetValue(DecreaseButtonProperty, value); } 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) 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) if (!double.IsNaN(ViewportSize))
{ {
return new Size(0, thumb.DesiredSize.Height); // ScrollBar can shrink to 0 in the direction of scrolling
} if (Orientation == Orientation.Vertical)
desiredSize = desiredSize.WithHeight(0.0);
else else
{ desiredSize = desiredSize.WithWidth(0.0);
return new Size(thumb.DesiredSize.Width, 0);
}
} }
return base.MeasureOverride(availableSize); return desiredSize;
} }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size arrangeSize)
{ {
var thumb = Thumb; double decreaseButtonLength, thumbLength, increaseButtonLength;
var increaseButton = IncreaseButton; var isVertical = Orientation == Orientation.Vertical;
var decreaseButton = DecreaseButton; var viewportSize = Math.Max(0.0, ViewportSize);
var range = Maximum - Minimum;
var offset = Math.Min(Value - Minimum, range);
var viewportSize = ViewportSize;
var extent = range + 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; ComputeSliderLengths(arrangeSize, isVertical, out decreaseButtonLength, out thumbLength, out increaseButtonLength);
}
if (double.IsNaN(viewportSize)) else
{ {
thumbWidth = thumb?.DesiredSize.Width ?? 0; // 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))
else if (extent > 0)
{ {
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; offset = offset.WithY(isDirectionReversed ? decreaseButtonLength + thumbLength : 0.0);
var firstWidth = range <= 0 ? 0 : remaining * offset / range; 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( Thumb.Arrange(new Rect(offset, pieceSize));
firstWidth + thumbWidth,
0,
Math.Max(0, remaining - firstWidth),
finalSize.Height));
} }
ThumbCenterOffset = offset.Y + (thumbLength * 0.5);
} }
else else
{ {
double thumbHeight = 0; CoerceLength(ref decreaseButtonLength, arrangeSize.Width);
CoerceLength(ref increaseButtonLength, arrangeSize.Width);
CoerceLength(ref thumbLength, arrangeSize.Width);
if (double.IsNaN(viewportSize)) offset = offset.WithY(isDirectionReversed ? increaseButtonLength + thumbLength : 0.0);
{ pieceSize = pieceSize.WithWidth(decreaseButtonLength);
thumbHeight = thumb?.DesiredSize.Height ?? 0;
} if (DecreaseButton != null)
else if (extent > 0)
{ {
thumbHeight = finalSize.Height * viewportSize / extent; DecreaseButton.Arrange(new Rect(offset, pieceSize));
} }
var remaining = finalSize.Height - thumbHeight; offset = offset.WithX(isDirectionReversed ? 0.0 : decreaseButtonLength + thumbLength);
var firstHeight = range <= 0 ? 0 : remaining * offset / range; 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) offset = offset.WithX(isDirectionReversed ? increaseButtonLength : decreaseButtonLength);
{ pieceSize = pieceSize.WithWidth(thumbLength);
thumb.Arrange(new Rect(0, firstHeight, finalSize.Width, thumbHeight));
}
if (increaseButton != null) if (Thumb != null)
{ {
increaseButton.Arrange(new Rect( Thumb.Arrange(new Rect(offset, pieceSize));
0,
firstHeight + thumbHeight,
finalSize.Width,
Math.Max(remaining - firstHeight, 0)));
} }
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) private void ThumbChanged(AvaloniaPropertyChangedEventArgs e)
@ -244,26 +408,10 @@ namespace Avalonia.Controls.Primitives
private void ThumbDragged(object sender, VectorEventArgs e) private void ThumbDragged(object sender, VectorEventArgs e)
{ {
double range = Maximum - Minimum; Value = MathUtilities.Clamp(
double value = Value; Value + ValueFromDistance(e.Vector.X, e.Vector.Y),
double offset; Minimum,
Maximum);
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;
}
} }
} }
} }

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

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

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

@ -46,7 +46,7 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Border Name="TrackBackground" Grid.Column="1" Width="4" Margin="0,6" HorizontalAlignment="Center"/> <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> <Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton" <RepeatButton Name="PART_DecreaseButton"
Classes="repeattrack" /> Classes="repeattrack" />

Loading…
Cancel
Save