using System; using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using DynamicData; using DynamicData.Binding; using JetBrains.Annotations; namespace StabilityMatrix.Avalonia.Collections; [PublicAPI] public class SearchCollection : AbstractNotifyPropertyChanged, IDisposable where TObject : notnull where TKey : notnull { private readonly IDisposable cleanUp; private Func>? PredicateSelector { get; } private Func>? ScorerSelector { get; } private Func? Scorer { get; set; } private TQuery? _query; public TQuery? Query { get => _query; set => SetAndRaise(ref _query, value); } private SortExpressionComparer _sortComparer = []; public SortExpressionComparer SortComparer { get => _sortComparer; set => SetAndRaise(ref _sortComparer, value); } /// /// Converts to . /// private SortExpressionComparer SearchItemSortComparer => [ ..SortComparer .Select(sortExpression => new SortExpression( item => sortExpression.Expression(item.Item), sortExpression.Direction )).Prepend(new SortExpression(item => item.Score, SortDirection.Descending)) ]; public IObservableCollection Items { get; } = new ObservableCollectionExtended(); public IObservableCollection FilteredItems { get; } = new ObservableCollectionExtended(); public SearchCollection( IObservable> source, Func> predicateSelector, SortExpressionComparer? sortComparer = null ) { PredicateSelector = predicateSelector; if (sortComparer is not null) { SortComparer = sortComparer; } // Observable which creates a new predicate whenever Query property changes var dynamicPredicate = this.WhenValueChanged(@this => @this.Query).Select(predicateSelector); cleanUp = source .Bind(Items) .Filter(dynamicPredicate) .Sort(SortComparer) .Bind(FilteredItems) .Subscribe(); } public SearchCollection( IObservable> source, Func> scorerSelector, SortExpressionComparer? sortComparer = null ) { ScorerSelector = scorerSelector; if (sortComparer is not null) { SortComparer = sortComparer; } // Monitor Query property for changes var queryChanged = this.WhenValueChanged(@this => @this.Query).Select(_ => Unit.Default); cleanUp = new CompositeDisposable( // Update Scorer property whenever Query property changes queryChanged.Subscribe(_ => Scorer = scorerSelector(Query)), // Transform source items into SearchItems source .Transform( obj => { var (isMatch, score) = Scorer?.Invoke(obj) ?? (true, 0); return new SearchItem { Item = obj, IsMatch = isMatch, Score = score }; }, forceTransform: queryChanged ) .Filter(item => item.IsMatch) .Sort(SearchItemSortComparer, SortOptimisations.ComparesImmutableValuesOnly) .Transform(searchItem => searchItem.Item) .Bind(FilteredItems) .Subscribe() ); } /// /// Clears property by setting it to default value. /// public void ClearQuery() { Query = default; } public void Dispose() { cleanUp.Dispose(); GC.SuppressFinalize(this); } private readonly record struct SearchItem { public TObject Item { get; init; } public int Score { get; init; } public bool IsMatch { get; init; } } }