瀏覽代碼

Merge branch 'master' into closing-event

Steven Kirk 7 年之前
父節點
當前提交
6727d2f4a9
共有 25 個文件被更改,包括 668 次插入27 次删除
  1. 6 0
      samples/ControlCatalog/ControlCatalog.csproj
  2. 1 0
      samples/ControlCatalog/MainView.xaml
  3. 24 0
      samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
  4. 54 0
      samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs
  5. 258 0
      src/Avalonia.Controls/ButtonSpinner.cs
  6. 2 2
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  7. 27 2
      src/Avalonia.Controls/RepeatButton.cs
  8. 174 0
      src/Avalonia.Controls/Spinner.cs
  9. 86 0
      src/Avalonia.Themes.Default/ButtonSpinner.xaml
  10. 1 0
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  11. 5 5
      src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs
  12. 3 3
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  13. 2 2
      src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs
  14. 12 0
      src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
  15. 1 1
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  16. 1 1
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  17. 2 2
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  18. 2 2
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs
  19. 5 5
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  20. 1 1
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  21. 1 1
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  22. 0 0
      tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png
  23. 0 0
      tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png
  24. 0 0
      tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png
  25. 0 0
      tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png

+ 6 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -35,6 +35,9 @@
     <EmbeddedResource Include="DecoratedWindow.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
+    <EmbeddedResource Include="Pages\ButtonSpinnerPage.xaml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
     <EmbeddedResource Include="Pages\DialogsPage.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
@@ -164,6 +167,9 @@
     <Compile Include="Pages\ToolTipPage.xaml.cs">
       <DependentUpon>ToolTipPage.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Pages\ButtonSpinnerPage.xaml.cs">
+      <DependentUpon>ButtonSpinnerPage.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Pages\ScreenPage.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>

+ 1 - 0
samples/ControlCatalog/MainView.xaml

@@ -7,6 +7,7 @@
     </TabControl.Transition>
     <TabItem Header="Border"><pages:BorderPage/></TabItem>
     <TabItem Header="Button"><pages:ButtonPage/></TabItem>
+    <TabItem Header="ButtonSpinner"><pages:ButtonSpinnerPage/></TabItem>
     <TabItem Header="Calendar"><pages:CalendarPage/></TabItem> 
     <TabItem Header="Canvas"><pages:CanvasPage/></TabItem>
     <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>

+ 24 - 0
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml

@@ -0,0 +1,24 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Classes="h1">ButtonSpinner</TextBlock>
+    <TextBlock Classes="h2">The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element.</TextBlock>
+
+    <StackPanel Orientation="Vertical" Gap="8" Width="200" Margin="0,20,0,0">
+      <CheckBox Name="allowSpinCheck" IsChecked="True">AllowSpin</CheckBox>
+      <CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox>
+      <ButtonSpinner Spin="OnSpin" Height="30"
+                     AllowSpin="{Binding #allowSpinCheck.IsChecked}"
+                     ShowButtonSpinner="{Binding #showSpinCheck.IsChecked}">
+        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Everest"/>
+      </ButtonSpinner>
+      <ButtonSpinner Spin="OnSpin" Height="30" ButtonSpinnerLocation="Left"
+                     AllowSpin="{Binding #allowSpinCheck.IsChecked}"
+                     ShowButtonSpinner="{Binding #showSpinCheck.IsChecked}">
+        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Everest"/>
+      </ButtonSpinner>
+    </StackPanel>
+  </StackPanel>
+
+</UserControl>

+ 54 - 0
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs

@@ -0,0 +1,54 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+    public class ButtonSpinnerPage : UserControl
+    {
+        public ButtonSpinnerPage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void OnSpin(object sender, SpinEventArgs e)
+        {
+            var spinner = (ButtonSpinner)sender;
+            var txtBox = (TextBlock)spinner.Content;
+
+            int value = Array.IndexOf(_mountains, txtBox.Text);
+            if (e.Direction == SpinDirection.Increase)
+                value++;
+            else
+                value--;
+
+            if (value < 0)
+                value = _mountains.Length - 1;
+            else if (value >= _mountains.Length)
+                value = 0;
+
+            txtBox.Text = _mountains[value];
+        }
+
+        private readonly string[] _mountains = new[]
+        {
+            "Everest",
+            "K2 (Mount Godwin Austen)",
+            "Kangchenjunga",
+            "Lhotse",
+            "Makalu",
+            "Cho Oyu",
+            "Dhaulagiri",
+            "Manaslu",
+            "Nanga Parbat",
+            "Annapurna"
+        };
+    }
+}

+ 258 - 0
src/Avalonia.Controls/ButtonSpinner.cs

@@ -0,0 +1,258 @@
+using System;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    public enum Location
+    {
+        Left,
+        Right
+    }
+
+    /// <summary>
+    /// Represents a spinner control that includes two Buttons.
+    /// </summary>
+    public class ButtonSpinner : Spinner
+    {
+        /// <summary>
+        /// Defines the <see cref="AllowSpin"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AllowSpinProperty =
+            AvaloniaProperty.Register<ButtonSpinner, bool>(nameof(AllowSpin), true);
+
+        /// <summary>
+        /// Defines the <see cref="ShowButtonSpinner"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
+            AvaloniaProperty.Register<ButtonSpinner, bool>(nameof(ShowButtonSpinner), true);
+
+        /// <summary>
+        /// Defines the <see cref="ButtonSpinnerLocation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
+            AvaloniaProperty.Register<ButtonSpinner, Location>(nameof(ButtonSpinnerLocation), Location.Right);
+
+        private Button _decreaseButton;
+        /// <summary>
+        /// Gets or sets the DecreaseButton template part.
+        /// </summary>
+        private Button DecreaseButton
+        {
+            get { return _decreaseButton; }
+            set
+            {
+                if (_decreaseButton != null)
+                {
+                    _decreaseButton.Click  -= OnButtonClick;
+                }
+                _decreaseButton = value;
+                if (_decreaseButton != null)
+                {
+                    _decreaseButton.Click += OnButtonClick;
+                }
+            }
+        }
+
+        private Button _increaseButton;
+        /// <summary>
+        /// Gets or sets the IncreaseButton template part.
+        /// </summary>
+        private Button IncreaseButton
+        {
+            get
+            {
+                return _increaseButton;
+            }
+            set
+            {
+                if (_increaseButton != null)
+                {
+                    _increaseButton.Click -= OnButtonClick;
+                }
+                _increaseButton = value;
+                if (_increaseButton != null)
+                {
+                    _increaseButton.Click += OnButtonClick;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Initializes static members of the <see cref="ButtonSpinner"/> class.
+        /// </summary>
+        static ButtonSpinner()
+        {
+            AllowSpinProperty.Changed.Subscribe(AllowSpinChanged);
+            PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left");
+            PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right");
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the <see cref="ButtonSpinner"/> should allow to spin.
+        /// </summary>
+        public bool AllowSpin
+        {
+            get { return GetValue(AllowSpinProperty); }
+            set { SetValue(AllowSpinProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the spin buttons should be shown.
+        /// </summary>
+        public bool ShowButtonSpinner
+        {
+            get { return GetValue(ShowButtonSpinnerProperty); }
+            set { SetValue(ShowButtonSpinnerProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets current location of the <see cref="ButtonSpinner"/>.
+        /// </summary>
+        public Location ButtonSpinnerLocation
+        {
+            get { return GetValue(ButtonSpinnerLocationProperty); }
+            set { SetValue(ButtonSpinnerLocationProperty, value); }
+        }
+
+        /// <inheritdoc />
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            IncreaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
+            DecreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
+            SetButtonUsage();
+        }
+
+        /// <inheritdoc />
+        protected override void OnPointerReleased(PointerReleasedEventArgs e)
+        {
+            base.OnPointerReleased(e);
+            Point mousePosition;
+            if (IncreaseButton != null && IncreaseButton.IsEnabled == false)
+            {
+                mousePosition = e.GetPosition(IncreaseButton);
+                if (mousePosition.X > 0 && mousePosition.X < IncreaseButton.Width &&
+                    mousePosition.Y > 0 && mousePosition.Y < IncreaseButton.Height)
+                {
+                    e.Handled = true;
+                }
+            }
+
+            if (DecreaseButton != null && DecreaseButton.IsEnabled == false)
+            {
+                mousePosition = e.GetPosition(DecreaseButton);
+                if (mousePosition.X > 0 && mousePosition.X < DecreaseButton.Width &&
+                    mousePosition.Y > 0 && mousePosition.Y < DecreaseButton.Height)
+                {
+                    e.Handled = true;
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            switch (e.Key)
+            {
+                case Key.Up:
+                {
+                    if (AllowSpin)
+                    {
+                        OnSpin(new SpinEventArgs(SpinEvent, SpinDirection.Increase));
+                        e.Handled = true;
+                    }
+                    break;
+                }
+                case Key.Down:
+                {
+                    if (AllowSpin)
+                    {
+                        OnSpin(new SpinEventArgs(SpinEvent, SpinDirection.Decrease));
+                        e.Handled = true;
+                    }
+                    break;
+                }
+                case Key.Enter:
+                {
+                    //Do not Spin on enter Key when spinners have focus
+                    if (((IncreaseButton != null) && (IncreaseButton.IsFocused))
+                        || ((DecreaseButton != null) && DecreaseButton.IsFocused))
+                    {
+                        e.Handled = true;
+                    }
+                    break;
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+        {
+            base.OnPointerWheelChanged(e);
+            if (!e.Handled && AllowSpin)
+            {
+                if (e.Delta.Y != 0)
+                {
+                    var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true);
+                    OnSpin(spinnerEventArgs);
+                    e.Handled = spinnerEventArgs.Handled;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="AllowSpin"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnAllowSpinChanged(bool oldValue, bool newValue)
+        {
+            SetButtonUsage();
+        }
+
+        /// <summary>
+        /// Called when the <see cref="AllowSpin"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void AllowSpinChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is ButtonSpinner spinner)
+            {
+                var oldValue = (bool)e.OldValue;
+                var newValue = (bool)e.NewValue;
+                spinner.OnAllowSpinChanged(oldValue, newValue);
+            }
+        }
+        
+        /// <summary>
+        /// Disables or enables the buttons based on the valid spin direction.
+        /// </summary>
+        private void SetButtonUsage()
+        {
+            if (IncreaseButton != null)
+            {
+                IncreaseButton.IsEnabled = AllowSpin && ((ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase);
+            }
+
+            if (DecreaseButton != null)
+            {
+                DecreaseButton.IsEnabled = AllowSpin && ((ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease);
+            }
+        }
+
+        /// <summary>
+        /// Called when user clicks one of the spin buttons.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void OnButtonClick(object sender, RoutedEventArgs e)
+        {
+            if (AllowSpin)
+            {
+                var direction = sender == IncreaseButton ? SpinDirection.Increase : SpinDirection.Decrease;
+                OnSpin(new SpinEventArgs(SpinEvent, direction));
+            }
+        }
+    }
+}

+ 2 - 2
src/Avalonia.Controls/Remote/RemoteWidget.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Controls.Remote
     {
         private readonly IAvaloniaRemoteTransportConnection _connection;
         private FrameMessage _lastFrame;
-        private WritableBitmap _bitmap;
+        private WriteableBitmap _bitmap;
         public RemoteWidget(IAvaloniaRemoteTransportConnection connection)
         {
             _connection = connection;
@@ -62,7 +62,7 @@ namespace Avalonia.Controls.Remote
                 var fmt = (PixelFormat) _lastFrame.Format;
                 if (_bitmap == null || _bitmap.PixelWidth != _lastFrame.Width ||
                     _bitmap.PixelHeight != _lastFrame.Height)
-                    _bitmap = new WritableBitmap(_lastFrame.Width, _lastFrame.Height, fmt);
+                    _bitmap = new WriteableBitmap(_lastFrame.Width, _lastFrame.Height, fmt);
                 using (var l = _bitmap.Lock())
                 {
                     var lineLen = (fmt == PixelFormat.Rgb565 ? 2 : 4) * _lastFrame.Width;

+ 27 - 2
src/Avalonia.Controls/RepeatButton.cs

@@ -6,17 +6,32 @@ namespace Avalonia.Controls
 {
     public class RepeatButton : Button
     {
+        /// <summary>
+        /// Defines the <see cref="Interval"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> IntervalProperty =
+            AvaloniaProperty.Register<Button, int>(nameof(Interval), 100);
+
         /// <summary>
         /// Defines the <see cref="Delay"/> property.
         /// </summary>
         public static readonly StyledProperty<int> DelayProperty =
-            AvaloniaProperty.Register<Button, int>(nameof(Delay), 100);
+            AvaloniaProperty.Register<Button, int>(nameof(Delay), 300);
 
         private DispatcherTimer _repeatTimer;
 
         /// <summary>
         /// Gets or sets the amount of time, in milliseconds, of repeating clicks.
         /// </summary>
+        public int Interval
+        {
+            get { return GetValue(IntervalProperty); }
+            set { SetValue(IntervalProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the amount of time, in milliseconds, to wait before repeating begins.
+        /// </summary>
         public int Delay
         {
             get { return GetValue(DelayProperty); }
@@ -28,7 +43,7 @@ namespace Avalonia.Controls
             if (_repeatTimer == null)
             {
                 _repeatTimer = new DispatcherTimer();
-                _repeatTimer.Tick += (o, e) => OnClick();
+                _repeatTimer.Tick += RepeatTimerOnTick;
             }
 
             if (_repeatTimer.IsEnabled) return;
@@ -37,6 +52,16 @@ namespace Avalonia.Controls
             _repeatTimer.Start();
         }
 
+        private void RepeatTimerOnTick(object sender, EventArgs e)
+        {
+            var interval = TimeSpan.FromMilliseconds(Interval);
+            if (_repeatTimer.Interval != interval)
+            {
+                _repeatTimer.Interval = interval;
+            }
+            OnClick();
+        }
+
         private void StopTimer()
         {
             _repeatTimer?.Stop();

+ 174 - 0
src/Avalonia.Controls/Spinner.cs

@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents spin directions that are valid.
+    /// </summary>
+    [Flags]
+    public enum ValidSpinDirections
+    {
+        /// <summary>
+        /// Can not increase nor decrease.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Can increase.
+        /// </summary>
+        Increase = 1,
+
+        /// <summary>
+        /// Can decrease.
+        /// </summary>
+        Decrease = 2
+    }
+
+    /// <summary>
+    /// Represents spin directions that could be initiated by the end-user.
+    /// </summary>
+    public enum SpinDirection
+    {
+        /// <summary>
+        /// Represents a spin initiated by the end-user in order to Increase a value.
+        /// </summary>
+        Increase = 0,
+
+        /// <summary>
+        /// Represents a spin initiated by the end-user in order to Decrease a value.
+        /// </summary>
+        Decrease = 1
+    }
+
+    /// <summary>
+    /// Provides data for the Spinner.Spin event.
+    /// </summary>
+    public class SpinEventArgs : RoutedEventArgs
+    {
+        /// <summary>
+        /// Gets the SpinDirection for the spin that has been initiated by the end-user.
+        /// </summary>
+        public SpinDirection Direction { get; }
+
+        /// <summary>
+        /// Get or set whheter the spin event originated from a mouse wheel event.
+        /// </summary>
+        public bool UsingMouseWheel{ get; }
+
+        /// <summary>
+        /// Initializes a new instance of the SpinEventArgs class.
+        /// </summary>
+        /// <param name="direction">Spin direction.</param>
+        public SpinEventArgs(SpinDirection direction)
+        {
+            Direction = direction;
+        }
+
+        public SpinEventArgs(RoutedEvent routedEvent, SpinDirection direction)
+            : base(routedEvent)
+        {
+            Direction = direction;
+        }
+
+        public SpinEventArgs(SpinDirection direction, bool usingMouseWheel)
+        {
+            Direction = direction;
+            UsingMouseWheel = usingMouseWheel;
+        }
+
+        public SpinEventArgs(RoutedEvent routedEvent, SpinDirection direction, bool usingMouseWheel)
+            : base(routedEvent)
+        {
+            Direction = direction;
+            UsingMouseWheel = usingMouseWheel;
+        }
+    }
+
+    /// <summary>
+    /// Base class for controls that represents controls that can spin.
+    /// </summary>
+    public abstract class Spinner : ContentControl
+    {
+        /// <summary>
+        /// Defines the <see cref="ValidSpinDirection"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ValidSpinDirections> ValidSpinDirectionProperty =
+            AvaloniaProperty.Register<Spinner, ValidSpinDirections>(nameof(ValidSpinDirection),
+                ValidSpinDirections.Increase | ValidSpinDirections.Decrease);
+
+        /// <summary>
+        /// Defines the <see cref="Spin"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<SpinEventArgs> SpinEvent =
+            RoutedEvent.Register<Spinner, SpinEventArgs>(nameof(Spin), RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Initializes static members of the <see cref="Spinner"/> class.
+        /// </summary>
+        static Spinner()
+        {
+            ValidSpinDirectionProperty.Changed.Subscribe(OnValidSpinDirectionPropertyChanged);
+        }
+
+        /// <summary>
+        /// Occurs when spinning is initiated by the end-user.
+        /// </summary>
+        public event EventHandler<SpinEventArgs> Spin
+        {
+            add { AddHandler(SpinEvent, value); }
+            remove { RemoveHandler(SpinEvent, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets <see cref="ValidSpinDirections"/> allowed for this control.
+        /// </summary>
+        public ValidSpinDirections ValidSpinDirection
+        {
+            get { return GetValue(ValidSpinDirectionProperty); }
+            set { SetValue(ValidSpinDirectionProperty, value); }
+        }
+
+        /// <summary>
+        /// Called when valid spin direction changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
+        {
+        }
+
+        /// <summary>
+        /// Raises the OnSpin event when spinning is initiated by the end-user.
+        /// </summary>
+        /// <param name="e">Spin event args.</param>
+        protected virtual void OnSpin(SpinEventArgs e)
+        {
+            var valid = e.Direction == SpinDirection.Increase
+                ? ValidSpinDirections.Increase
+                : ValidSpinDirections.Decrease;
+
+            //Only raise the event if spin is allowed.
+            if ((ValidSpinDirection & valid) == valid)
+            {
+                RaiseEvent(e);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="ValidSpinDirection"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnValidSpinDirectionPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is Spinner spinner)
+            {
+                var oldValue = (ValidSpinDirections)e.OldValue;
+                var newValue = (ValidSpinDirections)e.NewValue;
+                spinner.OnValidSpinDirectionChanged(oldValue, newValue);
+            }
+        }
+    }
+}

+ 86 - 0
src/Avalonia.Themes.Default/ButtonSpinner.xaml

@@ -0,0 +1,86 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="ButtonSpinner">
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLightBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
+    <Setter Property="VerticalContentAlignment" Value="Center"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton">
+    <Setter Property="RepeatButton.Background" Value="Transparent"/>
+    <Setter Property="RepeatButton.BorderBrush" Value="Transparent"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton:pointerover">
+    <Setter Property="RepeatButton.Background" Value="{DynamicResource ThemeControlMidBrush}"/>
+    <Setter Property="RepeatButton.BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton#PART_IncreaseButton">
+    <Setter Property="Content">
+      <Template>
+        <Path Fill="{DynamicResource ThemeForegroundBrush}"
+              Width="8"
+              Height="4"
+              Stretch="Uniform"
+              HorizontalAlignment="Center"
+              VerticalAlignment="Center"
+              Data="M0,5 L4.5,.5 9,5 6,5 4.5,3.5 3,5 z"/>
+      </Template>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton#PART_DecreaseButton">
+    <Setter Property="Content">
+      <Template>
+        <Path Fill="{DynamicResource ThemeForegroundBrush}"
+              Width="8"
+              Height="4"
+              Stretch="Uniform"
+              HorizontalAlignment="Center"
+              VerticalAlignment="Center"
+              Data="M0,0 L3,0 4.5,1.5 6,0 9,0 4.5,4.5 z"/>
+      </Template>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner:right">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Margin="{TemplateBinding Padding}"
+                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
+          <Grid ColumnDefinitions="*,Auto">
+            <ContentPresenter Name="PART_ContentPresenter" Grid.Column="0"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Content="{TemplateBinding Content}"/>
+            <Grid Grid.Column="1" RowDefinitions="*,*" IsVisible="{TemplateBinding ShowButtonSpinner}">
+              <RepeatButton Grid.Row="0" Name="PART_IncreaseButton"/>
+              <RepeatButton Grid.Row="1" Name="PART_DecreaseButton"/>
+            </Grid>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner:left">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Margin="{TemplateBinding Padding}"
+                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
+          <Grid ColumnDefinitions="Auto,*">
+            <Grid Grid.Column="0" RowDefinitions="*,*" IsVisible="{TemplateBinding ShowButtonSpinner}">
+              <RepeatButton Grid.Row="0" Name="PART_IncreaseButton"/>
+              <RepeatButton Grid.Row="1" Name="PART_DecreaseButton"/>
+            </Grid>
+            <ContentPresenter Name="PART_ContentPresenter" Grid.Column="1"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Content="{TemplateBinding Content}"/>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 1 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -42,4 +42,5 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.CalendarItem.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.Calendar.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.ButtonSpinner.xaml?assembly=Avalonia.Themes.Default"/>
 </Styles>

+ 5 - 5
src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs → src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs

@@ -9,15 +9,15 @@ using Avalonia.Utilities;
 namespace Avalonia.Media.Imaging
 {
     /// <summary>
-    /// Holds a writable bitmap image.
+    /// Holds a writeable bitmap image.
     /// </summary>
-    public class WritableBitmap : Bitmap
+    public class WriteableBitmap : Bitmap
     {
-        public WritableBitmap(int width, int height, PixelFormat? format = null) 
-            : base(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().CreateWritableBitmap(width, height, format))
+        public WriteableBitmap(int width, int height, PixelFormat? format = null) 
+            : base(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().CreateWriteableBitmap(width, height, format))
         {
         }
         
-        public ILockedFramebuffer Lock() => ((IWritableBitmapImpl) PlatformImpl.Item).Lock();
+        public ILockedFramebuffer Lock() => ((IWriteableBitmapImpl) PlatformImpl.Item).Lock();
     }
 }

+ 3 - 3
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -61,13 +61,13 @@ namespace Avalonia.Platform
             double dpiY);
 
         /// <summary>
-        /// Creates a writable bitmap implementation.
+        /// Creates a writeable bitmap implementation.
         /// </summary>
         /// <param name="width">The width of the bitmap.</param>
         /// <param name="height">The height of the bitmap.</param>
         /// <param name="format">Pixel format (optional).</param>
-        /// <returns>An <see cref="IWritableBitmapImpl"/>.</returns>
-        IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null);
+        /// <returns>An <see cref="IWriteableBitmapImpl"/>.</returns>
+        IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null);
 
         /// <summary>
         /// Loads a bitmap implementation from a file..

+ 2 - 2
src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs → src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs

@@ -7,9 +7,9 @@ using System.Threading.Tasks;
 namespace Avalonia.Platform
 {
     /// <summary>
-    /// Defines the platform-specific interface for a <see cref="Avalonia.Media.Imaging.WritableBitmap"/>.
+    /// Defines the platform-specific interface for a <see cref="Avalonia.Media.Imaging.WriteableBitmap"/>.
     /// </summary>
-    public interface IWritableBitmapImpl : IBitmapImpl
+    public interface IWriteableBitmapImpl : IBitmapImpl
     {
         ILockedFramebuffer Lock();
     }

+ 12 - 0
src/OSX/Avalonia.MonoMac/TopLevelImpl.cs

@@ -39,6 +39,7 @@ namespace Avalonia.MonoMac
             private NSTrackingArea _area;
             private NSCursor _cursor;
             private bool _nonUiRedrawQueued;
+            private bool _isMouseOver;
 
             public CGSize PixelSize { get; set; }
 
@@ -133,7 +134,11 @@ namespace Avalonia.MonoMac
             {
                 ResetCursorRects();
                 if (_cursor != null)
+                {
                     AddCursorRect(Frame, _cursor);
+                    if (_isMouseOver)
+                        _cursor.Set();
+                }
             }
 
             static readonly NSCursor ArrowCursor = NSCursor.ArrowCursor;
@@ -299,10 +304,17 @@ namespace Avalonia.MonoMac
 
             public override void MouseExited(NSEvent theEvent)
             {
+                _isMouseOver = false;
                 MouseEvent(theEvent, RawMouseEventType.LeaveWindow);
                 base.MouseExited(theEvent);
             }
 
+            public override void MouseEntered(NSEvent theEvent)
+            {
+                _isMouseOver = true;
+                base.MouseEntered(theEvent);
+            }
+
             void KeyboardEvent(RawKeyEventType type, NSEvent ev)
             {
                 var code = KeyTransform.TransformKeyCode(ev.KeyCode);

+ 1 - 1
src/Skia/Avalonia.Skia/BitmapImpl.cs

@@ -6,7 +6,7 @@ using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    class BitmapImpl : IRenderTargetBitmapImpl, IWritableBitmapImpl
+    class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl
     {
         private Vector _dpi;
 

+ 1 - 1
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -88,7 +88,7 @@ namespace Avalonia.Skia
             return new FramebufferRenderTarget(fb);
         }
 
-        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null)
+        public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null)
         {
             return new BitmapImpl(width, height, new Vector(96, 96), format);
         }

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -168,9 +168,9 @@ namespace Avalonia.Direct2D1
                 dpiY);
         }
 
-        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null)
+        public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null)
         {
-            return new WritableWicBitmapImpl(s_imagingFactory, width, height, format);
+            return new WriteableWicBitmapImpl(s_imagingFactory, width, height, format);
         }
 
         public IStreamGeometryImpl CreateStreamGeometry()

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs → src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs

@@ -9,9 +9,9 @@ using PixelFormat = Avalonia.Platform.PixelFormat;
 
 namespace Avalonia.Direct2D1.Media.Imaging
 {
-    class WritableWicBitmapImpl : WicBitmapImpl, IWritableBitmapImpl
+    class WriteableWicBitmapImpl : WicBitmapImpl, IWriteableBitmapImpl
     {
-        public WritableWicBitmapImpl(ImagingFactory factory, int width, int height, PixelFormat? pixelFormat) 
+        public WriteableWicBitmapImpl(ImagingFactory factory, int width, int height, PixelFormat? pixelFormat) 
             : base(factory, width, height, pixelFormat)
         {
         }

+ 5 - 5
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@@ -106,9 +106,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media
 
         [Theory]
         [InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgba8888)]
-        public void WritableBitmapShouldBeUsable(PixelFormat fmt)
+        public void WriteableBitmapShouldBeUsable(PixelFormat fmt)
         {
-            var writableBitmap = new WritableBitmap(256, 256, fmt);
+            var writeableBitmap = new WriteableBitmap(256, 256, fmt);
 
             var data = new int[256 * 256];
             for (int y = 0; y < 256; y++)
@@ -116,7 +116,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                     data[y * 256 + x] =(int)((uint)(x + (y << 8)) | 0xFF000000u);
 
 
-            using (var l = writableBitmap.Lock())
+            using (var l = writeableBitmap.Lock())
             {
                 for(var r = 0; r<256; r++)
                 {
@@ -125,9 +125,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             }
 
 
-            var name = nameof(WritableBitmapShouldBeUsable) + "_" + fmt;
+            var name = nameof(WriteableBitmapShouldBeUsable) + "_" + fmt;
 
-            writableBitmap.Save(System.IO.Path.Combine(OutputPath, name + ".out.png"));
+            writeableBitmap.Save(System.IO.Path.Combine(OutputPath, name + ".out.png"));
             CompareImagesNoRenderer(name);
 
         }

+ 1 - 1
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -39,7 +39,7 @@ namespace Avalonia.UnitTests
             return new MockStreamGeometryImpl();
         }
 
-        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = default(PixelFormat?))
+        public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = default(PixelFormat?))
         {
             throw new NotImplementedException();
         }

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
-        public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt)
+        public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? fmt)
         {
             throw new NotImplementedException();
         }

+ 0 - 0
tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png → tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png


+ 0 - 0
tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png → tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png


+ 0 - 0
tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png → tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png


+ 0 - 0
tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png → tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png