diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index f4d81418ac..edf3d41bf5 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -11,8 +11,7 @@ Spacing="16"> (Enumerable.Range(1, 10000).Select(i => GenerateItem())); - SelectedItems = new ObservableCollection(); - - AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); - - RemoveItemCommand = ReactiveCommand.Create(() => - { - while (SelectedItems.Count > 0) - { - Items.Remove(SelectedItems[0]); - } - }); - - SelectRandomItemCommand = ReactiveCommand.Create(() => - { - var random = new Random(); - - SelectedItem = Items[random.Next(Items.Count - 1)]; - }); - } - - public ObservableCollection Items { get; } - - private string _selectedItem; - - public string SelectedItem - { - get { return _selectedItem; } - set { this.RaiseAndSetIfChanged(ref _selectedItem, value); } - } - - - public ObservableCollection SelectedItems { get; } - - public ReactiveCommand AddItemCommand { get; } - - public ReactiveCommand RemoveItemCommand { get; } - - public ReactiveCommand SelectRandomItemCommand { get; } - - public SelectionMode SelectionMode - { - get => _selectionMode; - set - { - SelectedItems.Clear(); - this.RaiseAndSetIfChanged(ref _selectionMode, value); - } - } - - private string GenerateItem() => $"Item {_counter++.ToString()}"; - } } } diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs new file mode 100644 index 0000000000..6bdb5c0103 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class ListBoxPageViewModel : ReactiveObject + { + private int _counter; + private SelectionMode _selectionMode; + + public ListBoxPageViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); + Selection = new SelectionModel(); + Selection.Select(1); + + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); + + RemoveItemCommand = ReactiveCommand.Create(() => + { + while (Selection.SelectedItems.Count > 0) + { + Items.Remove((string)Selection.SelectedItems.First()); + } + }); + + SelectRandomItemCommand = ReactiveCommand.Create(() => + { + var random = new Random(); + + using (Selection.Update()) + { + Selection.ClearSelection(); + Selection.Select(random.Next(Items.Count - 1)); + } + }); + } + + public ObservableCollection Items { get; } + + public SelectionModel Selection { get; } + + public ReactiveCommand AddItemCommand { get; } + + public ReactiveCommand RemoveItemCommand { get; } + + public ReactiveCommand SelectRandomItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + Selection.ClearSelection(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private string GenerateItem() => $"Item {_counter++.ToString()}"; + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs index ecf8abc13f..def9301e2d 100644 --- a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs +++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs @@ -25,7 +25,6 @@ namespace Avalonia.Controls { private readonly IList _inner; private INotifyCollectionChanged _notifyCollectionChanged; - private int _cachedSize = -1; /// /// Initializes a new instance of the ItemsSourceView class for the specified data source. @@ -54,18 +53,7 @@ namespace Avalonia.Controls /// /// Gets the number of items in the collection. /// - public int Count - { - get - { - if (_cachedSize == -1) - { - _cachedSize = _inner.Count; - } - - return _cachedSize; - } - } + public int Count => _inner.Count; /// /// Gets a value that indicates whether the items source can provide a unique key for each item. @@ -126,7 +114,6 @@ namespace Avalonia.Controls protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) { - _cachedSize = _inner.Count; CollectionChanged?.Invoke(this, args); } diff --git a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs index c127771990..91cef9fe64 100644 --- a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs +++ b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Avalonia.Collections; @@ -16,6 +17,7 @@ namespace Avalonia.Controls.Utils private IList? _items; private bool _updatingItems; private bool _updatingModel; + private bool _initializeOnSourceAssignment; public SelectedItemsSync(ISelectionModel model) { @@ -63,10 +65,18 @@ namespace Avalonia.Controls.Utils _updatingModel = true; _items = items; - using (Model.Update()) + if (Model.Source is object) { - Model.ClearSelection(); - Add(items); + using (Model.Update()) + { + Model.ClearSelection(); + Add(items); + } + } + else if (!_initializeOnSourceAssignment) + { + Model.PropertyChanged += SelectionModelPropertyChanged; + _initializeOnSourceAssignment = true; } if (_items is INotifyCollectionChanged incc2) @@ -86,9 +96,11 @@ namespace Avalonia.Controls.Utils if (_items != null) { + Model.PropertyChanged -= SelectionModelPropertyChanged; Model.SelectionChanged -= SelectionModelSelectionChanged; Model = model; Model.SelectionChanged += SelectionModelSelectionChanged; + _initializeOnSourceAssignment = false; try { @@ -175,6 +187,25 @@ namespace Avalonia.Controls.Utils } } + private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (_initializeOnSourceAssignment && + _items != null && + e.PropertyName == nameof(SelectionModel.Source)) + { + try + { + _updatingModel = true; + Add(_items); + _initializeOnSourceAssignment = false; + } + finally + { + _updatingModel = false; + } + } + } + private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { if (_updatingModel) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 4274f7ee4d..44dfb9ea48 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -58,6 +58,9 @@ + + + 1,1,1,1 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index b1c0946d0f..9ed3207235 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -61,6 +61,9 @@ + + + 1 diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/DatePicker.xaml new file mode 100644 index 0000000000..da878c88e2 --- /dev/null +++ b/src/Avalonia.Themes.Default/DatePicker.xaml @@ -0,0 +1,334 @@ + + + + + 0,0,0,4 + 40 + 40 + 41 + 296 + 456 + 0,3,0,6 + 9,3,0,6 + 0,3,0,6 + 9,3,0,6 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 4e63d1d223..e5b654b490 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -55,4 +55,7 @@ + + + diff --git a/src/Avalonia.Themes.Default/SplitView.xaml b/src/Avalonia.Themes.Default/SplitView.xaml new file mode 100644 index 0000000000..4a9a7be164 --- /dev/null +++ b/src/Avalonia.Themes.Default/SplitView.xaml @@ -0,0 +1,219 @@ + + + + 320 + 48 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/TimePicker.xaml b/src/Avalonia.Themes.Default/TimePicker.xaml new file mode 100644 index 0000000000..6c4d0c97bd --- /dev/null +++ b/src/Avalonia.Themes.Default/TimePicker.xaml @@ -0,0 +1,283 @@ + + + + + 40 + 1 + 1 + 0,0,0,4 + 40 + 41 + 242 + 456 + 0,3,0,6 + 0,3,0,6 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 9318fcc68e..b116249fd4 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -31,7 +31,8 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: { - textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties); + textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties, + nextLineBreak); break; } default: @@ -118,7 +119,7 @@ namespace Avalonia.Media.TextFormatting /// The text run's. /// The length to split at. /// The split text runs. - internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList textRuns, int length) + internal static SplitTextRunsResult SplitTextRuns(List textRuns, int length) { var currentLength = 0; @@ -134,13 +135,13 @@ namespace Avalonia.Media.TextFormatting var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; - var first = new ShapedTextCharacters[firstCount]; + var first = new List(firstCount); if (firstCount > 1) { for (var j = 0; j < i; j++) { - first[j] = textRuns[j]; + first.Add(textRuns[j]); } } @@ -148,7 +149,7 @@ namespace Avalonia.Media.TextFormatting if (currentLength + currentRun.GlyphRun.Characters.Length == length) { - var second = new ShapedTextCharacters[secondCount]; + var second = new List(secondCount); var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; @@ -156,11 +157,11 @@ namespace Avalonia.Media.TextFormatting { for (var j = 0; j < secondCount; j++) { - second[j] = textRuns[i + j + offset]; + second.Add(textRuns[i + j + offset]); } } - first[i] = currentRun; + first.Add(currentRun); return new SplitTextRunsResult(first, second); } @@ -168,22 +169,22 @@ namespace Avalonia.Media.TextFormatting { secondCount++; - var second = new ShapedTextCharacters[secondCount]; + var second = new List(secondCount); + + var split = currentRun.Split(length - currentLength); + + first.Add(split.First); + + second.Add(split.Second); if (secondCount > 0) { for (var j = 1; j < secondCount; j++) { - second[j] = textRuns[i + j]; + second.Add(textRuns[i + j]); } } - var split = currentRun.Split(length - currentLength); - - first[i] = split.First; - - second[0] = split.Second; - return new SplitTextRunsResult(first, second); } } @@ -201,7 +202,7 @@ namespace Avalonia.Media.TextFormatting /// /// The formatted text runs. /// - private static IReadOnlyList FetchTextRuns(ITextSource textSource, + private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak) { nextLineBreak = default; @@ -212,8 +213,10 @@ namespace Avalonia.Media.TextFormatting if (previousLineBreak != null) { - foreach (var shapedCharacters in previousLineBreak.RemainingCharacters) + for (var index = 0; index < previousLineBreak.RemainingCharacters.Count; index++) { + var shapedCharacters = previousLineBreak.RemainingCharacters[index]; + if (shapedCharacters == null) { continue; @@ -225,6 +228,14 @@ namespace Avalonia.Media.TextFormatting { var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); + if (++index < previousLineBreak.RemainingCharacters.Count) + { + for (; index < previousLineBreak.RemainingCharacters.Count; index++) + { + splitResult.Second.Add(previousLineBreak.RemainingCharacters[index]); + } + } + nextLineBreak = new TextLineBreak(splitResult.Second); return splitResult.First; @@ -323,9 +334,10 @@ namespace Avalonia.Media.TextFormatting /// The text range that is covered by the text runs. /// The paragraph width. /// The text paragraph properties. + /// The current line break if the line was explicitly broken. /// The wrapped text line. - private static TextLine PerformTextWrapping(IReadOnlyList textRuns, TextRange textRange, - double paragraphWidth, TextParagraphProperties paragraphProperties) + private static TextLine PerformTextWrapping(List textRuns, TextRange textRange, + double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak currentLineBreak) { var availableWidth = paragraphWidth; var currentWidth = 0.0; @@ -388,8 +400,22 @@ namespace Avalonia.Media.TextFormatting var textLineMetrics = TextLineMetrics.Create(splitResult.First, new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties); - var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ? - new TextLineBreak(splitResult.Second) : + var remainingCharacters = splitResult.Second; + + if (currentLineBreak?.RemainingCharacters != null) + { + if (remainingCharacters != null) + { + remainingCharacters.AddRange(currentLineBreak.RemainingCharacters); + } + else + { + remainingCharacters = new List(currentLineBreak.RemainingCharacters); + } + } + + var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ? + new TextLineBreak(remainingCharacters) : null; return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak); @@ -403,7 +429,10 @@ namespace Avalonia.Media.TextFormatting } return new TextLineImpl(textRuns, - TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties), + currentLineBreak?.RemainingCharacters != null ? + new TextLineBreak(currentLineBreak.RemainingCharacters) : + null); } /// @@ -434,7 +463,7 @@ namespace Avalonia.Media.TextFormatting internal readonly struct SplitTextRunsResult { - public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second) + public SplitTextRunsResult(List first, List second) { First = first; @@ -447,7 +476,7 @@ namespace Avalonia.Media.TextFormatting /// /// The first text runs. /// - public IReadOnlyList First { get; } + public List First { get; } /// /// Gets the second text runs. @@ -455,7 +484,7 @@ namespace Avalonia.Media.TextFormatting /// /// The second text runs. /// - public IReadOnlyList Second { get; } + public List Second { get; } } private struct TextRunEnumerator diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 14602a2560..fa7d6cb4bf 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -183,7 +183,10 @@ namespace Avalonia.Media.TextFormatting var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice(s_empty, startingIndex, 1), properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo); - var textRuns = new[] { new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) }; + var textRuns = new List + { + new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) + }; return new TextLineImpl(textRuns, TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties)); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index 435752160e..8b44e32c48 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -6,9 +6,9 @@ namespace Avalonia.Media.TextFormatting { internal class TextLineImpl : TextLine { - private readonly IReadOnlyList _textRuns; + private readonly List _textRuns; - public TextLineImpl(IReadOnlyList textRuns, TextLineMetrics lineMetrics, + public TextLineImpl(List textRuns, TextLineMetrics lineMetrics, TextLineBreak lineBreak = null, bool hasCollapsed = false) { _textRuns = textRuns; diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index b0384a1fdf..61075171fe 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -123,10 +123,7 @@ namespace Avalonia.Skia return; } - if (offsetBuffer == null) - { - offsetBuffer = new Vector[glyphPositions.Length]; - } + offsetBuffer ??= new Vector[glyphPositions.Length]; var offsetX = position.XOffset * textScale; @@ -138,10 +135,7 @@ namespace Avalonia.Skia private static void SetAdvance(ReadOnlySpan glyphPositions, int index, double textScale, ref double[] advanceBuffer) { - if (advanceBuffer == null) - { - advanceBuffer = new double[glyphPositions.Length]; - } + advanceBuffer ??= new double[glyphPositions.Length]; // Depends on direction of layout // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; diff --git a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs index 3ab5950974..4aa7e24aa7 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs @@ -208,6 +208,20 @@ namespace Avalonia.Controls.UnitTests.Utils target.SetItems(new[] { "foo", "bar", "baz" })); } + [Fact] + public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source() + { + var model = new SelectionModel(); + var target = new SelectedItemsSync(model); + var items = new AvaloniaList { "foo", "bar", "baz" }; + var selectedItems = new AvaloniaList { "bar" }; + + target.SetItems(selectedItems); + model.Source = items; + + Assert.Equal(new IndexPath(1), model.SelectedIndex); + } + private static SelectedItemsSync CreateTarget( IEnumerable items = null) {