diff --git a/Source/HtmlRenderer/Core/Handlers/SelectionHandler.cs b/Source/HtmlRenderer/Core/Handlers/SelectionHandler.cs index 9913a9c5d..c9b75ef25 100644 --- a/Source/HtmlRenderer/Core/Handlers/SelectionHandler.cs +++ b/Source/HtmlRenderer/Core/Handlers/SelectionHandler.cs @@ -449,6 +449,11 @@ private void HandleSelection(RControl control, RPoint loc, bool allowPartialSele // if there is matching word if (word != null) { + var previousStart = _selectionStart; + var previousEnd = _selectionEnd; + var previousStartIndex = _selectionStartIndex; + var previousEndIndex = _selectionEndIndex; + if (_selectionStart == null) { // on start set the selection start word @@ -476,7 +481,14 @@ private void HandleSelection(RControl control, RPoint loc, bool allowPartialSele _cursorChanged = true; control.SetCursorIBeam(); - control.Invalidate(); + + // Most mouse moves stay within the same character, repainting the whole control for + // them only adds flicker, especially over remote desktop. + if (_selectionStart != previousStart || _selectionEnd != previousEnd + || _selectionStartIndex != previousStartIndex || _selectionEndIndex != previousEndIndex) + { + control.Invalidate(); + } } } } diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Text/SelectionInvalidateIntegrationTests.cs b/Source/Test/HtmlRenderer.IntegrationTest/Text/SelectionInvalidateIntegrationTests.cs new file mode 100644 index 000000000..ba2befa67 --- /dev/null +++ b/Source/Test/HtmlRenderer.IntegrationTest/Text/SelectionInvalidateIntegrationTests.cs @@ -0,0 +1,81 @@ +using HtmlRenderer.IntegrationTest.TestSupport; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Dom; + +namespace HtmlRenderer.IntegrationTest.Text; + +/// +/// Dragging a selection repaints the control only when the selection changes, every extra repaint shows up +/// as flicker over remote desktop (#271). +/// +[DoNotParallelize] +[TestClass] +public sealed class SelectionInvalidateIntegrationTests +{ + [TestMethod] + public void MouseMove_WithinTheSameCharacter_DoesNotRepaint() + { + var (root, container) = LayoutHarness.Layout(LayoutHarness.Wrap("
first second third
")); + var words = new List(); + CollectWords(LayoutHarness.FindById(root, "p")!, words); + var first = words.First(w => w.Text == "first"); + var third = words.First(w => w.Text == "third"); + var control = new FakeControl(container.Adapter); + + var start = new RPoint(first.Left + 1, first.Top + 2); + container.HandleMouseDown(control, start); + control.Invalidations = 0; + + container.HandleMouseMove(control, start); + Assert.AreEqual(1, control.Invalidations, "starting the selection must repaint"); + Assert.AreEqual("", container.SelectedText ?? ""); + + var target = new RPoint(third.Left + third.Width / 2, third.Top + 2); + container.HandleMouseMove(control, target); + Assert.AreEqual(2, control.Invalidations, "selecting up to the target must repaint"); + var selected = container.SelectedText; + StringAssert.StartsWith(selected, "first second t"); + Assert.AreNotEqual("first second third", selected); + + container.HandleMouseMove(control, target); + container.HandleMouseMove(control, new RPoint(target.X + 0.1, target.Y + 1)); + Assert.AreEqual(2, control.Invalidations, "moving within the same character must not repaint"); + Assert.AreEqual(selected, container.SelectedText); + + container.HandleMouseMove(control, new RPoint(third.Right - 1, target.Y)); + Assert.AreEqual(3, control.Invalidations, "extending the selection must repaint"); + Assert.AreEqual("first second third", container.SelectedText); + } + + private static void CollectWords(CssBox box, List words) + { + words.AddRange(box.Words); + foreach (var child in box.Boxes) + CollectWords(child, words); + } + + /// A control with the left mouse button held down that counts its repaints. + private sealed class FakeControl(RAdapter adapter) : RControl(adapter) + { + private const double CharWidth = 8; + + public int Invalidations { get; set; } + + public override bool LeftMouseButton => true; + public override bool RightMouseButton => false; + public override RPoint MouseLocation => RPoint.Empty; + public override void SetCursorDefault() { } + public override void SetCursorHand() { } + public override void SetCursorIBeam() { } + public override void DoDragDropCopy(object dragDropData) { } + + public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth) + { + charFit = Math.Min(str.Length, (int)(maxWidth / CharWidth)); + charFitWidth = charFit * CharWidth; + } + + public override void Invalidate() => Invalidations++; + } +}