Browse Source

Merge pull request #1 from AvaloniaUI/master

Merge
pull/1524/head
Benedikt Schroeder 8 years ago
committed by GitHub
parent
commit
b09174a67f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      readme.md
  2. 12
      samples/ControlCatalog/ControlCatalog.csproj
  3. 6
      samples/ControlCatalog/MainView.xaml
  4. 59
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  5. 143
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
  6. 80
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  7. 94
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  8. 4
      samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
  9. 4
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  10. 2726
      src/Avalonia.Controls/AutoCompleteBox.cs
  11. 51
      src/Avalonia.Controls/Border.cs
  12. 2
      src/Avalonia.Controls/Button.cs
  13. 5
      src/Avalonia.Controls/ButtonSpinner.cs
  14. 1
      src/Avalonia.Controls/DropDown.cs
  15. 35
      src/Avalonia.Controls/MenuItem.cs
  16. 998
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  17. 16
      src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs
  18. 153
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  19. 4
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  20. 29
      src/Avalonia.Controls/TextBox.cs
  21. 279
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  22. 64
      src/Avalonia.Controls/Utils/ISelectionAdapter.cs
  23. 342
      src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
  24. 5
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  25. 2
      src/Avalonia.Input/KeyboardDevice.cs
  26. 2
      src/Avalonia.Themes.Default/Accents/BaseLight.xaml
  27. 43
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  28. 4
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  29. 4
      src/Avalonia.Themes.Default/MenuItem.xaml
  30. 41
      src/Avalonia.Themes.Default/NumericUpDown.xaml
  31. 97
      src/Avalonia.Visuals/CornerRadius.cs
  32. 2
      src/Avalonia.Visuals/Media/GradientBrush.cs
  33. 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  34. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  35. 7
      src/Avalonia.Visuals/Thickness.cs
  36. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  37. 19
      src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs
  38. 1
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  39. 1
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
  40. 7
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  41. 2
      tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
  42. 1042
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  43. 24
      tests/Avalonia.Controls.UnitTests/BorderTests.cs
  44. 50
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs
  45. 66
      tests/Avalonia.RenderTests/Controls/BorderTests.cs
  46. 2
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  47. 2
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  48. 6
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  49. 43
      tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs
  50. 48
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  51. 4
      tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs
  52. BIN
      tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png
  53. BIN
      tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png
  54. BIN
      tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png
  55. BIN
      tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png

2
readme.md

@ -35,7 +35,7 @@ https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts
## Documentation ## Documentation
As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/guides/quickstart) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia).
There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/.

12
samples/ControlCatalog/ControlCatalog.csproj

@ -41,6 +41,9 @@
<EmbeddedResource Include="Pages\DialogsPage.xaml"> <EmbeddedResource Include="Pages\DialogsPage.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="Pages\AutoCompleteBoxPage.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Pages\BorderPage.xaml"> <EmbeddedResource Include="Pages\BorderPage.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</EmbeddedResource> </EmbeddedResource>
@ -78,6 +81,9 @@
<EmbeddedResource Include="Pages\MenuPage.xaml"> <EmbeddedResource Include="Pages\MenuPage.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="Pages\NumericUpDownPage.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Pages\ProgressBarPage.xaml"> <EmbeddedResource Include="Pages\ProgressBarPage.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
</EmbeddedResource> </EmbeddedResource>
@ -113,6 +119,9 @@
<Compile Include="Pages\BorderPage.xaml.cs"> <Compile Include="Pages\BorderPage.xaml.cs">
<DependentUpon>BorderPage.xaml</DependentUpon> <DependentUpon>BorderPage.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Pages\AutoCompleteBoxPage.xaml.cs">
<DependentUpon>AutoCompleteBoxPage.xaml</DependentUpon>
</Compile>
<Compile Include="Pages\ButtonPage.xaml.cs"> <Compile Include="Pages\ButtonPage.xaml.cs">
<DependentUpon>ButtonPage.xaml</DependentUpon> <DependentUpon>ButtonPage.xaml</DependentUpon>
</Compile> </Compile>
@ -169,6 +178,9 @@
</Compile> </Compile>
<Compile Include="Pages\ButtonSpinnerPage.xaml.cs"> <Compile Include="Pages\ButtonSpinnerPage.xaml.cs">
<DependentUpon>ButtonSpinnerPage.xaml</DependentUpon> <DependentUpon>ButtonSpinnerPage.xaml</DependentUpon>
</Compile>
<Compile Include="Pages\NumericUpDownPage.xaml.cs">
<DependentUpon>NumericUpDownPage.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Pages\ScreenPage.cs" /> <Compile Include="Pages\ScreenPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />

6
samples/ControlCatalog/MainView.xaml

@ -5,10 +5,11 @@
<TabControl.Transition> <TabControl.Transition>
<CrossFade Duration="0.25"/> <CrossFade Duration="0.25"/>
</TabControl.Transition> </TabControl.Transition>
<TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
<TabItem Header="Border"><pages:BorderPage/></TabItem> <TabItem Header="Border"><pages:BorderPage/></TabItem>
<TabItem Header="Button"><pages:ButtonPage/></TabItem> <TabItem Header="Button"><pages:ButtonPage/></TabItem>
<TabItem Header="ButtonSpinner"><pages:ButtonSpinnerPage/></TabItem> <TabItem Header="ButtonSpinner"><pages:ButtonSpinnerPage/></TabItem>
<TabItem Header="Calendar"><pages:CalendarPage/></TabItem> <TabItem Header="Calendar"><pages:CalendarPage/></TabItem>
<TabItem Header="Canvas"><pages:CanvasPage/></TabItem> <TabItem Header="Canvas"><pages:CanvasPage/></TabItem>
<TabItem Header="Carousel"><pages:CarouselPage/></TabItem> <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
<TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem> <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
@ -19,6 +20,7 @@
<TabItem Header="Image"><pages:ImagePage/></TabItem> <TabItem Header="Image"><pages:ImagePage/></TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem> <TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem> <TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem> <TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem> <TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem> <TabItem Header="Slider"><pages:SliderPage/></TabItem>
@ -26,4 +28,4 @@
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem> <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem> <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
</TabControl> </TabControl>
</UserControl> </UserControl>

59
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@ -0,0 +1,59 @@
<UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4">
<TextBlock Classes="h1">AutoCompleteBox</TextBlock>
<TextBlock Classes="h2">A control into which the user can input text</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="8">
<StackPanel Orientation="Vertical">
<TextBlock Text="MinimumPrefixLength: 1"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
MinimumPrefixLength="1"/>
<TextBlock Text="MinimumPrefixLength: 3"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
MinimumPrefixLength="3"/>
<TextBlock Text="MinimumPopulateDelay: 1 Second"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
MinimumPopulateDelay="1"/>
<TextBlock Text="MaxDropDownHeight: 60"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
MaxDropDownHeight="60"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
Watermark="Watermark"/>
<TextBlock Text="Disabled"/>
<AutoCompleteBox Width="200"
IsEnabled="False"/>
</StackPanel>
<StackPanel Orientation="Vertical">
<TextBlock Text="ValueMemeberSelector"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
ValueMemberSelector="Capital"/>
<TextBlock Text="ValueMemberBinding"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
ValueMemberBinding="{Binding Capital}"/>
<TextBlock Text="Multi-Binding"/>
<AutoCompleteBox Name="MultiBindingBox"
Width="200"
Margin="0,0,0,8"
FilterMode="Contains"/>
<TextBlock Text="Async Populate"/>
<AutoCompleteBox Name="AsyncBox"
Width="200"
Margin="0,0,0,8"
FilterMode="None"/>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

143
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs

@ -0,0 +1,143 @@
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Markup;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace ControlCatalog.Pages
{
public class AutoCompleteBoxPage : UserControl
{
public class StateData
{
public string Name { get; private set; }
public string Abbreviation { get; private set; }
public string Capital { get; private set; }
public StateData(string name, string abbreviatoin, string capital)
{
Name = name;
Abbreviation = abbreviatoin;
Capital = capital;
}
public override string ToString()
{
return Name;
}
}
private StateData[] BuildAllStates()
{
return new StateData[]
{
new StateData("Alabama","AL","Montgomery"),
new StateData("Alaska","AK","Juneau"),
new StateData("Arizona","AZ","Phoenix"),
new StateData("Arkansas","AR","Little Rock"),
new StateData("California","CA","Sacramento"),
new StateData("Colorado","CO","Denver"),
new StateData("Connecticut","CT","Hartford"),
new StateData("Delaware","DE","Dover"),
new StateData("Florida","FL","Tallahassee"),
new StateData("Georgia","GA","Atlanta"),
new StateData("Hawaii","HI","Honolulu"),
new StateData("Idaho","ID","Boise"),
new StateData("Illinois","IL","Springfield"),
new StateData("Indiana","IN","Indianapolis"),
new StateData("Iowa","IA","Des Moines"),
new StateData("Kansas","KS","Topeka"),
new StateData("Kentucky","KY","Frankfort"),
new StateData("Louisiana","LA","Baton Rouge"),
new StateData("Maine","ME","Augusta"),
new StateData("Maryland","MD","Annapolis"),
new StateData("Massachusetts","MA","Boston"),
new StateData("Michigan","MI","Lansing"),
new StateData("Minnesota","MN","St. Paul"),
new StateData("Mississippi","MS","Jackson"),
new StateData("Missouri","MO","Jefferson City"),
new StateData("Montana","MT","Helena"),
new StateData("Nebraska","NE","Lincoln"),
new StateData("Nevada","NV","Carson City"),
new StateData("New Hampshire","NH","Concord"),
new StateData("New Jersey","NJ","Trenton"),
new StateData("New Mexico","NM","Santa Fe"),
new StateData("New York","NY","Albany"),
new StateData("North Carolina","NC","Raleigh"),
new StateData("North Dakota","ND","Bismarck"),
new StateData("Ohio","OH","Columbus"),
new StateData("Oklahoma","OK","Oklahoma City"),
new StateData("Oregon","OR","Salem"),
new StateData("Pennsylvania","PA","Harrisburg"),
new StateData("Rhode Island","RI","Providence"),
new StateData("South Carolina","SC","Columbia"),
new StateData("South Dakota","SD","Pierre"),
new StateData("Tennessee","TN","Nashville"),
new StateData("Texas","TX","Austin"),
new StateData("Utah","UT","Salt Lake City"),
new StateData("Vermont","VT","Montpelier"),
new StateData("Virginia","VA","Richmond"),
new StateData("Washington","WA","Olympia"),
new StateData("West Virginia","WV","Charleston"),
new StateData("Wisconsin","WI","Madison"),
new StateData("Wyoming","WY","Cheyenne"),
};
}
public StateData[] States { get; private set; }
public AutoCompleteBoxPage()
{
this.InitializeComponent();
States = BuildAllStates();
foreach (AutoCompleteBox box in GetAllAutoCompleteBox())
{
box.Items = States;
}
var converter = new FuncMultiValueConverter<string, string>(parts =>
{
return String.Format("{0} ({1})", parts.ToArray());
});
var binding = new MultiBinding { Converter = converter };
binding.Bindings.Add(new Binding("Name"));
binding.Bindings.Add(new Binding("Abbreviation"));
var multibindingBox = this.FindControl<AutoCompleteBox>("MultiBindingBox");
multibindingBox.ValueMemberBinding = binding;
var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox");
asyncBox.AsyncPopulator = PopulateAsync;
}
private IEnumerable<AutoCompleteBox> GetAllAutoCompleteBox()
{
return
this.GetLogicalDescendants()
.OfType<AutoCompleteBox>();
}
private bool StringContains(string str, string query)
{
return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
}
private async Task<IEnumerable<object>> PopulateAsync(string searchText, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken);
return
States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText))
.ToList();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

80
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -0,0 +1,80 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock>
<TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock>
<TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock>
<Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto">
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="35,35,35,35,35">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock>
<CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #upDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">IsReadOnly:</TextBlock>
<CheckBox Grid.Row="1" Grid.Column="1" IsChecked="{Binding #upDown.IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">AllowSpin:</TextBlock>
<CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding #upDown.AllowSpin}" IsEnabled="{Binding #upDown.!IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">ClipValueToMinMax:</TextBlock>
<CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #upDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/>
</Grid>
<Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="35,35,35,35,35">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
<DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
VerticalAlignment="Center" Margin="2">
<DropDown.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Gap="2">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="-"/>
<TextBlock Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</DropDown.ItemTemplate>
</DropDown>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
<DropDown Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
<DropDown Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #upDown.CultureInfo}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #upDown.Watermark}" VerticalAlignment="Center" Margin="2" />
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" />
</Grid>
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="35,35,35,35,35" ColumnDefinitions="Auto, 120">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock>
<NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock>
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock>
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center"
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center"
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
</Grid>
</Grid>
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
</StackPanel>
</UserControl>

94
samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
using ReactiveUI;
namespace ControlCatalog.Pages
{
public class NumericUpDownPage : UserControl
{
public NumericUpDownPage()
{
this.InitializeComponent();
var viewModel = new NumbersPageViewModel();
DataContext = viewModel;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
public class NumbersPageViewModel : ReactiveObject
{
private IList<FormatObject> _formats;
private FormatObject _selectedFormat;
private IList<Location> _spinnerLocations;
public NumbersPageViewModel()
{
SelectedFormat = Formats.FirstOrDefault();
}
public IList<FormatObject> Formats
{
get
{
return _formats ?? (_formats = new List<FormatObject>()
{
new FormatObject() {Name = "Currency", Value = "C2"},
new FormatObject() {Name = "Fixed point", Value = "F2"},
new FormatObject() {Name = "General", Value = "G"},
new FormatObject() {Name = "Number", Value = "N"},
new FormatObject() {Name = "Percent", Value = "P"},
new FormatObject() {Name = "Degrees", Value = "{0:N2} °"},
});
}
}
public IList<Location> SpinnerLocations
{
get
{
if (_spinnerLocations == null)
{
_spinnerLocations = new List<Location>();
foreach (Location value in Enum.GetValues(typeof(Location)))
{
_spinnerLocations.Add(value);
}
}
return _spinnerLocations ;
}
}
public IList<CultureInfo> Cultures { get; } = new List<CultureInfo>()
{
new CultureInfo("en-US"),
new CultureInfo("en-GB"),
new CultureInfo("fr-FR"),
new CultureInfo("ar-DZ"),
new CultureInfo("zh-CN"),
new CultureInfo("cs-CZ")
};
public FormatObject SelectedFormat
{
get { return _selectedFormat; }
set { this.RaiseAndSetIfChanged(ref _selectedFormat, value); }
}
}
public class FormatObject
{
public string Value { get; set; }
public string Name { get; set; }
}
}

4
samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj

@ -17,7 +17,9 @@
<None Remove="MiniCube.fx" /> <None Remove="MiniCube.fx" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="MiniCube.fx" /> <EmbeddedResource Include="MiniCube.fx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" /> <ProjectReference Include="..\..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />

4
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@ -117,7 +117,7 @@ namespace Avalonia.Collections
_inner = new Dictionary<TKey, TValue>(); _inner = new Dictionary<TKey, TValue>();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[]")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
if (CollectionChanged != null) if (CollectionChanged != null)
@ -222,4 +222,4 @@ namespace Avalonia.Collections
} }
} }
} }
} }

2726
src/Avalonia.Controls/AutoCompleteBox.cs

File diff suppressed because it is too large

51
src/Avalonia.Controls/Border.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia;
using Avalonia.Controls.Utils;
using Avalonia.Media; using Avalonia.Media;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -8,7 +10,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control which decorates a child with a border and background. /// A control which decorates a child with a border and background.
/// </summary> /// </summary>
public class Border : Decorator public partial class Border : Decorator
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
@ -25,21 +27,24 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="BorderThickness"/> property. /// Defines the <see cref="BorderThickness"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<double> BorderThicknessProperty = public static readonly StyledProperty<Thickness> BorderThicknessProperty =
AvaloniaProperty.Register<Border, double>(nameof(BorderThickness)); AvaloniaProperty.Register<Border, Thickness>(nameof(BorderThickness));
/// <summary> /// <summary>
/// Defines the <see cref="CornerRadius"/> property. /// Defines the <see cref="CornerRadius"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<float> CornerRadiusProperty = public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
AvaloniaProperty.Register<Border, float>(nameof(CornerRadius)); AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
/// <summary> /// <summary>
/// Initializes static members of the <see cref="Border"/> class. /// Initializes static members of the <see cref="Border"/> class.
/// </summary> /// </summary>
static Border() static Border()
{ {
AffectsRender(BackgroundProperty, BorderBrushProperty); AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsMeasure(BorderThicknessProperty);
} }
/// <summary> /// <summary>
@ -63,7 +68,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets the thickness of the border. /// Gets or sets the thickness of the border.
/// </summary> /// </summary>
public double BorderThickness public Thickness BorderThickness
{ {
get { return GetValue(BorderThicknessProperty); } get { return GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); } set { SetValue(BorderThicknessProperty, value); }
@ -72,7 +77,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets the radius of the border rounded corners. /// Gets or sets the radius of the border rounded corners.
/// </summary> /// </summary>
public float CornerRadius public CornerRadius CornerRadius
{ {
get { return GetValue(CornerRadiusProperty); } get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); } set { SetValue(CornerRadiusProperty, value); }
@ -84,21 +89,7 @@ namespace Avalonia.Controls
/// <param name="context">The drawing context.</param> /// <param name="context">The drawing context.</param>
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
var background = Background; _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
var borderBrush = BorderBrush;
var borderThickness = BorderThickness;
var cornerRadius = CornerRadius;
var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
if (background != null)
{
context.FillRectangle(background, rect, cornerRadius);
}
if (borderBrush != null && borderThickness > 0)
{
context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
}
} }
/// <summary> /// <summary>
@ -120,10 +111,12 @@ namespace Avalonia.Controls
{ {
if (Child != null) if (Child != null)
{ {
var padding = Padding + new Thickness(BorderThickness); var padding = Padding + BorderThickness;
Child.Arrange(new Rect(finalSize).Deflate(padding)); Child.Arrange(new Rect(finalSize).Deflate(padding));
} }
_borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);
return finalSize; return finalSize;
} }
@ -131,19 +124,17 @@ namespace Avalonia.Controls
Size availableSize, Size availableSize,
IControl child, IControl child,
Thickness padding, Thickness padding,
double borderThickness) Thickness borderThickness)
{ {
padding += new Thickness(borderThickness); padding += borderThickness;
if (child != null) if (child != null)
{ {
child.Measure(availableSize.Deflate(padding)); child.Measure(availableSize.Deflate(padding));
return child.DesiredSize.Inflate(padding); return child.DesiredSize.Inflate(padding);
} }
else
{ return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
}
} }
} }
} }

2
src/Avalonia.Controls/Button.cs

@ -245,7 +245,7 @@ namespace Avalonia.Controls
{ {
base.OnPointerReleased(e); base.OnPointerReleased(e);
if (e.MouseButton == MouseButton.Left) if (IsPressed && e.MouseButton == MouseButton.Left)
{ {
e.Device.Capture(null); e.Device.Capture(null);
IsPressed = false; IsPressed = false;

5
src/Avalonia.Controls/ButtonSpinner.cs

@ -201,6 +201,11 @@ namespace Avalonia.Controls
} }
} }
protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
{
SetButtonUsage();
}
/// <summary> /// <summary>
/// Called when the <see cref="AllowSpin"/> property value changed. /// Called when the <see cref="AllowSpin"/> property value changed.
/// </summary> /// </summary>

1
src/Avalonia.Controls/DropDown.cs

@ -164,6 +164,7 @@ namespace Avalonia.Controls
else else
{ {
IsDropDownOpen = !IsDropDownOpen; IsDropDownOpen = !IsDropDownOpen;
e.Handled = true;
} }
} }
base.OnPointerPressed(e); base.OnPointerPressed(e);

35
src/Avalonia.Controls/MenuItem.cs

@ -93,6 +93,7 @@ namespace Avalonia.Controls
static MenuItem() static MenuItem()
{ {
SelectableMixin.Attach<MenuItem>(IsSelectedProperty); SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue<MenuItem>(true); FocusableProperty.OverrideDefaultValue<MenuItem>(true);
IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged); IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged);
ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel);
@ -424,6 +425,40 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Called when the <see cref="Command"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is MenuItem menuItem)
{
if (e.OldValue is ICommand oldCommand)
{
oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
}
if (e.NewValue is ICommand newCommand)
{
newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
}
menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
}
}
/// <summary>
/// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void CanExecuteChanged(object sender, EventArgs e)
{
// HACK: Just set the IsEnabled property for the moment. This needs to be changed to
// use IsEnabledCore etc. but it will do for now.
IsEnabled = Command == null || Command.CanExecute(CommandParameter);
}
/// <summary> /// <summary>
/// Called when the <see cref="Icon"/> property changes. /// Called when the <see cref="Icon"/> property changes.
/// </summary> /// </summary>

998
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -0,0 +1,998 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <summary>
/// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
/// </summary>
public class NumericUpDown : TemplatedControl
{
/// <summary>
/// Defines the <see cref="AllowSpin"/> property.
/// </summary>
public static readonly StyledProperty<bool> AllowSpinProperty =
ButtonSpinner.AllowSpinProperty.AddOwner<NumericUpDown>();
/// <summary>
/// Defines the <see cref="ButtonSpinnerLocation"/> property.
/// </summary>
public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner<NumericUpDown>();
/// <summary>
/// Defines the <see cref="ShowButtonSpinner"/> property.
/// </summary>
public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<NumericUpDown>();
/// <summary>
/// Defines the <see cref="ClipValueToMinMax"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, bool> ClipValueToMinMaxProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, bool>(nameof(ClipValueToMinMax),
updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b);
/// <summary>
/// Defines the <see cref="CultureInfo"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, CultureInfo> CultureInfoProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo,
(o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture);
/// <summary>
/// Defines the <see cref="FormatString"/> property.
/// </summary>
public static readonly StyledProperty<string> FormatStringProperty =
AvaloniaProperty.Register<NumericUpDown, string>(nameof(FormatString), string.Empty);
/// <summary>
/// Defines the <see cref="Increment"/> property.
/// </summary>
public static readonly StyledProperty<double> IncrementProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement);
/// <summary>
/// Defines the <see cref="IsReadOnly"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsReadOnlyProperty =
AvaloniaProperty.Register<NumericUpDown, bool>(nameof(IsReadOnly));
/// <summary>
/// Defines the <see cref="Maximum"/> property.
/// </summary>
public static readonly StyledProperty<double> MaximumProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum);
/// <summary>
/// Defines the <see cref="Minimum"/> property.
/// </summary>
public static readonly StyledProperty<double> MinimumProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum);
/// <summary>
/// Defines the <see cref="ParsingNumberStyle"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, NumberStyles> ParsingNumberStyleProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle),
updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style);
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, string> TextProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="Value"/> property.
/// </summary>
public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="Watermark"/> property.
/// </summary>
public static readonly StyledProperty<string> WatermarkProperty =
AvaloniaProperty.Register<NumericUpDown, string>(nameof(Watermark));
private IDisposable _textBoxTextChangedSubscription;
private double _value;
private string _text;
private bool _internalValueSet;
private bool _clipValueToMinMax;
private bool _isSyncingTextAndValueProperties;
private bool _isTextChangedFromUI;
private CultureInfo _cultureInfo;
private NumberStyles _parsingNumberStyle = NumberStyles.Any;
/// <summary>
/// Gets the Spinner template part.
/// </summary>
private Spinner Spinner { get; set; }
/// <summary>
/// Gets the TextBox template part.
/// </summary>
private TextBox TextBox { get; set; }
/// <summary>
/// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
/// </summary>
public bool AllowSpin
{
get { return GetValue(AllowSpinProperty); }
set { SetValue(AllowSpinProperty, value); }
}
/// <summary>
/// Gets or sets current location of the <see cref="ButtonSpinner"/>.
/// </summary>
public Location ButtonSpinnerLocation
{
get { return GetValue(ButtonSpinnerLocationProperty); }
set { SetValue(ButtonSpinnerLocationProperty, 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 if the value should be clipped when minimum/maximum is reached.
/// </summary>
public bool ClipValueToMinMax
{
get { return _clipValueToMinMax; }
set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); }
}
/// <summary>
/// Gets or sets the current CultureInfo.
/// </summary>
public CultureInfo CultureInfo
{
get { return _cultureInfo; }
set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); }
}
/// <summary>
/// Gets or sets the display format of the <see cref="Value"/>.
/// </summary>
public string FormatString
{
get { return GetValue(FormatStringProperty); }
set { SetValue(FormatStringProperty, value); }
}
/// <summary>
/// Gets or sets the amount in which to increment the <see cref="Value"/>.
/// </summary>
public double Increment
{
get { return GetValue(IncrementProperty); }
set { SetValue(IncrementProperty, value); }
}
/// <summary>
/// Gets or sets if the control is read only.
/// </summary>
public bool IsReadOnly
{
get { return GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
/// <summary>
/// Gets or sets the maximum allowed value.
/// </summary>
public double Maximum
{
get { return GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
/// <summary>
/// Gets or sets the minimum allowed value.
/// </summary>
public double Minimum
{
get { return GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
/// <summary>
/// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
/// </summary>
public NumberStyles ParsingNumberStyle
{
get { return _parsingNumberStyle; }
set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); }
}
/// <summary>
/// Gets or sets the formatted string representation of the value.
/// </summary>
public string Text
{
get { return _text; }
set { SetAndRaise(TextProperty, ref _text, value); }
}
/// <summary>
/// Gets or sets the value.
/// </summary>
public double Value
{
get { return _value; }
set
{
value = OnCoerceValue(value);
SetAndRaise(ValueProperty, ref _value, value);
}
}
/// <summary>
/// Gets or sets the object to use as a watermark if the <see cref="Value"/> is null.
/// </summary>
public string Watermark
{
get { return GetValue(WatermarkProperty); }
set { SetValue(WatermarkProperty, value); }
}
/// <summary>
/// Initializes new instance of <see cref="NumericUpDown"/> class.
/// </summary>
public NumericUpDown()
{
Initialized += (sender, e) =>
{
if (!_internalValueSet && IsInitialized)
{
SyncTextAndValueProperties(false, null, true);
}
SetValidSpinDirection();
};
}
/// <summary>
/// Initializes static members of the <see cref="NumericUpDown"/> class.
/// </summary>
static NumericUpDown()
{
CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
FormatStringProperty.Changed.Subscribe(FormatStringChanged);
IncrementProperty.Changed.Subscribe(IncrementChanged);
IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged);
MaximumProperty.Changed.Subscribe(OnMaximumChanged);
MinimumProperty.Changed.Subscribe(OnMinimumChanged);
TextProperty.Changed.Subscribe(OnTextChanged);
ValueProperty.Changed.Subscribe(OnValueChanged);
}
/// <inheritdoc />
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
if (TextBox != null)
{
TextBox.PointerPressed -= TextBoxOnPointerPressed;
_textBoxTextChangedSubscription?.Dispose();
}
TextBox = e.NameScope.Find<TextBox>("PART_TextBox");
if (TextBox != null)
{
TextBox.Text = Text;
TextBox.PointerPressed += TextBoxOnPointerPressed;
_textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged());
}
if (Spinner != null)
{
Spinner.Spin -= OnSpinnerSpin;
}
Spinner = e.NameScope.Find<Spinner>("PART_Spinner");
if (Spinner != null)
{
Spinner.Spin += OnSpinnerSpin;
}
SetValidSpinDirection();
}
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Enter:
var commitSuccess = CommitInput();
e.Handled = !commitSuccess;
break;
}
}
/// <summary>
/// Called when the <see cref="CultureInfo"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue)
{
if (IsInitialized)
{
SyncTextAndValueProperties(false, null);
}
}
/// <summary>
/// Called when the <see cref="FormatString"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnFormatStringChanged(string oldValue, string newValue)
{
if (IsInitialized)
{
SyncTextAndValueProperties(false, null);
}
}
/// <summary>
/// Called when the <see cref="Increment"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnIncrementChanged(double oldValue, double newValue)
{
if (IsInitialized)
{
SetValidSpinDirection();
}
}
/// <summary>
/// Called when the <see cref="IsReadOnly"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue)
{
SetValidSpinDirection();
}
/// <summary>
/// Called when the <see cref="Maximum"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnMaximumChanged(double oldValue, double newValue)
{
if (IsInitialized)
{
SetValidSpinDirection();
}
if (ClipValueToMinMax)
{
Value = MathUtilities.Clamp(Value, Minimum, Maximum);
}
}
/// <summary>
/// Called when the <see cref="Minimum"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnMinimumChanged(double oldValue, double newValue)
{
if (IsInitialized)
{
SetValidSpinDirection();
}
if (ClipValueToMinMax)
{
Value = MathUtilities.Clamp(Value, Minimum, Maximum);
}
}
/// <summary>
/// Called when the <see cref="Text"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnTextChanged(string oldValue, string newValue)
{
if (IsInitialized)
{
SyncTextAndValueProperties(true, Text);
}
}
/// <summary>
/// Called when the <see cref="Value"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnValueChanged(double oldValue, double newValue)
{
if (!_internalValueSet && IsInitialized)
{
SyncTextAndValueProperties(false, null, true);
}
SetValidSpinDirection();
RaiseValueChangedEvent(oldValue, newValue);
}
/// <summary>
/// Called when the <see cref="Increment"/> property has to be coerced.
/// </summary>
/// <param name="baseValue">The value.</param>
protected virtual double OnCoerceIncrement(double baseValue)
{
return baseValue;
}
/// <summary>
/// Called when the <see cref="Maximum"/> property has to be coerced.
/// </summary>
/// <param name="baseValue">The value.</param>
protected virtual double OnCoerceMaximum(double baseValue)
{
return Math.Max(baseValue, Minimum);
}
/// <summary>
/// Called when the <see cref="Minimum"/> property has to be coerced.
/// </summary>
/// <param name="baseValue">The value.</param>
protected virtual double OnCoerceMinimum(double baseValue)
{
return Math.Min(baseValue, Maximum);
}
/// <summary>
/// Called when the <see cref="Value"/> property has to be coerced.
/// </summary>
/// <param name="baseValue">The value.</param>
protected virtual double OnCoerceValue(double baseValue)
{
return baseValue;
}
/// <summary>
/// Raises the OnSpin event when spinning is initiated by the end-user.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSpin(SpinEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
var handler = Spinned;
handler?.Invoke(this, e);
if (e.Direction == SpinDirection.Increase)
{
DoIncrement();
}
else
{
DoDecrement();
}
}
/// <summary>
/// Raises the <see cref="ValueChanged"/> event.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void RaiseValueChangedEvent(double oldValue, double newValue)
{
var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue);
RaiseEvent(e);
}
/// <summary>
/// Converts the formatted text to a value.
/// </summary>
private double ConvertTextToValue(string text)
{
double result = 0;
if (string.IsNullOrEmpty(text))
{
return result;
}
// Since the conversion from Value to text using a FormartString may not be parsable,
// we verify that the already existing text is not the exact same value.
var currentValueText = ConvertValueToText();
if (Equals(currentValueText, text))
{
return Value;
}
result = ConvertTextToValueCore(currentValueText, text);
if (ClipValueToMinMax)
{
return MathUtilities.Clamp(result, Minimum, Maximum);
}
ValidateMinMax(result);
return result;
}
/// <summary>
/// Converts the value to formatted text.
/// </summary>
/// <returns></returns>
private string ConvertValueToText()
{
//Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
if (FormatString.Contains("{0"))
{
return string.Format(CultureInfo, FormatString, Value);
}
return Value.ToString(FormatString, CultureInfo);
}
/// <summary>
/// Called by OnSpin when the spin direction is SpinDirection.Increase.
/// </summary>
private void OnIncrement()
{
var result = Value + Increment;
Value = MathUtilities.Clamp(result, Minimum, Maximum);
}
/// <summary>
/// Called by OnSpin when the spin direction is SpinDirection.Descrease.
/// </summary>
private void OnDecrement()
{
var result = Value - Increment;
Value = MathUtilities.Clamp(result, Minimum, Maximum);
}
/// <summary>
/// Sets the valid spin directions.
/// </summary>
private void SetValidSpinDirection()
{
var validDirections = ValidSpinDirections.None;
// Zero increment always prevents spin.
if (Increment != 0 && !IsReadOnly)
{
if (Value < Maximum)
{
validDirections = validDirections | ValidSpinDirections.Increase;
}
if (Value > Minimum)
{
validDirections = validDirections | ValidSpinDirections.Decrease;
}
}
if (Spinner != null)
{
Spinner.ValidSpinDirection = validDirections;
}
}
/// <summary>
/// Called when the <see cref="CultureInfo"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (CultureInfo)e.OldValue;
var newValue = (CultureInfo)e.NewValue;
upDown.OnCultureInfoChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Increment"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (double)e.OldValue;
var newValue = (double)e.NewValue;
upDown.OnIncrementChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="FormatString"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (string)e.OldValue;
var newValue = (string)e.NewValue;
upDown.OnFormatStringChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="IsReadOnly"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (bool)e.OldValue;
var newValue = (bool)e.NewValue;
upDown.OnIsReadOnlyChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Maximum"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (double)e.OldValue;
var newValue = (double)e.NewValue;
upDown.OnMaximumChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Minimum"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (double)e.OldValue;
var newValue = (double)e.NewValue;
upDown.OnMinimumChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Text"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (string)e.OldValue;
var newValue = (string)e.NewValue;
upDown.OnTextChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Value"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is NumericUpDown upDown)
{
var oldValue = (double)e.OldValue;
var newValue = (double)e.NewValue;
upDown.OnValueChanged(oldValue, newValue);
}
}
private void SetValueInternal(double value)
{
_internalValueSet = true;
try
{
Value = value;
}
finally
{
_internalValueSet = false;
}
}
private static double OnCoerceMaximum(NumericUpDown upDown, double value)
{
return upDown.OnCoerceMaximum(value);
}
private static double OnCoerceMinimum(NumericUpDown upDown, double value)
{
return upDown.OnCoerceMinimum(value);
}
private static double OnCoerceIncrement(NumericUpDown upDown, double value)
{
return upDown.OnCoerceIncrement(value);
}
private void TextBoxOnTextChanged()
{
try
{
_isTextChangedFromUI = true;
if (TextBox != null)
{
Text = TextBox.Text;
}
}
finally
{
_isTextChangedFromUI = false;
}
}
private void OnSpinnerSpin(object sender, SpinEventArgs e)
{
if (AllowSpin && !IsReadOnly)
{
var spin = !e.UsingMouseWheel;
spin |= ((TextBox != null) && TextBox.IsFocused);
if (spin)
{
e.Handled = true;
OnSpin(e);
}
}
}
private void DoDecrement()
{
if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease)
{
OnDecrement();
}
}
private void DoIncrement()
{
if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase)
{
OnIncrement();
}
}
public event EventHandler<SpinEventArgs> Spinned;
private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e)
{
if (e.Device.Captured != Spinner)
{
Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input);
}
}
/// <summary>
/// Defines the <see cref="ValueChanged"/> event.
/// </summary>
public static readonly RoutedEvent<NumericUpDownValueChangedEventArgs> ValueChangedEvent =
RoutedEvent.Register<NumericUpDown, NumericUpDownValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
/// <summary>
/// Raised when the <see cref="Value"/> changes.
/// </summary>
public event EventHandler<SpinEventArgs> ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
private bool CommitInput()
{
return SyncTextAndValueProperties(true, Text);
}
/// <summary>
/// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
/// </summary>
/// <param name="updateValueFromText">If value should be updated from text.</param>
/// <param name="text">The text.</param>
private bool SyncTextAndValueProperties(bool updateValueFromText, string text)
{
return SyncTextAndValueProperties(updateValueFromText, text, false);
}
/// <summary>
/// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
/// </summary>
/// <param name="updateValueFromText">If value should be updated from text.</param>
/// <param name="text">The text.</param>
/// <param name="forceTextUpdate">Force text update.</param>
private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate)
{
if (_isSyncingTextAndValueProperties)
return true;
_isSyncingTextAndValueProperties = true;
var parsedTextIsValid = true;
try
{
if (updateValueFromText)
{
if (!string.IsNullOrEmpty(text))
{
try
{
var newValue = ConvertTextToValue(text);
if (!Equals(newValue, Value))
{
SetValueInternal(newValue);
}
}
catch
{
parsedTextIsValid = false;
}
}
}
// Do not touch the ongoing text input from user.
if (!_isTextChangedFromUI)
{
var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text);
if (!keepEmpty)
{
var newText = ConvertValueToText();
if (!Equals(Text, newText))
{
Text = newText;
}
}
// Sync Text and textBox
if (TextBox != null)
{
TextBox.Text = Text;
}
}
if (_isTextChangedFromUI && !parsedTextIsValid)
{
// Text input was made from the user and the text
// repesents an invalid value. Disable the spinner in this case.
if (Spinner != null)
{
Spinner.ValidSpinDirection = ValidSpinDirections.None;
}
}
else
{
SetValidSpinDirection();
}
}
finally
{
_isSyncingTextAndValueProperties = false;
}
return parsedTextIsValid;
}
private double ConvertTextToValueCore(string currentValueText, string text)
{
double result;
if (IsPercent(FormatString))
{
result = decimal.ToDouble(ParsePercent(text, CultureInfo));
}
else
{
// Problem while converting new text
if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue))
{
var shouldThrow = true;
// Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _))
{
// extract non-digit characters
var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c));
var textSpecialCharacters = text.Where(c => !char.IsDigit(c));
// same non-digit characters on currentValueText and new text => remove them on new Text to parse it again.
if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0)
{
foreach (var character in textSpecialCharacters)
{
text = text.Replace(character.ToString(), string.Empty);
}
// if without the special characters, parsing is good, do not throw
if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue))
{
shouldThrow = false;
}
}
}
if (shouldThrow)
{
throw new InvalidDataException("Input string was not in a correct format.");
}
}
result = outputValue;
}
return result;
}
private void ValidateMinMax(double value)
{
if (value < Minimum)
{
throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum));
}
else if (value > Maximum)
{
throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum));
}
}
/// <summary>
/// Parse percent format text
/// </summary>
/// <param name="text">Text to parse.</param>
/// <param name="cultureInfo">The culture info.</param>
private static decimal ParsePercent(string text, IFormatProvider cultureInfo)
{
var info = NumberFormatInfo.GetInstance(cultureInfo);
text = text.Replace(info.PercentSymbol, null);
var result = decimal.Parse(text, NumberStyles.Any, info);
result = result / 100;
return result;
}
private bool IsPercent(string stringToTest)
{
var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal);
if (PIndex >= 0)
{
//stringToTest contains a "P" between 2 "'", it's considered as text, not percent
var isText = stringToTest.Substring(0, PIndex).Contains("'")
&& stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'");
return !isText;
}
return false;
}
}
}

16
src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs

@ -0,0 +1,16 @@
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
public class NumericUpDownValueChangedEventArgs : RoutedEventArgs
{
public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent)
{
OldValue = oldValue;
NewValue = newValue;
}
public double OldValue { get; }
public double NewValue { get; }
}
}

153
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -4,6 +4,7 @@
using System; using System;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
@ -31,7 +32,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Defines the <see cref="BorderThickness"/> property. /// Defines the <see cref="BorderThickness"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<double> BorderThicknessProperty = public static readonly StyledProperty<Thickness> BorderThicknessProperty =
Border.BorderThicknessProperty.AddOwner<ContentPresenter>(); Border.BorderThicknessProperty.AddOwner<ContentPresenter>();
/// <summary> /// <summary>
@ -57,7 +58,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Defines the <see cref="CornerRadius"/> property. /// Defines the <see cref="CornerRadius"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<float> CornerRadiusProperty = public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<ContentPresenter>(); Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
/// <summary> /// <summary>
@ -76,17 +77,20 @@ namespace Avalonia.Controls.Presenters
/// Defines the <see cref="Padding"/> property. /// Defines the <see cref="Padding"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Thickness> PaddingProperty = public static readonly StyledProperty<Thickness> PaddingProperty =
Border.PaddingProperty.AddOwner<ContentPresenter>(); Decorator.PaddingProperty.AddOwner<ContentPresenter>();
private IControl _child; private IControl _child;
private bool _createdChild; private bool _createdChild;
private IDataTemplate _dataTemplate; private IDataTemplate _dataTemplate;
private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ContentPresenter"/> class. /// Initializes static members of the <see cref="ContentPresenter"/> class.
/// </summary> /// </summary>
static ContentPresenter() static ContentPresenter()
{ {
AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsMeasure(BorderThicknessProperty);
ContentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged); ContentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
ContentTemplateProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged); ContentTemplateProperty.Changed.AddClassHandler<ContentPresenter>(x => x.ContentChanged);
TemplatedParentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.TemplatedParentChanged); TemplatedParentProperty.Changed.AddClassHandler<ContentPresenter>(x => x.TemplatedParentChanged);
@ -120,7 +124,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Gets or sets the thickness of the border. /// Gets or sets the thickness of the border.
/// </summary> /// </summary>
public double BorderThickness public Thickness BorderThickness
{ {
get { return GetValue(BorderThicknessProperty); } get { return GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); } set { SetValue(BorderThicknessProperty, value); }
@ -157,7 +161,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Gets or sets the radius of the border rounded corners. /// Gets or sets the radius of the border rounded corners.
/// </summary> /// </summary>
public float CornerRadius public CornerRadius CornerRadius
{ {
get { return GetValue(CornerRadiusProperty); } get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); } set { SetValue(CornerRadiusProperty, value); }
@ -221,7 +225,7 @@ namespace Avalonia.Controls.Presenters
{ {
var content = Content; var content = Content;
var oldChild = Child; var oldChild = Child;
var newChild = CreateChild(); var newChild = CreateChild();
// Remove the old child if we're not recycling it. // Remove the old child if we're not recycling it.
if (oldChild != null && newChild != oldChild) if (oldChild != null && newChild != oldChild)
@ -277,21 +281,7 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
var background = Background; _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
var borderBrush = BorderBrush;
var borderThickness = BorderThickness;
var cornerRadius = CornerRadius;
var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
if (background != null)
{
context.FillRectangle(background, rect, cornerRadius);
}
if (borderBrush != null && borderThickness > 0)
{
context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
}
} }
/// <summary> /// <summary>
@ -344,7 +334,11 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
return ArrangeOverrideImpl(finalSize, new Vector()); finalSize = ArrangeOverrideImpl(finalSize, new Vector());
_borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
return finalSize;
} }
/// <summary> /// <summary>
@ -372,74 +366,69 @@ namespace Avalonia.Controls.Presenters
internal Size ArrangeOverrideImpl(Size finalSize, Vector offset) internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
{ {
if (Child != null) if (Child == null) return finalSize;
{
var padding = Padding;
var borderThickness = BorderThickness;
var horizontalContentAlignment = HorizontalContentAlignment;
var verticalContentAlignment = VerticalContentAlignment;
var useLayoutRounding = UseLayoutRounding;
var availableSizeMinusMargins = new Size(
Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness),
Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness));
var size = availableSizeMinusMargins;
var scale = GetLayoutScale();
var originX = offset.X + padding.Left + borderThickness;
var originY = offset.Y + padding.Top + borderThickness;
if (horizontalContentAlignment != HorizontalAlignment.Stretch)
{
size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
}
if (verticalContentAlignment != VerticalAlignment.Stretch) var padding = Padding;
{ var borderThickness = BorderThickness;
size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom)); var horizontalContentAlignment = HorizontalContentAlignment;
} var verticalContentAlignment = VerticalContentAlignment;
var useLayoutRounding = UseLayoutRounding;
size = LayoutHelper.ApplyLayoutConstraints(Child, size); var availableSizeMinusMargins = new Size(
Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness.Left - borderThickness.Right),
Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness.Top - borderThickness.Bottom));
var size = availableSizeMinusMargins;
var scale = GetLayoutScale();
var originX = offset.X + padding.Left + borderThickness.Left;
var originY = offset.Y + padding.Top + borderThickness.Top;
if (horizontalContentAlignment != HorizontalAlignment.Stretch)
{
size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
}
if (useLayoutRounding) if (verticalContentAlignment != VerticalAlignment.Stretch)
{ {
size = new Size( size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
Math.Ceiling(size.Width * scale) / scale, }
Math.Ceiling(size.Height * scale) / scale);
availableSizeMinusMargins = new Size(
Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
}
switch (horizontalContentAlignment) if (useLayoutRounding)
{ {
case HorizontalAlignment.Center: size = new Size(
case HorizontalAlignment.Stretch: Math.Ceiling(size.Width * scale) / scale,
originX += (availableSizeMinusMargins.Width - size.Width) / 2; Math.Ceiling(size.Height * scale) / scale);
break; availableSizeMinusMargins = new Size(
case HorizontalAlignment.Right: Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
originX += availableSizeMinusMargins.Width - size.Width; Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
break; }
}
switch (verticalContentAlignment) switch (horizontalContentAlignment)
{ {
case VerticalAlignment.Center: case HorizontalAlignment.Center:
case VerticalAlignment.Stretch: originX += (availableSizeMinusMargins.Width - size.Width) / 2;
originY += (availableSizeMinusMargins.Height - size.Height) / 2; break;
break; case HorizontalAlignment.Right:
case VerticalAlignment.Bottom: originX += availableSizeMinusMargins.Width - size.Width;
originY += availableSizeMinusMargins.Height - size.Height; break;
break; }
}
if (useLayoutRounding) switch (verticalContentAlignment)
{ {
originX = Math.Floor(originX * scale) / scale; case VerticalAlignment.Center:
originY = Math.Floor(originY * scale) / scale; originY += (availableSizeMinusMargins.Height - size.Height) / 2;
} break;
case VerticalAlignment.Bottom:
originY += availableSizeMinusMargins.Height - size.Height;
break;
}
Child.Arrange(new Rect(originX, originY, size.Width, size.Height)); if (useLayoutRounding)
{
originX = Math.Floor(originX * scale) / scale;
originY = Math.Floor(originY * scale) / scale;
} }
Child.Arrange(new Rect(originX, originY, Math.Max(0, size.Width), Math.Max(0, size.Height)));
return finalSize; return finalSize;
} }

4
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="BorderThickness"/> property. /// Defines the <see cref="BorderThickness"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<double> BorderThicknessProperty = public static readonly StyledProperty<Thickness> BorderThicknessProperty =
Border.BorderThicknessProperty.AddOwner<TemplatedControl>(); Border.BorderThicknessProperty.AddOwner<TemplatedControl>();
/// <summary> /// <summary>
@ -132,7 +132,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets or sets the thickness of the control's border. /// Gets or sets the thickness of the control's border.
/// </summary> /// </summary>
public double BorderThickness public Thickness BorderThickness
{ {
get { return GetValue(BorderThicknessProperty); } get { return GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); } set { SetValue(BorderThicknessProperty, value); }

29
src/Avalonia.Controls/TextBox.cs

@ -85,6 +85,7 @@ namespace Avalonia.Controls
private int _selectionEnd; private int _selectionEnd;
private TextPresenter _presenter; private TextPresenter _presenter;
private UndoRedoHelper<UndoRedoState> _undoRedoHelper; private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing;
private bool _ignoreTextChanges; private bool _ignoreTextChanges;
private static readonly string[] invalidCharacters = new String[1]{"\u007f"}; private static readonly string[] invalidCharacters = new String[1]{"\u007f"};
@ -198,7 +199,11 @@ namespace Avalonia.Controls
if (!_ignoreTextChanges) if (!_ignoreTextChanges)
{ {
CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0); CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0);
SetAndRaise(TextProperty, ref _text, value);
if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
{
_undoRedoHelper.Clear();
}
} }
} }
} }
@ -367,14 +372,30 @@ namespace Avalonia.Controls
case Key.Z: case Key.Z:
if (modifiers == InputModifiers.Control) if (modifiers == InputModifiers.Control)
{ {
_undoRedoHelper.Undo(); try
{
_isUndoingRedoing = true;
_undoRedoHelper.Undo();
}
finally
{
_isUndoingRedoing = false;
}
handled = true; handled = true;
} }
break; break;
case Key.Y: case Key.Y:
if (modifiers == InputModifiers.Control) if (modifiers == InputModifiers.Control)
{ {
_undoRedoHelper.Redo(); try
{
_isUndoingRedoing = true;
_undoRedoHelper.Redo();
}
finally
{
_isUndoingRedoing = false;
}
handled = true; handled = true;
} }
break; break;
@ -791,7 +812,7 @@ namespace Avalonia.Controls
int pos = 0; int pos = 0;
int i; int i;
for (i = 0; i < lines.Count; ++i) for (i = 0; i < lines.Count - 1; ++i)
{ {
var line = lines[i]; var line = lines[i];
pos += line.Length; pos += line.Length;

279
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -0,0 +1,279 @@
// 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.
using System;
using Avalonia.Media;
namespace Avalonia.Controls.Utils
{
internal class BorderRenderHelper
{
private bool _useComplexRendering;
private StreamGeometry _backgroundGeometryCache;
private StreamGeometry _borderGeometryCache;
public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
{
if (borderThickness.IsUniform && cornerRadius.IsUniform)
{
_backgroundGeometryCache = null;
_borderGeometryCache = null;
_useComplexRendering = false;
}
else
{
_useComplexRendering = true;
var boundRect = new Rect(finalSize);
var innerRect = boundRect.Deflate(borderThickness);
var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false);
StreamGeometry backgroundGeometry = null;
if (innerRect.Width != 0 && innerRect.Height != 0)
{
backgroundGeometry = new StreamGeometry();
using (var ctx = backgroundGeometry.Open())
{
CreateGeometry(ctx, innerRect, innerCoordinates);
}
_backgroundGeometryCache = backgroundGeometry;
}
else
{
_backgroundGeometryCache = null;
}
if (boundRect.Width != 0 && innerRect.Height != 0)
{
var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true);
var borderGeometry = new StreamGeometry();
using (var ctx = borderGeometry.Open())
{
CreateGeometry(ctx, boundRect, outerCoordinates);
if (backgroundGeometry != null)
{
CreateGeometry(ctx, innerRect, innerCoordinates);
}
}
_borderGeometryCache = borderGeometry;
}
else
{
_borderGeometryCache = null;
}
}
}
public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
{
if (_useComplexRendering)
{
var backgroundGeometry = _backgroundGeometryCache;
if (backgroundGeometry != null)
{
context.DrawGeometry(background, null, backgroundGeometry);
}
var borderGeometry = _borderGeometryCache;
if (borderGeometry != null)
{
context.DrawGeometry(borderBrush, null, borderGeometry);
}
}
else
{
var borderThickness = borders.Left;
var cornerRadius = (float)radii.TopLeft;
var rect = new Rect(size);
if (background != null)
{
context.FillRectangle(background, rect.Deflate(borders), cornerRadius);
}
if (borderBrush != null && borderThickness > 0)
{
context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius);
}
}
}
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates)
{
var topLeft = new Point(borderCoordinates.LeftTop, 0);
var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0);
var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight);
var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight);
var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height);
var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height);
var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft);
var leftTop = new Point(0, borderCoordinates.TopLeft);
if (topLeft.X > topRight.X)
{
var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width;
topLeft = new Point(scaledX, topLeft.Y);
topRight = new Point(scaledX, topRight.Y);
}
if (rightTop.Y > rightBottom.Y)
{
var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height;
rightTop = new Point(rightTop.X, scaledY);
rightBottom = new Point(rightBottom.X, scaledY);
}
if (bottomRight.X < bottomLeft.X)
{
var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width;
bottomRight = new Point(scaledX, bottomRight.Y);
bottomLeft = new Point(scaledX, bottomLeft.Y);
}
if (leftBottom.Y < leftTop.Y)
{
var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height;
leftBottom = new Point(leftBottom.X, scaledY);
leftTop = new Point(leftTop.X, scaledY);
}
var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y);
topLeft += offset;
topRight += offset;
rightTop += offset;
rightBottom += offset;
bottomRight += offset;
bottomLeft += offset;
leftBottom += offset;
leftTop += offset;
context.BeginFigure(topLeft, true);
//Top
context.LineTo(topRight);
//TopRight corner
var radiusX = boundRect.TopRight.X - topRight.X;
var radiusY = rightTop.Y - boundRect.TopRight.Y;
if (radiusX != 0 || radiusY != 0)
{
context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise);
}
//Right
context.LineTo(rightBottom);
//BottomRight corner
radiusX = boundRect.BottomRight.X - bottomRight.X;
radiusY = boundRect.BottomRight.Y - rightBottom.Y;
if (radiusX != 0 || radiusY != 0)
{
context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
}
//Bottom
context.LineTo(bottomLeft);
//BottomLeft corner
radiusX = bottomLeft.X - boundRect.BottomLeft.X;
radiusY = boundRect.BottomLeft.Y - leftBottom.Y;
if (radiusX != 0 || radiusY != 0)
{
context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
}
//Left
context.LineTo(leftTop);
//TopLeft corner
radiusX = topLeft.X - boundRect.TopLeft.X;
radiusY = leftTop.Y - boundRect.TopLeft.Y;
if (radiusX != 0 || radiusY != 0)
{
context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
}
context.EndFigure(true);
}
private struct BorderCoordinates
{
internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter)
{
var left = 0.5 * borderThickness.Left;
var top = 0.5 * borderThickness.Top;
var right = 0.5 * borderThickness.Right;
var bottom = 0.5 * borderThickness.Bottom;
if (isOuter)
{
if (cornerRadius.TopLeft == 0)
{
LeftTop = TopLeft = 0.0;
}
else
{
LeftTop = cornerRadius.TopLeft + left;
TopLeft = cornerRadius.TopLeft + top;
}
if (cornerRadius.TopRight == 0)
{
TopRight = RightTop = 0;
}
else
{
TopRight = cornerRadius.TopRight + top;
RightTop = cornerRadius.TopRight + right;
}
if (cornerRadius.BottomRight == 0)
{
RightBottom = BottomRight = 0;
}
else
{
RightBottom = cornerRadius.BottomRight + right;
BottomRight = cornerRadius.BottomRight + bottom;
}
if (cornerRadius.BottomLeft == 0)
{
BottomLeft = LeftBottom = 0;
}
else
{
BottomLeft = cornerRadius.BottomLeft + bottom;
LeftBottom = cornerRadius.BottomLeft + left;
}
}
else
{
LeftTop = Math.Max(0, cornerRadius.TopLeft - left);
TopLeft = Math.Max(0, cornerRadius.TopLeft - top);
TopRight = Math.Max(0, cornerRadius.TopRight - top);
RightTop = Math.Max(0, cornerRadius.TopRight - right);
RightBottom = Math.Max(0, cornerRadius.BottomRight - right);
BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom);
BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom);
LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left);
}
}
internal readonly double LeftTop;
internal readonly double TopLeft;
internal readonly double TopRight;
internal readonly double RightTop;
internal readonly double RightBottom;
internal readonly double BottomRight;
internal readonly double BottomLeft;
internal readonly double LeftBottom;
}
}
}

64
src/Avalonia.Controls/Utils/ISelectionAdapter.cs

@ -0,0 +1,64 @@
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.Collections;
using Avalonia.Interactivity;
using Avalonia.Input;
namespace Avalonia.Controls.Utils
{
/// <summary>
/// Defines an item collection, selection members, and key handling for the
/// selection adapter contained in the drop-down portion of an
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
/// </summary>
public interface ISelectionAdapter
{
/// <summary>
/// Gets or sets the selected item.
/// </summary>
/// <value>The currently selected item.</value>
object SelectedItem { get; set; }
/// <summary>
/// Occurs when the
/// <see cref="P:Avalonia.Controls.Utils.ISelectionAdapter.SelectedItem" />
/// property value changes.
/// </summary>
event EventHandler<SelectionChangedEventArgs> SelectionChanged;
/// <summary>
/// Gets or sets a collection that is used to generate content for the
/// selection adapter.
/// </summary>
/// <value>The collection that is used to generate content for the
/// selection adapter.</value>
IEnumerable Items { get; set; }
/// <summary>
/// Occurs when a selected item is not cancelled and is committed as the
/// selected item.
/// </summary>
event EventHandler<RoutedEventArgs> Commit;
/// <summary>
/// Occurs when a selection has been canceled.
/// </summary>
event EventHandler<RoutedEventArgs> Cancel;
/// <summary>
/// Provides handling for the
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
/// when a key is pressed while the drop-down portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
/// </summary>
/// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
/// that contains data about the
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
void HandleKeyDown(KeyEventArgs e);
}
}

342
src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs

@ -0,0 +1,342 @@
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Input;
using Avalonia.LogicalTree;
using System.Collections;
using System.Diagnostics;
namespace Avalonia.Controls.Utils
{
/// <summary>
/// Represents the selection adapter contained in the drop-down portion of
/// an <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
/// </summary>
public class SelectingItemsControlSelectionAdapter : ISelectionAdapter
{
/// <summary>
/// The SelectingItemsControl instance.
/// </summary>
private SelectingItemsControl _selector;
/// <summary>
/// Gets or sets a value indicating whether the selection change event
/// should not be fired.
/// </summary>
private bool IgnoringSelectionChanged { get; set; }
/// <summary>
/// Gets or sets the underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
/// <value>The underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.</value>
public SelectingItemsControl SelectorControl
{
get { return _selector; }
set
{
if (_selector != null)
{
_selector.SelectionChanged -= OnSelectionChanged;
_selector.PointerReleased -= OnSelectorPointerReleased;
}
_selector = value;
if (_selector != null)
{
_selector.SelectionChanged += OnSelectionChanged;
_selector.PointerReleased += OnSelectorPointerReleased;
}
}
}
/// <summary>
/// Occurs when the
/// <see cref="P:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.SelectedItem" />
/// property value changes.
/// </summary>
public event EventHandler<SelectionChangedEventArgs> SelectionChanged;
/// <summary>
/// Occurs when an item is selected and is committed to the underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
public event EventHandler<RoutedEventArgs> Commit;
/// <summary>
/// Occurs when a selection is canceled before it is committed.
/// </summary>
public event EventHandler<RoutedEventArgs> Cancel;
/// <summary>
/// Initializes a new instance of the
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />
/// class.
/// </summary>
public SelectingItemsControlSelectionAdapter()
{
}
/// <summary>
/// Initializes a new instance of the
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapterr" />
/// class with the specified
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
/// <param name="selector">The
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" /> control
/// to wrap as a
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />.</param>
public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector)
{
SelectorControl = selector;
}
/// <summary>
/// Gets or sets the selected item of the selection adapter.
/// </summary>
/// <value>The selected item of the underlying selection adapter.</value>
public object SelectedItem
{
get
{
return SelectorControl?.SelectedItem;
}
set
{
IgnoringSelectionChanged = true;
if (SelectorControl != null)
{
SelectorControl.SelectedItem = value;
}
// Attempt to reset the scroll viewer's position
if (value == null)
{
ResetScrollViewer();
}
IgnoringSelectionChanged = false;
}
}
/// <summary>
/// Gets or sets a collection that is used to generate the content of
/// the selection adapter.
/// </summary>
/// <value>The collection used to generate content for the selection
/// adapter.</value>
public IEnumerable Items
{
get
{
return SelectorControl?.Items;
}
set
{
if (SelectorControl != null)
{
SelectorControl.Items = value;
}
}
}
/// <summary>
/// If the control contains a ScrollViewer, this will reset the viewer
/// to be scrolled to the top.
/// </summary>
private void ResetScrollViewer()
{
if (SelectorControl != null)
{
ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType<ScrollViewer>().FirstOrDefault();
if (sv != null)
{
sv.Offset = new Vector(0, 0);
}
}
}
/// <summary>
/// Handles the mouse left button up event on the selector control.
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param>
private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e)
{
if (e.MouseButton == MouseButton.Left)
{
OnCommit();
}
}
/// <summary>
/// Handles the SelectionChanged event on the SelectingItemsControl control.
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The selection changed event data.</param>
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (IgnoringSelectionChanged)
{
return;
}
SelectionChanged?.Invoke(sender, e);
}
/// <summary>
/// Increments the
/// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
/// property of the underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
protected void SelectedIndexIncrement()
{
if (SelectorControl != null)
{
SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1;
}
}
/// <summary>
/// Decrements the
/// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
/// property of the underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
protected void SelectedIndexDecrement()
{
if (SelectorControl != null)
{
int index = SelectorControl.SelectedIndex;
if (index >= 0)
{
SelectorControl.SelectedIndex--;
}
else if (index == -1)
{
SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1;
}
}
}
/// <summary>
/// Provides handling for the
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
/// when a key is pressed while the drop-down portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
/// </summary>
/// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
/// that contains data about the
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
public void HandleKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Enter:
OnCommit();
e.Handled = true;
break;
case Key.Up:
SelectedIndexDecrement();
e.Handled = true;
break;
case Key.Down:
if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None)
{
SelectedIndexIncrement();
e.Handled = true;
}
break;
case Key.Escape:
OnCancel();
e.Handled = true;
break;
default:
break;
}
}
/// <summary>
/// Raises the
/// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Commit" />
/// event.
/// </summary>
protected virtual void OnCommit()
{
OnCommit(this, new RoutedEventArgs());
}
/// <summary>
/// Fires the Commit event.
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param>
private void OnCommit(object sender, RoutedEventArgs e)
{
Commit?.Invoke(sender, e);
AfterAdapterAction();
}
/// <summary>
/// Raises the
/// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Cancel" />
/// event.
/// </summary>
protected virtual void OnCancel()
{
OnCancel(this, new RoutedEventArgs());
}
/// <summary>
/// Fires the Cancel event.
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param>
private void OnCancel(object sender, RoutedEventArgs e)
{
Cancel?.Invoke(sender, e);
AfterAdapterAction();
}
/// <summary>
/// Change the selection after the actions are complete.
/// </summary>
private void AfterAdapterAction()
{
IgnoringSelectionChanged = true;
if (SelectorControl != null)
{
SelectorControl.SelectedItem = null;
SelectorControl.SelectedIndex = -1;
}
IgnoringSelectionChanged = false;
}
}
}

5
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@ -91,6 +91,11 @@ namespace Avalonia.Controls.Utils
} }
} }
public void Clear()
{
_states.Clear();
}
bool WeakTimer.IWeakTimerSubscriber.Tick() bool WeakTimer.IWeakTimerSubscriber.Tick()
{ {
Snapshot(); Snapshot();

2
src/Avalonia.Input/KeyboardDevice.cs

@ -46,13 +46,13 @@ namespace Avalonia.Input
if (element != FocusedElement) if (element != FocusedElement)
{ {
var interactive = FocusedElement as IInteractive; var interactive = FocusedElement as IInteractive;
FocusedElement = element;
interactive?.RaiseEvent(new RoutedEventArgs interactive?.RaiseEvent(new RoutedEventArgs
{ {
RoutedEvent = InputElement.LostFocusEvent, RoutedEvent = InputElement.LostFocusEvent,
}); });
FocusedElement = element;
interactive = element as IInteractive; interactive = element as IInteractive;
interactive?.RaiseEvent(new GotFocusEventArgs interactive?.RaiseEvent(new GotFocusEventArgs

2
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@ -20,7 +20,7 @@
<SolidColorBrush x:Key="ErrorBrush">Red</SolidColorBrush> <SolidColorBrush x:Key="ErrorBrush">Red</SolidColorBrush>
<SolidColorBrush x:Key="ErrorBrushLight">#10ff0000</SolidColorBrush> <SolidColorBrush x:Key="ErrorBrushLight">#10ff0000</SolidColorBrush>
<sys:Double x:Key="ThemeBorderThickness">2</sys:Double> <Thickness x:Key="ThemeBorderThickness">2</Thickness>
<sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double> <sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double>
<sys:Double x:Key="FontSizeSmall">10</sys:Double> <sys:Double x:Key="FontSizeSmall">10</sys:Double>

43
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@ -0,0 +1,43 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="AutoCompleteBox">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="Template">
<ControlTemplate>
<Panel>
<TextBox Name="PART_TextBox"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
Watermark="{TemplateBinding Watermark}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" />
<Popup Name="PART_Popup"
MinWidth="{TemplateBinding Bounds.Width}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
StaysOpen="False">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<ListBox Name="PART_SelectingItemsControl"
BorderThickness="0"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding ValueMemberSelector}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Border>
</Popup>
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="AutoCompleteBox ListBoxItem:pointerover">
<Setter Property="Background" Value="#ffd0d0d0"/>
</Style>
</Styles>

4
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -23,7 +23,7 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.RadioButton.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.RadioButton.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.RepeatButton.xaml?assembly=Avalonia.Themes.Default" /> <StyleInclude Source="resm:Avalonia.Themes.Default.RepeatButton.xaml?assembly=Avalonia.Themes.Default" />
<StyleInclude Source="resm:Avalonia.Themes.Default.Separator.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.Separator.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Slider.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.Slider.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ScrollBar.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.ScrollBar.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ScrollViewer.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.ScrollViewer.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.TabStrip.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.TabStrip.xaml?assembly=Avalonia.Themes.Default"/>
@ -43,4 +43,6 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.Calendar.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.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ButtonSpinner.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.ButtonSpinner.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.NumericUpDown.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.AutoCompleteBox.xaml?assembly=Avalonia.Themes.Default"/>
</Styles> </Styles>

4
src/Avalonia.Themes.Default/MenuItem.xaml

@ -133,4 +133,8 @@
<Style Selector="MenuItem:empty /template/ Path#rightArrow"> <Style Selector="MenuItem:empty /template/ Path#rightArrow">
<Setter Property="IsVisible" Value="False"/> <Setter Property="IsVisible" Value="False"/>
</Style> </Style>
<Style Selector="MenuItem:disabled">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
</Style>
</Styles> </Styles>

41
src/Avalonia.Themes.Default/NumericUpDown.xaml

@ -0,0 +1,41 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="NumericUpDown">
<Setter Property="TemplatedControl.BorderBrush" Value="{DynamicResource ThemeBorderLightBrush}"/>
<Setter Property="TemplatedControl.BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="TemplatedControl.Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="TemplatedControl.Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="TemplatedControl.Template">
<ControlTemplate>
<ButtonSpinner Name="PART_Spinner"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
AllowSpin="{TemplateBinding AllowSpin}"
ShowButtonSpinner="{TemplateBinding ShowButtonSpinner}"
ButtonSpinnerLocation="{TemplateBinding ButtonSpinnerLocation}">
<TextBox Name="PART_TextBox"
BorderThickness="0"
Background="Transparent"
ContextMenu="{TemplateBinding ContextMenu}"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontStyle="{TemplateBinding FontStyle}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}"
Watermark="{TemplateBinding Watermark}"
IsReadOnly="{TemplateBinding IsReadOnly}"
Text="{TemplateBinding Text}"
Padding="{TemplateBinding Padding}"
TextAlignment="Left"
Margin="1"
MinWidth="20"
AcceptsReturn="False"
TextWrapping="NoWrap">
</TextBox>
</ButtonSpinner>
</ControlTemplate>
</Setter>
</Style>
</Styles>

97
src/Avalonia.Visuals/CornerRadius.cs

@ -0,0 +1,97 @@
// 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.
using System;
using System.Globalization;
using System.Linq;
namespace Avalonia
{
public struct CornerRadius
{
public CornerRadius(double uniformRadius)
{
TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius;
}
public CornerRadius(double top, double bottom)
{
TopLeft = TopRight = top;
BottomLeft = BottomRight = bottom;
}
public CornerRadius(double topLeft, double topRight, double bottomRight, double bottomLeft)
{
TopLeft = topLeft;
TopRight = topRight;
BottomRight = bottomRight;
BottomLeft = bottomLeft;
}
public double TopLeft { get; }
public double TopRight { get; }
public double BottomRight { get; }
public double BottomLeft { get; }
public bool IsEmpty => TopLeft.Equals(0) && IsUniform;
public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight);
public override bool Equals(object obj)
{
if (obj is CornerRadius)
{
return this == (CornerRadius)obj;
}
return false;
}
public override int GetHashCode()
{
return TopLeft.GetHashCode() ^ TopRight.GetHashCode() ^ BottomLeft.GetHashCode() ^ BottomRight.GetHashCode();
}
public override string ToString()
{
return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}";
}
public static CornerRadius Parse(string s, CultureInfo culture)
{
var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToList();
switch (parts.Count)
{
case 1:
var uniform = double.Parse(parts[0], culture);
return new CornerRadius(uniform);
case 2:
var top = double.Parse(parts[0], culture);
var bottom = double.Parse(parts[1], culture);
return new CornerRadius(top, bottom);
case 4:
var topLeft = double.Parse(parts[0], culture);
var topRight = double.Parse(parts[1], culture);
var bottomRight = double.Parse(parts[2], culture);
var bottomLeft = double.Parse(parts[3], culture);
return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft);
default:
{
throw new FormatException("Invalid CornerRadius.");
}
}
}
public static bool operator ==(CornerRadius cr1, CornerRadius cr2)
{
return cr1.TopLeft.Equals(cr2.TopLeft)
&& cr1.TopRight.Equals(cr2.TopRight)
&& cr1.BottomRight.Equals(cr2.BottomRight)
&& cr1.BottomLeft.Equals(cr2.BottomLeft);
}
public static bool operator !=(CornerRadius cr1, CornerRadius cr2)
{
return !(cr1 == cr2);
}
}
}

2
src/Avalonia.Visuals/Media/GradientBrush.cs

@ -22,7 +22,7 @@ namespace Avalonia.Media
/// Defines the <see cref="GradientStops"/> property. /// Defines the <see cref="GradientStops"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IList<GradientStop>> GradientStopsProperty = public static readonly StyledProperty<IList<GradientStop>> GradientStopsProperty =
AvaloniaProperty.Register<GradientBrush, IList<GradientStop>>(nameof(Opacity)); AvaloniaProperty.Register<GradientBrush, IList<GradientStop>>(nameof(GradientStops));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GradientBrush"/> class. /// Initializes a new instance of the <see cref="GradientBrush"/> class.

1
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@ -9,6 +9,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] [assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]

4
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -167,7 +167,9 @@ namespace Avalonia.Rendering.SceneGraph
using (context.PushPostTransform(m)) using (context.PushPostTransform(m))
using (context.PushTransformContainer()) using (context.PushTransformContainer())
{ {
var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip); var clipBounds = clipToBounds ?
bounds.TransformToAABB(contextImpl.Transform).Intersect(clip) :
clip;
forceRecurse = forceRecurse || forceRecurse = forceRecurse ||
node.ClipBounds != clipBounds || node.ClipBounds != clipBounds ||

7
src/Avalonia.Visuals/Thickness.cs

@ -90,7 +90,12 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets a value indicating whether all sides are set to 0. /// Gets a value indicating whether all sides are set to 0.
/// </summary> /// </summary>
public bool IsEmpty => Left == 0 && Top == 0 && Right == 0 && Bottom == 0; public bool IsEmpty => Left.Equals(0) && IsUniform;
/// <summary>
/// Gets a value indicating whether all sides are equal.
/// </summary>
public bool IsUniform => Left.Equals(Right) && Top.Equals(Bottom) && Right.Equals(Bottom);
/// <summary> /// <summary>
/// Compares two Thicknesses. /// Compares two Thicknesses.

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -31,6 +31,7 @@
</Compile> </Compile>
<Compile Include="AvaloniaXamlLoaderPortableXaml.cs" /> <Compile Include="AvaloniaXamlLoaderPortableXaml.cs" />
<Compile Include="AvaloniaXamlLoader.cs" /> <Compile Include="AvaloniaXamlLoader.cs" />
<Compile Include="Converters\CornerRadiusTypeConverter.cs" />
<Compile Include="Converters\MatrixTypeConverter.cs" /> <Compile Include="Converters\MatrixTypeConverter.cs" />
<Compile Include="Converters\RectTypeConverter.cs" /> <Compile Include="Converters\RectTypeConverter.cs" />
<Compile Include="Converters\SetterValueTypeConverter.cs" /> <Compile Include="Converters\SetterValueTypeConverter.cs" />

19
src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs

@ -0,0 +1,19 @@
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Markup.Xaml.Converters
{
public class CornerRadiusTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return CornerRadius.Parse((string)value, culture);
}
}
}

1
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -43,6 +43,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Mode = Mode, Mode = Mode,
Path = pathInfo.Path, Path = pathInfo.Path,
Priority = Priority, Priority = Priority,
Source = Source,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource, RelativeSource = pathInfo.RelativeSource ?? RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider)) DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
}; };

1
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs

@ -43,6 +43,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml
{ typeof(Selector), typeof(SelectorTypeConverter)}, { typeof(Selector), typeof(SelectorTypeConverter)},
{ typeof(SolidColorBrush), typeof(BrushTypeConverter) }, { typeof(SolidColorBrush), typeof(BrushTypeConverter) },
{ typeof(Thickness), typeof(ThicknessTypeConverter) }, { typeof(Thickness), typeof(ThicknessTypeConverter) },
{ typeof(CornerRadius), typeof(CornerRadiusTypeConverter) },
{ typeof(TimeSpan), typeof(TimeSpanTypeConverter) }, { typeof(TimeSpan), typeof(TimeSpanTypeConverter) },
//{ typeof(Uri), typeof(Converters.UriTypeConverter) }, //{ typeof(Uri), typeof(Converters.UriTypeConverter) },
{ typeof(Cursor), typeof(CursorTypeConverter) }, { typeof(Cursor), typeof(CursorTypeConverter) },

7
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.Direct2D1; using SharpDX.Direct2D1;
@ -20,14 +19,12 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc/> /// <inheritdoc/>
public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia(); public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia();
/// <inheritdoc/>
public Geometry Geometry { get; } public Geometry Geometry { get; }
/// <inheritdoc/> /// <inheritdoc/>
public Rect GetRenderBounds(Avalonia.Media.Pen pen) public Rect GetRenderBounds(Avalonia.Media.Pen pen)
{ {
var factory = AvaloniaLocator.Current.GetService<Factory>(); return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia();
return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -51,7 +48,7 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc/> /// <inheritdoc/>
public bool StrokeContains(Avalonia.Media.Pen pen, Point point) public bool StrokeContains(Avalonia.Media.Pen pen, Point point)
{ {
return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness); return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0));
} }
public ITransformedGeometryImpl WithTransform(Matrix transform) public ITransformedGeometryImpl WithTransform(Matrix transform)

2
tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs

@ -33,7 +33,7 @@ namespace Avalonia.Benchmarks.Styling
var border = (Border)textBox.GetVisualChildren().Single(); var border = (Border)textBox.GetVisualChildren().Single();
if (border.BorderThickness != 2) if (border.BorderThickness != new Thickness(2))
{ {
throw new Exception("Styles not applied."); throw new Exception("Styles not applied.");
} }

1042
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

File diff suppressed because it is too large

24
tests/Avalonia.Controls.UnitTests/BorderTests.cs

@ -13,12 +13,34 @@ namespace Avalonia.Controls.UnitTests
var target = new Border var target = new Border
{ {
Padding = new Thickness(6), Padding = new Thickness(6),
BorderThickness = 4, BorderThickness = new Thickness(4)
}; };
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
Assert.Equal(new Size(20, 20), target.DesiredSize); Assert.Equal(new Size(20, 20), target.DesiredSize);
} }
[Fact]
public void Child_Should_Arrange_With_Zero_Height_Width_If_Padding_Greater_Than_Child_Size()
{
Border content;
var target = new Border
{
Padding = new Thickness(6),
MaxHeight = 12,
MaxWidth = 12,
Child = content = new Border
{
Height = 0,
Width = 0
}
};
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(6, 6, 0, 0), content.Bounds);
}
} }
} }

50
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs

@ -80,6 +80,31 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds); Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
} }
[Fact]
public void Should_Correctly_Align_Child_With_Fixed_Size()
{
Border content;
var target = new ContentPresenter
{
HorizontalContentAlignment = HorizontalAlignment.Stretch,
VerticalContentAlignment = VerticalAlignment.Stretch,
Content = content = new Border
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Bottom,
Width = 16,
Height = 16,
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Check correct result for Issue #1447.
Assert.Equal(new Rect(0, 84, 16, 16), content.Bounds);
}
[Fact] [Fact]
public void Content_Can_Be_Stretched() public void Content_Can_Be_Stretched()
{ {
@ -185,5 +210,30 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds); Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds);
} }
[Fact]
public void Child_Arrange_With_Zero_Height_When_Padding_Height_Greater_Than_Child_Height()
{
Border content;
var target = new ContentPresenter
{
Padding = new Thickness(32),
MaxHeight = 32,
MaxWidth = 32,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
Content = content = new Border
{
Height = 0,
Width = 0,
},
};
target.UpdateChild();
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(48, 48, 0, 0), content.Bounds);
}
} }
} }

66
tests/Avalonia.RenderTests/Controls/BorderTests.cs

@ -31,7 +31,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 1, BorderThickness = new Thickness(1),
} }
}; };
@ -50,7 +50,47 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
}
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task Border_Uniform_CornerRadius()
{
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 200,
Height = 200,
Child = new Border
{
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(2),
CornerRadius = new CornerRadius(16),
}
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task Border_NonUniform_CornerRadius()
{
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 200,
Height = 200,
Child = new Border
{
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(2),
CornerRadius = new CornerRadius(16, 4, 7, 10),
} }
}; };
@ -87,7 +127,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new Border Child = new Border
{ {
Background = Brushes.Red, Background = Brushes.Red,
@ -110,7 +150,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Padding = new Thickness(2), Padding = new Thickness(2),
Child = new Border Child = new Border
{ {
@ -134,7 +174,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new Border Child = new Border
{ {
Background = Brushes.Red, Background = Brushes.Red,
@ -159,7 +199,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new TextBlock Child = new TextBlock
{ {
Text = "Foo", Text = "Foo",
@ -186,7 +226,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new TextBlock Child = new TextBlock
{ {
Text = "Foo", Text = "Foo",
@ -213,7 +253,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new TextBlock Child = new TextBlock
{ {
Text = "Foo", Text = "Foo",
@ -240,7 +280,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new TextBlock Child = new TextBlock
{ {
Text = "Foo", Text = "Foo",
@ -267,7 +307,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new TextBlock Child = new TextBlock
{ {
Text = "Foo", Text = "Foo",
@ -294,7 +334,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new TextBlock Child = new TextBlock
{ {
Text = "Foo", Text = "Foo",
@ -321,7 +361,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new TextBlock Child = new TextBlock
{ {
Text = "Foo", Text = "Foo",
@ -348,7 +388,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Black, BorderBrush = Brushes.Black,
BorderThickness = 2, BorderThickness = new Thickness(2),
Child = new TextBlock Child = new TextBlock
{ {
Text = "Foo", Text = "Foo",

2
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@ -42,7 +42,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
new Border new Border
{ {
BorderBrush = Brushes.Blue, BorderBrush = Brushes.Blue,
BorderThickness = 2, BorderThickness = new Thickness(2),
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock Child = new TextBlock

2
tests/Avalonia.RenderTests/Shapes/PathTests.cs

@ -316,7 +316,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
Child = new Border Child = new Border
{ {
BorderBrush = Brushes.Red, BorderBrush = Brushes.Red,
BorderThickness = 1, BorderThickness = new Thickness(1),
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
Child = new Path Child = new Path

6
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@ -151,7 +151,7 @@ namespace Avalonia.Styling.UnitTests
{ {
Setters = new[] Setters = new[]
{ {
new Setter(Border.BorderThicknessProperty, 4), new Setter(Border.BorderThicknessProperty, new Thickness(4)),
} }
}; };
@ -162,9 +162,9 @@ namespace Avalonia.Styling.UnitTests
style.Attach(border, null); style.Attach(border, null);
Assert.Equal(4, border.BorderThickness); Assert.Equal(new Thickness(4), border.BorderThickness);
root.Child = null; root.Child = null;
Assert.Equal(0, border.BorderThickness); Assert.Equal(new Thickness(0), border.BorderThickness);
} }
private class Class1 : Control private class Class1 : Control

43
tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs

@ -0,0 +1,43 @@
// 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.
using System.Globalization;
using Xunit;
namespace Avalonia.Visuals.UnitTests
{
public class CornerRadiusTests
{
[Fact]
public void Parse_Parses_Single_Uniform_Radius()
{
var result = CornerRadius.Parse("3.4", CultureInfo.InvariantCulture);
Assert.Equal(new CornerRadius(3.4), result);
}
[Fact]
public void Parse_Parses_Top_Bottom()
{
var result = CornerRadius.Parse("1.1,2.2", CultureInfo.InvariantCulture);
Assert.Equal(new CornerRadius(1.1, 2.2), result);
}
[Fact]
public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft()
{
var result = CornerRadius.Parse("1.1,2.2,3.3,4.4", CultureInfo.InvariantCulture);
Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
}
[Fact]
public void Parse_Accepts_Spaces()
{
var result = CornerRadius.Parse("1.1 2.2 3.3 4.4", CultureInfo.InvariantCulture);
Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
}
}
}

48
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@ -83,6 +83,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Margin = new Thickness(10, 20, 30, 40), Margin = new Thickness(10, 20, 30, 40),
Child = canvas = new Canvas Child = canvas = new Canvas
{ {
ClipToBounds = true,
Background = Brushes.AliceBlue, Background = Brushes.AliceBlue,
} }
} }
@ -129,6 +130,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
(border = new Border (border = new Border
{ {
Background = Brushes.AliceBlue, Background = Brushes.AliceBlue,
ClipToBounds = true,
Width = 100, Width = 100,
Height = 100, Height = 100,
[Canvas.LeftProperty] = 50, [Canvas.LeftProperty] = 50,
@ -173,6 +175,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
(border = new Border (border = new Border
{ {
Background = Brushes.AliceBlue, Background = Brushes.AliceBlue,
ClipToBounds = true,
Width = 100, Width = 100,
Height = 100, Height = 100,
[Canvas.LeftProperty] = 50, [Canvas.LeftProperty] = 50,
@ -254,6 +257,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Margin = new Thickness(24, 26), Margin = new Thickness(24, 26),
Child = target = new Border Child = target = new Border
{ {
ClipToBounds = true,
Margin = new Thickness(26, 24), Margin = new Thickness(26, 24),
Width = 100, Width = 100,
Height = 100, Height = 100,
@ -515,6 +519,50 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
} }
} }
[Fact]
public void Should_Update_ClipBounds_For_Negative_Margin()
{
using (TestApplication())
{
Decorator decorator;
Border border;
var tree = new TestRoot
{
Width = 100,
Height = 100,
Child = decorator = new Decorator
{
Margin = new Thickness(0, 10, 0, 0),
Child = border = new Border
{
Background = Brushes.Red,
ClipToBounds = true,
Margin = new Thickness(0, -5, 0, 0),
}
}
};
var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
layout.ExecuteInitialLayoutPass(tree);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
var borderNode = scene.FindNode(border);
Assert.Equal(new Rect(0, 5, 100, 95), borderNode.ClipBounds);
border.Margin = new Thickness(0, -8, 0, 0);
layout.ExecuteLayoutPass();
scene = scene.CloneScene();
sceneBuilder.Update(scene, border);
borderNode = scene.FindNode(border);
Assert.Equal(new Rect(0, 2, 100, 98), borderNode.ClipBounds);
}
}
[Fact] [Fact]
public void Should_Update_Descendent_Tranform_When_Margin_Changed() public void Should_Update_Descendent_Tranform_When_Margin_Changed()
{ {

4
tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs

@ -4,7 +4,7 @@
using System.Globalization; using System.Globalization;
using Xunit; using Xunit;
namespace Avalonia.Visuals.UnitTests.Media namespace Avalonia.Visuals.UnitTests
{ {
public class ThicknessTests public class ThicknessTests
{ {
@ -40,4 +40,4 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result); Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result);
} }
} }
} }

BIN
tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Loading…
Cancel
Save