Search code examples
c#wpfxamlflowdocumentflowdocumentscrollviewer

How to detect Sections that are visible FlowDocumentScrollViewer view?


I have multiple section in FlowDocument as follows. Since there are multiple blocks inside FlowDocument and FlowDocument is inside FlowDocumentScrollViewer the content can be scrolled. Here, I am trying to detect the block that is currently visible in view. For example in following image <Section Name="sec2"> ... </Section> is visible.

enter image description here

MainWindow.xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <FlowDocumentScrollViewer IsToolBarVisible="False" MinZoom="50" Zoom="100"   BorderThickness="1" BorderBrush="Gainsboro" Name="flowDoc" Loaded="FlowDoc_Loaded">
            <FlowDocument Name="flowdoc1" ColumnWidth="999999" PageHeight="840">
                <Section Name="sec1">
                    <Paragraph>
                        <Run>
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec maximus libero. Proin at suscipit tellus. Maecenas interdum lacinia turpis, nec dictum nisi blandit ac. Mauris quis mauris sodales, aliquet tortor nec, dignissim turpis. Sed nec purus vitae tortor posuere tempus id at justo. Integer maximus, eros sit amet sollicitudin cursus, urna lacus posuere justo, ut tempus est nisi nec elit. Duis luctus, justo sed feugiat dapibus, arcu odio hendrerit est, tempus tempus ante elit ut augue. Quisque aliquet blandit libero sed congue. Quisque imperdiet tempor enim, at pulvinar nulla pellentesque in. Maecenas sit amet massa ultrices, fringilla leo id, scelerisque turpis. Cras non pharetra metus. Cras blandit hendrerit nulla at pellentesque. Nulla tristique eget nibh sed ornare. Praesent augue purus, tincidunt a tempus ut, iaculis vel mi.
                        </Run>
                        <Run>
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent molestie volutpat ante, ac dapibus risus varius vel. In tempor nulla non tristique scelerisque. Nam sollicitudin est tellus, sit amet tempus quam faucibus vel. Proin accumsan sed est ut facilisis. Sed non elit semper, euismod augue non, vulputate lorem. Duis malesuada fringilla mauris, ut tempus enim volutpat in. Duis dignissim ullamcorper est in auctor. Nam at tempor metus, in blandit ligula. Mauris sed urna eleifend, volutpat nulla at, ornare libero. Pellentesque dictum ac nibh at fringilla. Pellentesque eu mi leo. Proin aliquet ante at nisi consequat pharetra.
                        </Run>
                        <Run>
                            Etiam accumsan arcu justo, quis dignissim elit volutpat id. Maecenas nec eros id massa pellentesque euismod nec et libero. Aliquam in porttitor lectus. Mauris nec magna sit amet mauris dignissim blandit eget eget velit. Vestibulum dapibus tempor erat, in dapibus eros vestibulum venenatis. Aenean massa mi, efficitur quis fringilla in, elementum vel metus. Sed pulvinar ex lacinia lobortis pharetra. In vel nisi aliquet, porttitor purus vitae, tempus orci. Morbi et ipsum rhoncus, aliquam tortor et, iaculis nibh.
                        </Run>
                        <Run>
                            Curabitur hendrerit nisl ut erat viverra pharetra. Fusce rhoncus vitae nisl ac venenatis. Etiam eget magna vitae dolor placerat vestibulum. Maecenas et tristique orci, et molestie risus. In consectetur odio mi, at sollicitudin mauris aliquam in. Aenean a est vehicula lectus semper tempus id sed diam. Suspendisse potenti. Donec accumsan tortor ac lobortis hendrerit. Phasellus vel pellentesque tortor. Curabitur consectetur luctus nibh, non interdum orci placerat in. Pellentesque tempus est tellus, sit amet tempor eros rhoncus vitae. Nunc lobortis turpis sed condimentum mattis. Aliquam erat volutpat.
                        </Run>
                        <Run>
                            Proin ut pellentesque felis. Curabitur faucibus sed lectus id ornare. Suspendisse tempor pellentesque quam sit amet gravida. Sed venenatis lorem sit amet metus luctus efficitur. Ut enim nisl, luctus quis mollis vitae, aliquam in est. Donec et lobortis sapien. Aliquam suscipit augue a nunc tincidunt, vel sodales velit dapibus. Nulla lacinia mi sit amet libero scelerisque, at hendrerit eros vehicula. Nam lectus metus, faucibus non quam eu, dignissim ultrices metus. Cras urna libero, molestie euismod tortor id, ultrices posuere ligula. Nulla porta maximus nulla. Nunc vitae finibus diam, eu consequat tortor.
                        </Run>
                    </Paragraph>
                </Section>
                <Section Name="sec2">
                    <Paragraph>
                        <Run>
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec maximus libero. Proin at suscipit tellus. Maecenas interdum lacinia turpis, nec dictum nisi blandit ac. Mauris quis mauris sodales, aliquet tortor nec, dignissim turpis. Sed nec purus vitae tortor posuere tempus id at justo. Integer maximus, eros sit amet sollicitudin cursus, urna lacus posuere justo, ut tempus est nisi nec elit. Duis luctus, justo sed feugiat dapibus, arcu odio hendrerit est, tempus tempus ante elit ut augue. Quisque aliquet blandit libero sed congue. Quisque imperdiet tempor enim, at pulvinar nulla pellentesque in. Maecenas sit amet massa ultrices, fringilla leo id, scelerisque turpis. Cras non pharetra metus. Cras blandit hendrerit nulla at pellentesque. Nulla tristique eget nibh sed ornare. Praesent augue purus, tincidunt a tempus ut, iaculis vel mi.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent molestie volutpat ante, ac dapibus risus varius vel. In tempor nulla non tristique scelerisque. Nam sollicitudin est tellus, sit amet tempus quam faucibus vel. Proin accumsan sed est ut facilisis. Sed non elit semper, euismod augue non, vulputate lorem. Duis malesuada fringilla mauris, ut tempus enim volutpat in. Duis dignissim ullamcorper est in auctor. Nam at tempor metus, in blandit ligula. Mauris sed urna eleifend, volutpat nulla at, ornare libero. Pellentesque dictum ac nibh at fringilla. Pellentesque eu mi leo. Proin aliquet ante at nisi consequat pharetra.

Etiam accumsan arcu justo, quis dignissim elit volutpat id. Maecenas nec eros id massa pellentesque euismod nec et libero. Aliquam in porttitor lectus. Mauris nec magna sit amet mauris dignissim blandit eget eget velit. Vestibulum dapibus tempor erat, in dapibus eros vestibulum venenatis. Aenean massa mi, efficitur quis fringilla in, elementum vel metus. Sed pulvinar ex lacinia lobortis pharetra. In vel nisi aliquet, porttitor purus vitae, tempus orci. Morbi et ipsum rhoncus, aliquam tortor et, iaculis nibh.

Curabitur hendrerit nisl ut erat viverra pharetra. Fusce rhoncus vitae nisl ac venenatis. Etiam eget magna vitae dolor placerat vestibulum. Maecenas et tristique orci, et molestie risus. In consectetur odio mi, at sollicitudin mauris aliquam in. Aenean a est vehicula lectus semper tempus id sed diam. Suspendisse potenti. Donec accumsan tortor ac lobortis hendrerit. Phasellus vel pellentesque tortor. Curabitur consectetur luctus nibh, non interdum orci placerat in. Pellentesque tempus est tellus, sit amet tempor eros rhoncus vitae. Nunc lobortis turpis sed condimentum mattis. Aliquam erat volutpat.

Proin ut pellentesque felis. Curabitur faucibus sed lectus id ornare. Suspendisse tempor pellentesque quam sit amet gravida. Sed venenatis lorem sit amet metus luctus efficitur. Ut enim nisl, luctus quis mollis vitae, aliquam in est. Donec et lobortis sapien. Aliquam suscipit augue a nunc tincidunt, vel sodales velit dapibus. Nulla lacinia mi sit amet libero scelerisque, at hendrerit eros vehicula. Nam lectus metus, faucibus non quam eu, dignissim ultrices metus. Cras urna libero, molestie euismod tortor id, ultrices posuere ligula. Nulla porta maximus nulla. Nunc vitae finibus diam, eu consequat tortor.
                        </Run>
                    </Paragraph>
                </Section>
                <Section Name="sec3">
                    <Paragraph>
                        <Run>
                            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec maximus libero. Proin at suscipit tellus. Maecenas interdum lacinia turpis, nec dictum nisi blandit ac. Mauris quis mauris sodales, aliquet tortor nec, dignissim turpis. Sed nec purus vitae tortor posuere tempus id at justo. Integer maximus, eros sit amet sollicitudin cursus, urna lacus posuere justo, ut tempus est nisi nec elit. Duis luctus, justo sed feugiat dapibus, arcu odio hendrerit est, tempus tempus ante elit ut augue. Quisque aliquet blandit libero sed congue. Quisque imperdiet tempor enim, at pulvinar nulla pellentesque in. Maecenas sit amet massa ultrices, fringilla leo id, scelerisque turpis. Cras non pharetra metus. Cras blandit hendrerit nulla at pellentesque. Nulla tristique eget nibh sed ornare. Praesent augue purus, tincidunt a tempus ut, iaculis vel mi.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent molestie volutpat ante, ac dapibus risus varius vel. In tempor nulla non tristique scelerisque. Nam sollicitudin est tellus, sit amet tempus quam faucibus vel. Proin accumsan sed est ut facilisis. Sed non elit semper, euismod augue non, vulputate lorem. Duis malesuada fringilla mauris, ut tempus enim volutpat in. Duis dignissim ullamcorper est in auctor. Nam at tempor metus, in blandit ligula. Mauris sed urna eleifend, volutpat nulla at, ornare libero. Pellentesque dictum ac nibh at fringilla. Pellentesque eu mi leo. Proin aliquet ante at nisi consequat pharetra.

Etiam accumsan arcu justo, quis dignissim elit volutpat id. Maecenas nec eros id massa pellentesque euismod nec et libero. Aliquam in porttitor lectus. Mauris nec magna sit amet mauris dignissim blandit eget eget velit. Vestibulum dapibus tempor erat, in dapibus eros vestibulum venenatis. Aenean massa mi, efficitur quis fringilla in, elementum vel metus. Sed pulvinar ex lacinia lobortis pharetra. In vel nisi aliquet, porttitor purus vitae, tempus orci. Morbi et ipsum rhoncus, aliquam tortor et, iaculis nibh.

Curabitur hendrerit nisl ut erat viverra pharetra. Fusce rhoncus vitae nisl ac venenatis. Etiam eget magna vitae dolor placerat vestibulum. Maecenas et tristique orci, et molestie risus. In consectetur odio mi, at sollicitudin mauris aliquam in. Aenean a est vehicula lectus semper tempus id sed diam. Suspendisse potenti. Donec accumsan tortor ac lobortis hendrerit. Phasellus vel pellentesque tortor. Curabitur consectetur luctus nibh, non interdum orci placerat in. Pellentesque tempus est tellus, sit amet tempor eros rhoncus vitae. Nunc lobortis turpis sed condimentum mattis. Aliquam erat volutpat.

Proin ut pellentesque felis. Curabitur faucibus sed lectus id ornare. Suspendisse tempor pellentesque quam sit amet gravida. Sed venenatis lorem sit amet metus luctus efficitur. Ut enim nisl, luctus quis mollis vitae, aliquam in est. Donec et lobortis sapien. Aliquam suscipit augue a nunc tincidunt, vel sodales velit dapibus. Nulla lacinia mi sit amet libero scelerisque, at hendrerit eros vehicula. Nam lectus metus, faucibus non quam eu, dignissim ultrices metus. Cras urna libero, molestie euismod tortor id, ultrices posuere ligula. Nulla porta maximus nulla. Nunc vitae finibus diam, eu consequat tortor.
                        </Run>
                    </Paragraph>
                </Section>
            </FlowDocument>
        </FlowDocumentScrollViewer>
    </Grid>
</Window>

And on the ScrollChanged of FlowDocumentScrollViewer I am trying to get the sections that are currently visible in view. I tried the following but could not found the actual way to achieve this.

MainWindow.xaml.cs

using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void FlowDoc_Loaded(object sender, RoutedEventArgs e)
        {
            ScrollViewer sv = flowDoc.Template.FindName("PART_ContentHost", flowDoc) as ScrollViewer;
            sv.ScrollChanged += Sv_ScrollChanged;
        }

        private void Sv_ScrollChanged(object sender, ScrollChangedEventArgs e) 
        {
            var blocks1 = flowdoc1.Blocks.ToList();
            
            foreach(var block in blocks1) 
            {
                
                //block.
                var cstartPoint = block.ContentStart;
                var cendPoint = block.ContentEnd;
                
                var estartPoint = block.ElementStart;
                var eendPoint = block.ElementEnd;
                
                bool isFoucsed = block.IsFocused;
                
                //sysWindow.Rect test1 = new sysWindow.Rect(block1);
            }
        }
    }
}

enter image description here

Is there any solutions for this?


Solution

  • You would simply have to check if the bounds obtained from TextPointer.GetCharacterRect are within the bounds of the view port of the ScrollViewer. You would track the TextElement.ContentStart and TextElement.ContentEnd text pointers to determine whether the TextElement is visible to the user or hidden.

    The following example recursively traverses the document's TextElement tree and raises a TextElementScrolledIntoViewChanged event that notifies when any TextElement has entered or left the view port.

    Usage example

    private void FlowDoc_Loaded(object sender, RoutedEventArgs e)
    {
      var documentHost = (Control)sender;
      var textElementObserver = new TextElementObserver(documentHost)
      {
        IsNotifyOnScrollIntoViewEnabled = true,
        IsObserveTextElementsRecursiveEnabled = true
      };
    
      textElementObserver.TextElementScrolledIntoViewChanged += OnTextElementScrolledIntoViewChanged;
    
      textElementObserver.StartObserving();
    }
    
    private void OnTextElementScrolledIntoViewChanged(object? sender, TextElementScrolledIntoViewChangedEventArgs e)
    {
      var textElementObserver = (TextElementObserver)sender;
    
      // Get the new e.g. Section that has become visible
      if (e.TextElement is Section section 
        && e.ScrollState is ScrollState.EnterView)
      {
        string newVisibleSectionName = section.Name;
      }
    
      // Get the old e.g. Section that has become invisible
      if (e.TextElement is Section section 
        && e.ScrollState is ScrollState.LeaveView)
      {
        string oldVisibleSectionName = section.Name;
      }
    
      // Get all currently visible TextElements
      ReadOnlyObservableCollection<TextElement> currentlyVisibleTextElements 
        = textElementObserver.CurrentlyVisibleElements;
    
      // Filter visible elements for all visible Section elements
      IEnumerable<Section> currentlyVisibleSectionElements 
        = currentlyVisibleTextElements.OfType<Section>();  
    
      foreach (Section visibleSection in currentlyVisibleSectionElements)
      {
      }
    }
    

    Implementation

    TextElementObserver.cs

    public class TextElementObserver
    {
      // Helper class to track the index of an item (for faster removal by index)
      internal class Index
      {
        public Index(int value) => this.Value = value;
    
        public void Increment()
        {
          ++this.Value;
          this.Next?.Increment();
        }
    
        public void Decrement()
        {
          --this.Value;
          this.Next?.Decrement();
        }
    
        // Does not insert a link! 
        // If there is an existing next link then this link gets dropped.
        public void AddLink(Index last)
        {
          this.Next = last;
          last.Previous = this;
        }
    
        public void RemoveLink()
        {
          if (!this.IsLast)
          {
            this.Next!.Previous = this.Previous;
            this.Next.Decrement();
          }
    
          if (!this.IsFirst)
          {
            this.Previous!.Next = this.Next;
          }
        }
    
        public int Value { get; private set; }
        public Index? Next { get; private set; }
        public Index? Previous { get; private set; }
        public bool IsFirst => this.Previous is null;
        public bool IsLast => this.Next is null;
      }
    
      // Helper to store TextElement meta info
      internal class TextElementInfo
      {
        public TextElementInfo(TextElement textElement, int index)
        {
          this.TextElement = textElement;
          this.Index = new Index(index);
        }
    
        public TextElement TextElement { get; }
        public Index Index { get; }
      }
    
      public event EventHandler<TextElementScrolledIntoViewChangedEventArgs> TextElementScrolledIntoViewChanged;
      public bool IsNotifyOnScrollIntoViewEnabled { get; set; }
      public bool IsObserveTextElementsRecursiveEnabled { get; set; }
      public ReadOnlyObservableCollection<TextElement> CurrentlyVisibleElements { get; }
      private ObservableCollection<TextElement> InternalCurrentlyVisibleElements { get; }
      private Control DocumentHost { get; }
      private FlowDocument Document { get; }
      private Dictionary<TextElement, TextElementInfo> VisibleTextElementsMap { get; }
    
      public TextElementObserver(Control flowDocumentHost)
      {
        ArgumentNullException.ThrowIfNull(flowDocumentHost);
        this.DocumentHost = flowDocumentHost;
    
        this.Document = this.DocumentHost switch
        {
          DocumentViewerBase documentViewerBase => documentViewerBase.Document is FlowDocument flowDocument ? flowDocument : throw new NotSupportedException(),
          FlowDocumentReader flowDocumentReader => flowDocumentReader.Document,
          FlowDocumentScrollViewer flowDocumentScrollViewer => flowDocumentScrollViewer.Document,
          RichTextBox richTextBox => richTextBox.Document,
          _ => throw new NotSupportedException()
        };
    
        ArgumentNullException.ThrowIfNull(this.Document);
    
        this.VisibleTextElementsMap = new Dictionary<TextElement, TextElementInfo>();
        this.InternalCurrentlyVisibleElements = new ObservableCollection<TextElement>();
        this.CurrentlyVisibleElements = new ReadOnlyObservableCollection<TextElement>(this.InternalCurrentlyVisibleElements);
      }
    
      public void StartObserving() 
        => this.DocumentHost.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(OnDocumentScrolled));
    
      public void StopObserving()
        => this.DocumentHost.RemoveHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(OnDocumentScrolled));
    
      private void OnDocumentScrolled(object sender, ScrollChangedEventArgs e)
      {
        if (!this.IsNotifyOnScrollIntoViewEnabled)
        {
          return;
        }
    
        var scrollViewer = (ScrollViewer)e.OriginalSource;
        var viewportSize = new Size(scrollViewer.ViewportWidth, scrollViewer.ViewportHeight);
        var viewportLocation = new Point(0, 0);
        var viewPortBounds = new Rect(viewportLocation, viewportSize);
        LocateTextElements(this.Document.Blocks, viewPortBounds);
      }
    
      // Recursively traverses the documents TextElement tree
      private void LocateTextElements(IEnumerable<TextElement> textElements, Rect viewPortBounds)
      {
        bool hasVisibleTextElements = false;
        var dirtyTextElements = new HashSet<TextElement>(this.VisibleTextElementsMap.Keys);
        foreach (TextElement textElement in textElements)
        {
          Rect textElementStartBounds = textElement.ContentStart.GetCharacterRect(LogicalDirection.Forward);
          Rect textElementEndBounds = textElement.ContentEnd.GetCharacterRect(LogicalDirection.Backward);
          bool isStartOfElementVisible = viewPortBounds.Contains(textElementStartBounds.Location);
          bool isEndOfElementVisible = viewPortBounds.Contains(textElementEndBounds.Location);
          bool isTextElementStartOrEndVisible = isStartOfElementVisible || isEndOfElementVisible;
          bool isTextElementEnclosingViewport = textElementStartBounds.Top < viewPortBounds.Top && textElementEndBounds.Bottom > viewPortBounds.Bottom;
    
          // An element is visible if.. 
          bool isVisble = isTextElementStartOrEndVisible // ...its start or/and end is within the view port...
            || isTextElementEnclosingViewport; // ...or if the content but not start and end is visible.
    
          if (isVisble)
          {
            hasVisibleTextElements = true;
            if (!this.VisibleTextElementsMap.ContainsKey(textElement))
            {
              RegisterVisibleTextElement(textElement);
              OnTextElementScrolledIntoViewChanged(textElement, ScrollState.EnterView);
            }
    
            // Try to remove handled TextElement
            _ = dirtyTextElements.Remove(textElement);
    
            if (!this.IsObserveTextElementsRecursiveEnabled)
            {
              continue;
            }
    
            LocateChildTextElements(textElement, viewPortBounds);
          }
          else // Element not visible...
          {
            //  ...or not visible anymore
            if (this.VisibleTextElementsMap.TryGetValue(textElement, out TextElementInfo? textElementInfo))
            {
              UnregisterVisibleTextElement(textElementInfo);
              OnTextElementScrolledIntoViewChanged(textElement, ScrollState.LeaveView);
              LocateChildTextElements(textElement, viewPortBounds);
    
              continue;
            }
    
            /* Element was not visible before, therefore its children weren't too */
    
            bool isCurrentPositionAfterViewport = hasVisibleTextElements;
            if (isCurrentPositionAfterViewport)
            {
              // Skip document and complete remaining dirty TextElemnts
              LocateTextElements(dirtyTextElements, viewPortBounds);
    
              return;
            }
          }
        }
      }
    
      private void LocateChildTextElements(TextElement textElement, Rect viewPortBounds)
      {
        // Handle nested child elements 
        IEnumerable<TextElement> childTextElements = textElement switch
        {
          Section section => section.Blocks,
          Paragraph paragraph => paragraph.Inlines,
          Table table => table.RowGroups,
          List list => list.ListItems,
          ListItem listItem => listItem.Blocks,
          TableCell tableCell => tableCell.Blocks,
          TableRow tableRow => tableRow.Cells,
          TableRowGroup tableRowGroup => tableRowGroup.Rows,
          AnchoredBlock anchoredBlock => anchoredBlock.Blocks,
          Span span => span.Inlines,
          _ => Enumerable.Empty<TextElement>()
        };
    
        if (childTextElements.Any())
        {
          LocateTextElements(childTextElements, viewPortBounds);
        }
      }
    
      private void RegisterVisibleTextElement(TextElement textElement)
      {
        TextElement? previousElement = this.InternalCurrentlyVisibleElements.LastOrDefault();
    
        int index = this.InternalCurrentlyVisibleElements.Count;
        this.InternalCurrentlyVisibleElements.Add(textElement);
        var textElementInfo = new TextElementInfo(textElement, index);
        this.VisibleTextElementsMap.Add(textElement, textElementInfo);
    
        if (previousElement is not null
          && this.VisibleTextElementsMap.TryGetValue(previousElement, out TextElementInfo? previousTextElementInfo))
        {
          previousTextElementInfo.Index.AddLink(textElementInfo.Index);
        }
      }
    
      private void UnregisterVisibleTextElement(TextElementInfo textElementInfo)
      {
        this.InternalCurrentlyVisibleElements.RemoveAt(textElementInfo.Index.Value);
        _ = this.VisibleTextElementsMap.Remove(textElementInfo.TextElement);
        textElementInfo.Index.RemoveLink();
      }
    
      protected virtual void OnTextElementScrolledIntoViewChanged(TextElement textElement, ScrollState scrollState)
        => this.TextElementScrolledIntoViewChanged?.Invoke(this, new TextElementScrolledIntoViewChangedEventArgs(textElement, scrollState));
    }
    

    TextElementScrolledIntoViewChangedEventArgs.cs

    public class TextElementScrolledIntoViewChangedEventArgs : EventArgs
    {
      public TextElementScrolledIntoViewChangedEventArgs(TextElement textElement, ScrollState scrollState)
      {
        this.TextElement = textElement;
        this.ScrollState = scrollState;
      }
    
      public TextElement TextElement { get; }
      public ScrollState ScrollState { get; }
    }
    

    ScrollState.cs

    public enum ScrollState
    {
      Undefined = 0,
      EnterView,
      LeaveView
    }