using System.Windows.Controls; // ReSharper disable once CheckNamespace namespace WPFluent.Controls; /// /// Custom with events depending on actions taken by the user. /// [DefaultEvent("ScrollChangedEvent")] public class DynamicScrollViewer : PassiveScrollViewer { /// /// Identifies the dependency property. /// public static readonly DependencyProperty IsScrollingHorizontallyProperty = DependencyProperty.Register( nameof(IsScrollingHorizontally), typeof(bool), typeof(DynamicScrollViewer), new PropertyMetadata(false, OnIsScrollingHorizontallyChanged)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty IsScrollingVerticallyProperty = DependencyProperty.Register( nameof(IsScrollingVertically), typeof(bool), typeof(DynamicScrollViewer), new PropertyMetadata(false, OnIsScrollingVerticallyChanged)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty MinimalChangeProperty = DependencyProperty.Register( nameof(MinimalChange), typeof(double), typeof(DynamicScrollViewer), new PropertyMetadata(40d, OnMinimalChangeChanged)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty TimeoutProperty = DependencyProperty.Register( nameof(Timeout), typeof(int), typeof(DynamicScrollViewer), new PropertyMetadata(1200, OnTimeoutChanged)); private readonly EventIdentifier _horizontalIdentifier = new(); private double _minimalChange = 40d; private bool _scrollingHorizontally = false; // Due to the large number of triggered events, we limit the complex logic of DependencyProperty private bool _scrollingVertically = false; private int _timeout = 1200; private readonly EventIdentifier _verticalIdentifier = new(); private static void OnIsScrollingHorizontallyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if(d is not DynamicScrollViewer scroll) { return; } scroll._scrollingHorizontally = scroll.IsScrollingHorizontally; } private static void OnIsScrollingVerticallyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if(d is not DynamicScrollViewer scroll) { return; } scroll._scrollingVertically = scroll.IsScrollingVertically; } private static void OnMinimalChangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if(d is not DynamicScrollViewer scroll) { return; } scroll._minimalChange = scroll.MinimalChange; } private static void OnTimeoutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if(d is not DynamicScrollViewer scroll) { return; } scroll._timeout = scroll.Timeout; } private async void UpdateHorizontalScrollingState() { // TODO: Optimize // My main assumption here is that each scroll causes a new "event / thread" to be assigned. // If more than Timeout has passed since the last event, there is no interaction. // We pass this value to the ScrollBar and link it to IsMouseOver. // This way we have a dynamic scrollbar that responds to scroll / mouse over. long currentEvent = _horizontalIdentifier.GetNext(); if(!_scrollingHorizontally) { SetCurrentValue(IsScrollingHorizontallyProperty, true); } await Task.Delay(Timeout < 10000 ? Timeout : 1000); if(_horizontalIdentifier.IsEqual(currentEvent) && _scrollingHorizontally) { SetCurrentValue(IsScrollingHorizontallyProperty, false); } } private async void UpdateVerticalScrollingState() { // TODO: Optimize // My main assumption here is that each scroll causes a new "event / thread" to be assigned. // If more than Timeout has passed since the last event, there is no interaction. // We pass this value to the ScrollBar and link it to IsMouseOver. // This way we have a dynamic scrollbar that responds to scroll / mouse over. long currentEvent = _verticalIdentifier.GetNext(); if(!_scrollingVertically) { SetCurrentValue(IsScrollingVerticallyProperty, true); } if(_timeout > -1) { await Task.Delay(_timeout < 10000 ? _timeout : 1000); } if(_verticalIdentifier.IsEqual(currentEvent) && _scrollingVertically) { SetCurrentValue(IsScrollingVerticallyProperty, false); } } /// /// OnScrollChanged is an override called whenever scrolling state changes on this . /// /// /// OnScrollChanged fires the ScrollChangedEvent. Overriders of this method should call base.OnScrollChanged(args) /// if they want the event to be fired. /// /// ScrollChangedEventArgs containing information about the change in scrolling state. protected override void OnScrollChanged(ScrollChangedEventArgs e) { base.OnScrollChanged(e); if(e.HorizontalChange > _minimalChange || e.HorizontalChange < -_minimalChange) { UpdateHorizontalScrollingState(); } if(e.VerticalChange > _minimalChange || e.VerticalChange < -_minimalChange) { UpdateVerticalScrollingState(); } } /// /// Gets or sets a value indicating whether the user was scrolling horizontally for the last few seconds. /// public bool IsScrollingHorizontally { get => (bool)GetValue(IsScrollingHorizontallyProperty); set => SetValue(IsScrollingHorizontallyProperty, value); } /// /// Gets or sets a value indicating whether the user was scrolling vertically for the last few seconds. /// public bool IsScrollingVertically { get => (bool)GetValue(IsScrollingVerticallyProperty); set => SetValue(IsScrollingVerticallyProperty, value); } /// /// Gets or sets the value required for the scroll to show automatically. /// public double MinimalChange { get => (double)GetValue(MinimalChangeProperty); set => SetValue(MinimalChangeProperty, value); } /// /// Gets or sets time after which the scroll is to be hidden. /// public int Timeout { get => (int)GetValue(TimeoutProperty); set => SetValue(TimeoutProperty, value); } }