csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
639 lines
24 KiB
639 lines
24 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Controls.Primitives.PopupPositioning;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Media.TextFormatting;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Controls.Primitives
|
|
{
|
|
internal class TextSelectionHandleCanvas : Canvas
|
|
{
|
|
private static readonly bool s_shouldWrapAroundSelection;
|
|
|
|
private const int ContextMenuPadding = 16;
|
|
private static bool s_isInTouchMode;
|
|
|
|
private readonly TextSelectionHandle _caretHandle;
|
|
private readonly TextSelectionHandle _handle1;
|
|
private readonly TextSelectionHandle _handle2;
|
|
private TextPresenter? _presenter;
|
|
private TextBox? _textBox;
|
|
private bool _showHandle;
|
|
private IDisposable? _showDisposable;
|
|
private PresenterVisualListener _layoutListener;
|
|
private int? _savedSelectionStart;
|
|
private int? _saveSelectionEnd;
|
|
private SelectionHandleType? _initialHandleType;
|
|
|
|
internal bool ShowHandles
|
|
{
|
|
get => _showHandle;
|
|
set
|
|
{
|
|
_showHandle = value;
|
|
|
|
if (!value)
|
|
{
|
|
_handle1.IsVisible = false;
|
|
_handle2.IsVisible = false;
|
|
_caretHandle.IsVisible = false;
|
|
}
|
|
|
|
IsVisible = !string.IsNullOrEmpty(_presenter?.Text) && value;
|
|
}
|
|
}
|
|
|
|
static TextSelectionHandleCanvas()
|
|
{
|
|
s_shouldWrapAroundSelection = !OperatingSystem.IsAndroid();
|
|
}
|
|
|
|
public TextSelectionHandleCanvas()
|
|
{
|
|
_caretHandle = new TextSelectionHandle() { SelectionHandleType = SelectionHandleType.Caret };
|
|
_handle1 = new TextSelectionHandle();
|
|
_handle2 = new TextSelectionHandle();
|
|
|
|
_caretHandle.SelectionHandleType = SelectionHandleType.Caret;
|
|
_handle1.SelectionHandleType = SelectionHandleType.Start;
|
|
_handle2.SelectionHandleType = SelectionHandleType.End;
|
|
|
|
Children.Add(_caretHandle);
|
|
Children.Add(_handle1);
|
|
Children.Add(_handle2);
|
|
|
|
_caretHandle.DragStarted += Handle_DragStarted;
|
|
_caretHandle.DragDelta += CaretHandle_DragDelta;
|
|
_caretHandle.DragCompleted += Handle_DragCompleted;
|
|
_handle1.DragDelta += SelectionHandle_DragDelta;
|
|
_handle1.DragCompleted += Handle_DragCompleted;
|
|
_handle1.DragStarted += Handle_DragStarted;
|
|
_handle2.DragDelta += SelectionHandle_DragDelta;
|
|
_handle2.DragCompleted += Handle_DragCompleted;
|
|
_handle2.DragStarted += Handle_DragStarted;
|
|
|
|
_handle1.SetTopLeft(default);
|
|
_caretHandle.SetTopLeft(default);
|
|
_handle2.SetTopLeft(default);
|
|
|
|
_handle1.ContextCanceled += Caret_ContextCanceled;
|
|
_caretHandle.ContextCanceled += Caret_ContextCanceled;
|
|
_handle2.ContextCanceled += Caret_ContextCanceled;
|
|
_handle1.ContextRequested += Caret_ContextRequested;
|
|
_caretHandle.ContextRequested += Caret_ContextRequested;
|
|
_handle2.ContextRequested += Caret_ContextRequested;
|
|
|
|
IsVisible = ShowHandles;
|
|
|
|
ClipToBounds = false;
|
|
|
|
_layoutListener = new PresenterVisualListener();
|
|
_layoutListener.Invalidated += LayoutListener_Invalidated;
|
|
}
|
|
|
|
private void LayoutListener_Invalidated(object? sender, EventArgs e)
|
|
{
|
|
if (ShowHandles)
|
|
MoveHandlesToSelection();
|
|
}
|
|
|
|
private void Caret_ContextCanceled(object? sender, RoutedEventArgs e)
|
|
{
|
|
CloseFlyout();
|
|
}
|
|
|
|
private void Caret_ContextRequested(object? sender, ContextRequestedEventArgs e)
|
|
{
|
|
ShowFlyout();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void Handle_DragStarted(object? sender, VectorEventArgs e)
|
|
{
|
|
_savedSelectionStart = _textBox?.SelectionStart;
|
|
_saveSelectionEnd = _textBox?.SelectionEnd;
|
|
_initialHandleType = (sender as TextSelectionHandle)?.SelectionHandleType;
|
|
CloseFlyout();
|
|
}
|
|
|
|
private void CloseFlyout()
|
|
{
|
|
_textBox?.RaiseEvent(new Interactivity.RoutedEventArgs(InputElement.ContextCanceledEvent));
|
|
}
|
|
|
|
private void SelectionHandle_DragDelta(object? sender, VectorEventArgs e)
|
|
{
|
|
if (sender is TextSelectionHandle handle)
|
|
{
|
|
DragSelectionHandle(handle);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private void CaretHandle_DragDelta(object? sender, VectorEventArgs e)
|
|
{
|
|
if (_presenter != null && _textBox != null)
|
|
{
|
|
var indicatorPosition = GetSearchPoint(_caretHandle);
|
|
var point = ToPresenter(indicatorPosition);
|
|
using var _ = BeginChange();
|
|
_presenter.MoveCaretToPoint(point);
|
|
var caretIndex = _presenter.CaretIndex;
|
|
_textBox.SetCurrentValue(TextBox.CaretIndexProperty, caretIndex);
|
|
_textBox.SetCurrentValue(TextBox.SelectionStartProperty, caretIndex);
|
|
_textBox.SetCurrentValue(TextBox.SelectionEndProperty, caretIndex);
|
|
|
|
var caretBound = _presenter.GetCursorRectangle();
|
|
_caretHandle.SetTopLeft(ToLayer(caretBound.BottomLeft));
|
|
}
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
ShowHandles = false;
|
|
s_isInTouchMode = false;
|
|
}
|
|
|
|
internal void Show(bool forceTouchMode = false)
|
|
{
|
|
s_isInTouchMode = s_isInTouchMode || forceTouchMode;
|
|
if (s_isInTouchMode)
|
|
{
|
|
ShowHandles = true;
|
|
MoveHandlesToSelection();
|
|
|
|
CheckStateAndShowFlyout();
|
|
}
|
|
}
|
|
|
|
private IDisposable? BeginChange()
|
|
{
|
|
return _presenter?.CurrentImClient?.BeginChange();
|
|
}
|
|
|
|
private void Handle_DragCompleted(object? sender, VectorEventArgs e)
|
|
{
|
|
_savedSelectionStart = -1;
|
|
_saveSelectionEnd = -1;
|
|
|
|
if (!_handle1.IsDragging)
|
|
{
|
|
_handle1.NeedsIndicatorUpdate = true;
|
|
_handle1.SelectionHandleType = SelectionHandleType.Start;
|
|
}
|
|
|
|
if (!_handle2.IsDragging)
|
|
{
|
|
_handle2.NeedsIndicatorUpdate = true;
|
|
_handle2.SelectionHandleType = SelectionHandleType.End;
|
|
}
|
|
|
|
MoveHandlesToSelection();
|
|
CheckStateAndShowFlyout();
|
|
}
|
|
|
|
private void CheckStateAndShowFlyout()
|
|
{
|
|
if (_textBox?.SelectionStart != _textBox?.SelectionEnd && !(_handle1.IsDragging || _handle2.IsDragging))
|
|
{
|
|
// Show flyout if there's a selection
|
|
ShowFlyout();
|
|
}
|
|
}
|
|
|
|
private void EnsureVisible()
|
|
{
|
|
_showDisposable?.Dispose();
|
|
_showDisposable = null;
|
|
|
|
if (_presenter is { } presenter && presenter.VisualRoot is InputElement root)
|
|
{
|
|
var bounds = presenter.GetTransformedBounds();
|
|
|
|
if (bounds == null)
|
|
return;
|
|
|
|
var clip = bounds.Value.Clip.Inflate(_textBox?.Padding ?? new Thickness(4, 0));
|
|
|
|
var isSelectionDragging = _handle1.IsDragging || _handle2.IsDragging;
|
|
|
|
var hasSelection = _presenter.SelectionStart != _presenter.SelectionEnd || isSelectionDragging;
|
|
|
|
_handle1.IsVisible = ShowHandles && hasSelection &&
|
|
!IsOccluded(_handle1.IndicatorPosition);
|
|
_handle2.IsVisible = ShowHandles && hasSelection &&
|
|
!IsOccluded(_handle2.IndicatorPosition);
|
|
_caretHandle.IsVisible = ShowHandles && !hasSelection &&
|
|
!IsOccluded(_caretHandle.IndicatorPosition);
|
|
|
|
bool IsOccluded(Point point)
|
|
{
|
|
return !clip.Contains(point);
|
|
}
|
|
|
|
if (ShowHandles && !hasSelection)
|
|
{
|
|
_showDisposable = DispatcherTimer.RunOnce(() =>
|
|
{
|
|
ShowHandles = false;
|
|
_showDisposable?.Dispose();
|
|
}, TimeSpan.FromSeconds(5), DispatcherPriority.Background);
|
|
}
|
|
}
|
|
}
|
|
|
|
private Point GetSearchPoint(TextSelectionHandle handle)
|
|
{
|
|
if (_presenter == null)
|
|
return default;
|
|
|
|
var caretBounds = _presenter.GetCursorRectangle();
|
|
var searchOffset = caretBounds.Height / 2;
|
|
var indicator = handle.IndicatorPosition;
|
|
return indicator.WithY(indicator.Y - searchOffset);
|
|
}
|
|
|
|
private void DragSelectionHandle(TextSelectionHandle handle)
|
|
{
|
|
if (_presenter is { } presenter && _textBox is { } textbox)
|
|
{
|
|
CloseFlyout();
|
|
|
|
var position = GetTextPosition(handle);
|
|
|
|
var otherHandle = handle == _handle1 ? _handle2 : _handle1;
|
|
|
|
using var _ = BeginChange();
|
|
|
|
// Some platforms do not allow handles to cause selection bounds to reverse, i.e. allow handles to move over each other.
|
|
if (!s_shouldWrapAroundSelection)
|
|
{
|
|
if (handle.SelectionHandleType == SelectionHandleType.Start)
|
|
{
|
|
position = position >= textbox.SelectionEnd ? textbox.SelectionEnd - 1 : position;
|
|
textbox.SetCurrentValue(TextBox.SelectionStartProperty, position);
|
|
}
|
|
else
|
|
{
|
|
position = position <= textbox.SelectionStart ? textbox.SelectionStart + 1 : position;
|
|
textbox.SetCurrentValue(TextBox.SelectionEndProperty, position);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// For platforms that do, update the handle types for each and adjust selection.
|
|
|
|
if (_initialHandleType == SelectionHandleType.Start)
|
|
{
|
|
// If handle was previously the selection end, set new selection end to the previous selection start.
|
|
bool hasPositionSwapped = position > _saveSelectionEnd;
|
|
var otherPosition = _saveSelectionEnd;
|
|
|
|
if (position == otherPosition)
|
|
{
|
|
position = hasPositionSwapped ? Math.Min(position + 1, (textbox.Text?.Length - 1) ?? 0)
|
|
: Math.Max(position - 1, 0);
|
|
}
|
|
|
|
textbox.SetCurrentValue(TextBox.SelectionStartProperty, hasPositionSwapped ? otherPosition : position);
|
|
textbox.SetCurrentValue(TextBox.SelectionEndProperty, hasPositionSwapped ? position : otherPosition);
|
|
|
|
otherHandle.SelectionHandleType = hasPositionSwapped ? SelectionHandleType.Start : SelectionHandleType.End;
|
|
}
|
|
else
|
|
{
|
|
// If handle was previously the selection start, set new selection start to the previous selection end.
|
|
bool hasPositionSwapped = position < _savedSelectionStart;
|
|
var otherPosition = _savedSelectionStart;
|
|
|
|
if (position == otherPosition)
|
|
{
|
|
position = !hasPositionSwapped ? Math.Min(position + 1, (textbox.Text?.Length - 1) ?? 0)
|
|
: Math.Max(position - 1, 0);
|
|
}
|
|
|
|
textbox.SetCurrentValue(TextBox.SelectionStartProperty, hasPositionSwapped ? position : otherPosition);
|
|
textbox.SetCurrentValue(TextBox.SelectionEndProperty, hasPositionSwapped ? otherPosition : position);
|
|
|
|
otherHandle.SelectionHandleType = hasPositionSwapped ? SelectionHandleType.End : SelectionHandleType.Start;
|
|
}
|
|
}
|
|
|
|
presenter.MoveCaretToTextPosition(position);
|
|
var caretBound = presenter.GetCursorRectangle();
|
|
handle.SetTopLeft(ToLayer(handle.SelectionHandleType == SelectionHandleType.Start ? caretBound.BottomLeft : caretBound.BottomLeft));
|
|
|
|
MoveHandlesToSelection();
|
|
}
|
|
|
|
int GetTextPosition(TextSelectionHandle handle)
|
|
{
|
|
var indicatorPosition = GetSearchPoint(handle);
|
|
var point = ToPresenter(indicatorPosition);
|
|
var hit = presenter.TextLayout.HitTestPoint(point);
|
|
var position = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;
|
|
return position;
|
|
}
|
|
}
|
|
|
|
private Point ToLayer(Point point)
|
|
{
|
|
return (_presenter?.VisualRoot is Visual v) ? _presenter?.TranslatePoint(point, v) ?? point : point;
|
|
}
|
|
|
|
private Point ToPresenter(Point point)
|
|
{
|
|
return (_presenter is { } presenter) ? (presenter.VisualRoot as Visual)?.TranslatePoint(point, presenter) ?? point : point;
|
|
}
|
|
|
|
private Point ToTextBox(Point point)
|
|
{
|
|
return (_textBox is { } textbox) ? (textbox.VisualRoot as Visual)?.TranslatePoint(point, textbox) ?? point : point;
|
|
}
|
|
|
|
public void MoveHandlesToSelection()
|
|
{
|
|
if (_presenter == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var selectionStart = _presenter.SelectionStart;
|
|
var selectionEnd = _presenter.SelectionEnd;
|
|
var hasSelection = selectionStart != selectionEnd;
|
|
|
|
if (!_caretHandle.IsDragging)
|
|
{
|
|
var points = _presenter.GetCaretPoints();
|
|
|
|
_caretHandle.SetTopLeft(ToLayer(points.Item2));
|
|
}
|
|
|
|
if (hasSelection)
|
|
{
|
|
var start = Math.Min(selectionStart, selectionEnd);
|
|
var end = Math.Max(selectionStart, selectionEnd);
|
|
|
|
var startPoint = GetPosition(start, true);
|
|
var endPoint = GetPosition(end - 1, false);
|
|
|
|
if (!_handle1.IsDragging)
|
|
{
|
|
var pos = _handle1.SelectionHandleType == SelectionHandleType.Start ? startPoint : endPoint;
|
|
if (pos.isRtl != _handle1.IsRtl)
|
|
_handle1.NeedsIndicatorUpdate = true;
|
|
_handle1.IsRtl = pos.isRtl;
|
|
_handle1.SetTopLeft(ToLayer(pos.position));
|
|
}
|
|
|
|
if (!_handle2.IsDragging)
|
|
{
|
|
var pos = _handle2.SelectionHandleType == SelectionHandleType.Start ? startPoint : endPoint;
|
|
if (pos.isRtl != _handle2.IsRtl)
|
|
_handle2.NeedsIndicatorUpdate = true;
|
|
_handle2.IsRtl = pos.isRtl;
|
|
_handle2.SetTopLeft(ToLayer(pos.position));
|
|
}
|
|
|
|
(Point position, bool isRtl) GetPosition(int index, bool start)
|
|
{
|
|
var rect = new List<Rect>(_presenter!.TextLayout.HitTestTextRange(index, 1)).FirstOrDefault();
|
|
|
|
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(index, false);
|
|
var textLine = _presenter.TextLayout.TextLines[lineIndex];
|
|
var lineStart = textLine.FirstTextSourceIndex;
|
|
var characterLineIndex = Math.Max(0, index - lineStart);
|
|
TextRun? run = null;
|
|
int searchLength = 0;
|
|
|
|
for (var i = 0; i < textLine.TextRuns.Count; i++)
|
|
{
|
|
run = textLine.TextRuns[i];
|
|
|
|
searchLength += run.Length;
|
|
if (searchLength > characterLineIndex)
|
|
break;
|
|
}
|
|
|
|
bool isRtl;
|
|
var reversed = isRtl = !((run as ShapedTextRun)?.ShapedBuffer.IsLeftToRight ?? true);
|
|
if (!start)
|
|
reversed = !reversed;
|
|
|
|
return (reversed ? rect.BottomRight : rect.BottomLeft, isRtl);
|
|
}
|
|
}
|
|
|
|
EnsureVisible();
|
|
}
|
|
|
|
internal void SetPresenter(TextPresenter? textPresenter)
|
|
{
|
|
if (_presenter == textPresenter)
|
|
return;
|
|
|
|
if (_presenter != null)
|
|
{
|
|
_layoutListener.Detach();
|
|
_presenter.RemoveHandler(KeyDownEvent, PresenterKeyDown);
|
|
_presenter.RemoveHandler(TappedEvent, PresenterTapped);
|
|
_presenter.RemoveHandler(PointerPressedEvent, PresenterPressed);
|
|
_presenter.RemoveHandler(GotFocusEvent, PresenterFocused);
|
|
|
|
if (_textBox != null)
|
|
{
|
|
_textBox.PropertyChanged -= TextBox_PropertyChanged;
|
|
_textBox.RemoveHandler(ScrollGestureEvent, TextBoxScrolling);
|
|
}
|
|
_textBox = null;
|
|
|
|
_presenter = null;
|
|
}
|
|
|
|
_presenter = textPresenter;
|
|
if (_presenter != null)
|
|
{
|
|
_layoutListener.Attach(_presenter);
|
|
_presenter.AddHandler(KeyDownEvent, PresenterKeyDown, handledEventsToo: true);
|
|
_presenter.AddHandler(TappedEvent, PresenterTapped);
|
|
_presenter.AddHandler(PointerPressedEvent, PresenterPressed, handledEventsToo: true);
|
|
_presenter.AddHandler(GotFocusEvent, PresenterFocused, handledEventsToo: true);
|
|
|
|
_textBox = _presenter.FindAncestorOfType<TextBox>();
|
|
|
|
if (_textBox != null)
|
|
{
|
|
_textBox.PropertyChanged += TextBox_PropertyChanged;
|
|
_textBox.AddHandler(ScrollGestureEvent, TextBoxScrolling, handledEventsToo: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TextBoxScrolling(object? sender, ScrollGestureEventArgs e)
|
|
{
|
|
CloseFlyout();
|
|
}
|
|
|
|
private void PresenterPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
s_isInTouchMode = e.Pointer.Type != PointerType.Mouse;
|
|
}
|
|
|
|
private void PresenterFocused(object? sender, FocusChangedEventArgs e)
|
|
{
|
|
if (_presenter != null && _presenter.SelectionStart != _presenter.SelectionEnd)
|
|
{
|
|
ShowHandles = true;
|
|
EnsureVisible();
|
|
}
|
|
}
|
|
|
|
private void PresenterTapped(object? sender, TappedEventArgs e)
|
|
{
|
|
s_isInTouchMode = e.Pointer.Type != PointerType.Mouse;
|
|
|
|
if (s_isInTouchMode)
|
|
MoveHandlesToSelection();
|
|
else
|
|
{
|
|
ShowHandles = false;
|
|
_showDisposable?.Dispose();
|
|
_showDisposable = null;
|
|
}
|
|
}
|
|
|
|
private void Presenter_SizeChanged(object? sender, SizeChangedEventArgs e)
|
|
{
|
|
InvalidateMeasure();
|
|
}
|
|
|
|
internal bool ShowFlyout()
|
|
{
|
|
if (_textBox != null)
|
|
{
|
|
if (_textBox.ContextFlyout is PopupFlyoutBase flyout)
|
|
{
|
|
var verticalOffset = (double.IsNaN(_textBox.LineHeight) ? _textBox.FontSize : _textBox.LineHeight) +
|
|
ContextMenuPadding;
|
|
|
|
Point? topleft = default;
|
|
|
|
if (_textBox.SelectionStart != _textBox.SelectionEnd)
|
|
{
|
|
var p1 = _handle1.IndicatorPosition;
|
|
var p2 = _handle2.IndicatorPosition;
|
|
|
|
topleft = new Point((p1.X + p2.X) / 2, Math.Min(p1.Y, p2.Y));
|
|
}
|
|
else
|
|
{
|
|
if (_caretHandle.IsEffectivelyVisible)
|
|
{
|
|
topleft = _caretHandle.IndicatorPosition;
|
|
}
|
|
}
|
|
|
|
if (topleft != null)
|
|
{
|
|
var oldPlacement = flyout.Placement;
|
|
var oldCallback = flyout.CustomPopupPlacementCallback;
|
|
var oldShowMode = flyout.ShowMode;
|
|
var point = ToTextBox(topleft.Value);
|
|
point = point.WithY(Math.Max(0, point.Y));
|
|
|
|
|
|
flyout.CustomPopupPlacementCallback = (x) => Place(x);
|
|
flyout.Placement = PlacementMode.Custom;
|
|
flyout.ShowMode = FlyoutShowMode.Transient;
|
|
|
|
_textBox.RaiseEvent(new ContextRequestedEventArgs());
|
|
|
|
flyout.Placement = oldPlacement;
|
|
flyout.CustomPopupPlacementCallback = oldCallback;
|
|
flyout.ShowMode = oldShowMode;
|
|
|
|
void Place(CustomPopupPlacement parameters)
|
|
{
|
|
parameters.Anchor = PopupAnchor.TopLeft;
|
|
var offset = parameters.Offset;
|
|
parameters.Offset = offset.WithX(point.X)
|
|
.WithY(point.Y - verticalOffset);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void TextBox_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
|
{
|
|
if (e.Property == TextPresenter.TextProperty)
|
|
{
|
|
ShowHandles = false;
|
|
CloseFlyout();
|
|
}
|
|
else if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty
|
|
|| e.Property == TextBox.CaretIndexProperty)
|
|
{
|
|
MoveHandlesToSelection();
|
|
}
|
|
}
|
|
|
|
private void PresenterKeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
ShowHandles = false;
|
|
s_isInTouchMode = false;
|
|
}
|
|
|
|
// Listener to layout changes for presenter.
|
|
private class PresenterVisualListener
|
|
{
|
|
private TextPresenter? _presenter;
|
|
|
|
public event EventHandler? Invalidated;
|
|
|
|
public void Attach(TextPresenter presenter)
|
|
{
|
|
if (_presenter != null)
|
|
throw new InvalidOperationException("Listener is already attached to a TextPresenter");
|
|
|
|
_presenter = presenter;
|
|
presenter.SizeChanged += Presenter_SizeChanged;
|
|
presenter.EffectiveViewportChanged += Visual_EffectiveViewportChanged;
|
|
}
|
|
|
|
private void Visual_EffectiveViewportChanged(object? sender, Layout.EffectiveViewportChangedEventArgs e)
|
|
{
|
|
OnInvalidated();
|
|
}
|
|
|
|
private void Presenter_SizeChanged(object? sender, SizeChangedEventArgs e)
|
|
{
|
|
OnInvalidated();
|
|
}
|
|
|
|
public void Detach()
|
|
{
|
|
if (_presenter is { } presenter)
|
|
{
|
|
presenter.SizeChanged -= Presenter_SizeChanged;
|
|
presenter.EffectiveViewportChanged -= Visual_EffectiveViewportChanged;
|
|
}
|
|
|
|
_presenter = null;
|
|
}
|
|
|
|
private void OnInvalidated()
|
|
{
|
|
Invalidated?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|