include/svx/weldeditview.hxx       |   10 ++
 include/vcl/cursor.hxx             |    3 
 svx/source/dialog/weldeditview.cxx |   84 +++++++++++++++++++++
 vcl/source/window/cursor.cxx       |  144 ++++++++++++++++++++++---------------
 4 files changed, 181 insertions(+), 60 deletions(-)

New commits:
commit ac8fa2c59acfcd33bf990bf9e173b0f3b4d416a8
Author:     Caolán McNamara <[email protected]>
AuthorDate: Sat Feb 14 16:49:52 2026 +0000
Commit:     Caolán McNamara <[email protected]>
CommitDate: Mon Feb 16 00:24:26 2026 +0100

    Resolves: tdf#143449 implement a blinking cursor for weldeditview
    
    Take a snapshot of what's under the cursor first and keep it,
    draw the cursor and take another snapshot. Then timer just calls
    an invalidate on the cursor area with the cursor reason for the
    invalidation, and just swap those snapshots on each draw while
    that's the only invalidation reason.
    
    Change-Id: I49a26af292905556cd6033f814aa8175c0da69a4
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199435
    Tested-by: Jenkins
    Reviewed-by: Caolán McNamara <[email protected]>

diff --git a/include/svx/weldeditview.hxx b/include/svx/weldeditview.hxx
index 0f6232e6f97e..219432196e58 100644
--- a/include/svx/weldeditview.hxx
+++ b/include/svx/weldeditview.hxx
@@ -15,6 +15,8 @@
 #include <editeng/editeng.hxx>
 #include <editeng/editview.hxx>
 #include <vcl/outdev.hxx>
+#include <vcl/timer.hxx>
+#include <vcl/virdev.hxx>
 #include <vcl/weld/customweld.hxx>
 
 class WeldEditAccessible;
@@ -55,6 +57,14 @@ protected:
     std::unique_ptr<EditView> m_xEditView;
     rtl::Reference<WeldEditAccessible> m_xAccessible;
 
+    // Cursor blink support
+    AutoTimer m_aCursorTimer;
+    bool m_bCursorVisible;
+    tools::Rectangle m_aCachedCursorPixRect; // pixel coords of cached cursor 
area
+    ScopedVclPtrInstance<VirtualDevice> m_xCursorOnDev;
+    ScopedVclPtrInstance<VirtualDevice> m_xCursorOffDev;
+    DECL_DLLPRIVATE_LINK(BlinkTimerHdl, Timer*, void);
+
     virtual void makeEditEngine();
 
     void InitAccessible();
diff --git a/svx/source/dialog/weldeditview.cxx 
b/svx/source/dialog/weldeditview.cxx
index 085aa7b52aeb..b5bee3addfa8 100644
--- a/svx/source/dialog/weldeditview.cxx
+++ b/svx/source/dialog/weldeditview.cxx
@@ -99,7 +99,22 @@ void WeldEditView::Paste()
 
 WeldEditView::WeldEditView()
     : m_bAcceptsTab(false)
+    , m_aCursorTimer("WeldEditView CursorTimer")
+    , m_bCursorVisible(false)
 {
+    m_aCursorTimer.SetInvokeHandler(LINK(this, WeldEditView, BlinkTimerHdl));
+}
+
+IMPL_LINK_NOARG(WeldEditView, BlinkTimerHdl, Timer*, void)
+{
+    m_bCursorVisible = !m_bCursorVisible;
+    if (!m_aCachedCursorPixRect.IsEmpty())
+    {
+        OutputDevice& rDevice = EditViewOutputDevice();
+        Invalidate(rDevice.PixelToLogic(m_aCachedCursorPixRect), 
weld::InvalidateFlags::Cursor);
+    }
+    else
+        Invalidate();
 }
 
 // tdf#127033 want to use UI font so override makeEditEngine to enable that
@@ -228,6 +243,21 @@ void WeldEditView::DoPaint(vcl::RenderContext& 
rRenderContext, const tools::Rect
         return;
     }
 
+    // Fast path: blink-only repaint with valid cache
+    weld::InvalidateFlags ePending = GetInvalidateFlags();
+    if (ePending == weld::InvalidateFlags::Cursor && 
!m_aCachedCursorPixRect.IsEmpty())
+    {
+        VirtualDevice& rSrc = m_bCursorVisible ? *m_xCursorOnDev : 
*m_xCursorOffDev;
+        // Blit cached bitmap in pixel coordinates to avoid rounding issues
+        bool bMapMode = rRenderContext.IsMapModeEnabled();
+        rRenderContext.EnableMapMode(false);
+        rRenderContext.DrawOutDev(m_aCachedCursorPixRect.TopLeft(),
+                                  m_aCachedCursorPixRect.GetSize(), Point(0, 
0),
+                                  m_aCachedCursorPixRect.GetSize(), rSrc);
+        rRenderContext.EnableMapMode(bMapMode);
+        return;
+    }
+
     auto popIt = rRenderContext.ScopedPush(vcl::PushFlags::ALL);
     rRenderContext.SetClipRegion();
 
@@ -239,7 +269,45 @@ void WeldEditView::DoPaint(vcl::RenderContext& 
rRenderContext, const tools::Rect
     {
         pEditView->ShowCursor(false);
         vcl::Cursor* pCursor = pEditView->GetCursor();
-        pCursor->DrawToDevice(rRenderContext);
+
+        // Get the pixel bounding rect the cursor will occupy
+        tools::Rectangle aPixRect = pCursor->GetBoundRect(rRenderContext);
+        if (!aPixRect.IsEmpty())
+        {
+            m_aCachedCursorPixRect = aPixRect;
+            Size aPixSize = aPixRect.GetSize();
+
+            // Cache in pixel coordinates to avoid rounding issues
+            bool bMapMode = rRenderContext.IsMapModeEnabled();
+            rRenderContext.EnableMapMode(false);
+
+            // Cache "cursor off" — text without cursor (before drawing cursor)
+            m_xCursorOffDev->SetOutputSizePixel(aPixSize);
+            m_xCursorOffDev->SetMapMode(MapMode(MapUnit::MapPixel));
+            m_xCursorOffDev->DrawOutDev(Point(0, 0), aPixSize, 
aPixRect.TopLeft(), aPixSize,
+                                        rRenderContext);
+
+            // Draw cursor, then cache "cursor on"
+            rRenderContext.EnableMapMode(bMapMode);
+            pCursor->DrawToDevice(rRenderContext);
+            rRenderContext.EnableMapMode(false);
+
+            m_xCursorOnDev->SetOutputSizePixel(aPixSize);
+            m_xCursorOnDev->SetMapMode(MapMode(MapUnit::MapPixel));
+            m_xCursorOnDev->DrawOutDev(Point(0, 0), aPixSize, 
aPixRect.TopLeft(), aPixSize,
+                                       rRenderContext);
+
+            rRenderContext.EnableMapMode(bMapMode);
+
+            // If cursor should be hidden, restore to clean state
+            if (!m_bCursorVisible)
+            {
+                rRenderContext.EnableMapMode(false);
+                rRenderContext.DrawOutDev(aPixRect.TopLeft(), aPixSize, 
Point(0, 0), aPixSize,
+                                          *m_xCursorOffDev);
+                rRenderContext.EnableMapMode(bMapMode);
+            }
+        }
     }
 
     // get logic selection
@@ -1573,6 +1641,16 @@ void WeldEditView::GetFocus()
     if (pEditView)
     {
         pEditView->ShowCursor(false);
+
+        m_bCursorVisible = true;
+        m_aCachedCursorPixRect = tools::Rectangle();
+        sal_uInt64 nBlinkTime = 
Application::GetSettings().GetStyleSettings().GetCursorBlinkTime();
+        if (nBlinkTime != STYLE_CURSOR_NOBLINKTIME)
+        {
+            m_aCursorTimer.SetTimeout(nBlinkTime);
+            m_aCursorTimer.Start();
+        }
+
         Invalidate(); // redraw with cursor
     }
 
@@ -1591,6 +1669,10 @@ void WeldEditView::GetFocus()
 
 void WeldEditView::LoseFocus()
 {
+    m_aCursorTimer.Stop();
+    m_bCursorVisible = false;
+    m_aCachedCursorPixRect = tools::Rectangle();
+
     weld::CustomWidgetController::LoseFocus();
     Invalidate(); // redraw without cursor
 
commit cd77de410a4cf94a417b1e4ea3aacf5fdf168c2e
Author:     Caolán McNamara <[email protected]>
AuthorDate: Sun Feb 15 21:20:51 2026 +0000
Commit:     Caolán McNamara <[email protected]>
CommitDate: Mon Feb 16 00:24:18 2026 +0100

    Related: tdf#143449 rework vcl::Cursor drawing to measure the affected area
    
    Change-Id: Iddf8edae975a344f0d1e1ffb6cd5ad57c02031ad
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199434
    Tested-by: Jenkins
    Reviewed-by: Caolán McNamara <[email protected]>

diff --git a/include/vcl/cursor.hxx b/include/vcl/cursor.hxx
index 268c23c37c0c..37e7a663d325 100644
--- a/include/vcl/cursor.hxx
+++ b/include/vcl/cursor.hxx
@@ -97,10 +97,11 @@ public:
                         { return !(Cursor::operator==( rCursor )); }
 
     void            DrawToDevice(OutputDevice& rRenderContext);
+    tools::Rectangle GetBoundRect(OutputDevice const& rRenderContext) const;
 
 private:
     SAL_DLLPRIVATE void LOKNotify( vcl::Window* pWindow, const OUString& 
rAction );
-    SAL_DLLPRIVATE bool ImplPrepForDraw(const OutputDevice* pDevice, 
ImplCursorData& rData);
+    SAL_DLLPRIVATE bool ImplPrepForDraw(const OutputDevice* pDevice, 
ImplCursorData& rData) const;
     SAL_DLLPRIVATE void ImplRestore();
     SAL_DLLPRIVATE void ImplDoShow( bool bDrawDirect, bool bRestore );
     SAL_DLLPRIVATE bool ImplDoHide( bool bStop );
diff --git a/vcl/source/window/cursor.cxx b/vcl/source/window/cursor.cxx
index 8dc0e5a06056..052bc81fdf13 100644
--- a/vcl/source/window/cursor.cxx
+++ b/vcl/source/window/cursor.cxx
@@ -48,12 +48,77 @@ namespace
 {
 const char* pDisableCursorIndicator(getenv("SAL_DISABLE_CURSOR_INDICATOR"));
 bool bDisableCursorIndicator(nullptr != pDisableCursorIndicator);
+
+// Build the cursor shape polygon accounting for direction indicators
+// and orientation, or return empty polygon for simple rectangular cursors
+tools::Polygon ImplCursorPoly(ImplCursorData const* pData)
+{
+    tools::Rectangle aRect(pData->maPixPos, pData->maPixSize);
+    if (pData->mnDirection == CursorDirection::NONE && !pData->mnOrientation)
+        return {};
+
+    tools::Polygon aPoly(aRect);
+    if (aPoly.GetSize() != 5)
+        return {};
+
+    aPoly[1].AdjustX(1);  // include the right border
+    aPoly[2].AdjustX(1);
+
+    // apply direction flag after slant to use the correct shape
+    if (!bDisableCursorIndicator && pData->mnDirection != 
CursorDirection::NONE)
+    {
+        Point pAry[7];
+        // Related system settings for "delta" could be:
+        // gtk cursor-aspect-ratio and  windows SPI_GETCARETWIDTH
+        int delta = (aRect.getOpenHeight() * 4 / 100) + 1;
+        if (pData->mnDirection == CursorDirection::LTR)
+        {
+            // left-to-right
+            pAry[0] = aPoly.GetPoint(0);
+            pAry[1] = aPoly.GetPoint(1);
+            pAry[2] = pAry[1];
+            pAry[2].AdjustX(delta);
+            pAry[2].AdjustY(delta);
+            pAry[3] = pAry[1];
+            pAry[3].AdjustY(delta * 2);
+            pAry[4] = aPoly.GetPoint(2);
+            pAry[5] = aPoly.GetPoint(3);
+            pAry[6] = aPoly.GetPoint(4);
+        }
+        else if (pData->mnDirection == CursorDirection::RTL)
+        {
+            // right-to-left
+            pAry[0] = aPoly.GetPoint(0);
+            pAry[1] = aPoly.GetPoint(1);
+            pAry[2] = aPoly.GetPoint(2);
+            pAry[3] = aPoly.GetPoint(3);
+            pAry[4] = pAry[0];
+            pAry[4].AdjustY(delta * 2);
+            pAry[5] = pAry[0];
+            pAry[5].AdjustX(-delta);
+            pAry[5].AdjustY(delta);
+            pAry[6] = aPoly.GetPoint(4);
+        }
+        aPoly = tools::Polygon(7, pAry);
+    }
+
+    if (pData->mnOrientation)
+        aPoly.Rotate(pData->maPixRotOff, pData->mnOrientation);
+    return aPoly;
 }
 
-static tools::Rectangle ImplCursorInvert(vcl::RenderContext* pRenderContext, 
ImplCursorData const * pData)
+// Calculate the pixel bounding rect of the cursor
+tools::Rectangle ImplCursorBoundRect(ImplCursorData const* pData)
 {
-    tools::Rectangle aPaintRect;
+    tools::Polygon aPoly = ImplCursorPoly(pData);
+    if (aPoly.GetSize())
+        return aPoly.GetBoundRect();
+    return tools::Rectangle(pData->maPixPos, pData->maPixSize);
+}
+}
 
+static tools::Rectangle ImplCursorInvert(vcl::RenderContext* pRenderContext, 
ImplCursorData const * pData)
+{
     bool bMapMode = pRenderContext->IsMapModeEnabled();
     pRenderContext->EnableMapMode( false );
     InvertFlags nInvertStyle;
@@ -62,66 +127,21 @@ static tools::Rectangle 
ImplCursorInvert(vcl::RenderContext* pRenderContext, Imp
     else
         nInvertStyle = InvertFlags::NONE;
 
-    tools::Rectangle aRect( pData->maPixPos, pData->maPixSize );
-    if ( pData->mnDirection != CursorDirection::NONE || pData->mnOrientation )
+    tools::Rectangle aRect;
+    tools::Polygon aPoly = ImplCursorPoly(pData);
+    if (aPoly.GetSize())
     {
-        tools::Polygon aPoly( aRect );
-        if( aPoly.GetSize() == 5 )
-        {
-            aPoly[1].AdjustX(1 );  // include the right border
-            aPoly[2].AdjustX(1 );
-
-            // apply direction flag after slant to use the correct shape
-            if (!bDisableCursorIndicator && pData->mnDirection != 
CursorDirection::NONE)
-            {
-                Point pAry[7];
-                // Related system settings for "delta" could be:
-                // gtk cursor-aspect-ratio and  windows SPI_GETCARETWIDTH
-                int delta = (aRect.getOpenHeight() * 4 / 100) + 1;
-                if( pData->mnDirection == CursorDirection::LTR )
-                {
-                    // left-to-right
-                    pAry[0] = aPoly.GetPoint( 0 );
-                    pAry[1] = aPoly.GetPoint( 1 );
-                    pAry[2] = pAry[1];
-                    pAry[2].AdjustX(delta);
-                    pAry[2].AdjustY(delta);
-                    pAry[3] =  pAry[1];
-                    pAry[3].AdjustY(delta * 2);
-                    pAry[4] = aPoly.GetPoint( 2 );
-                    pAry[5] = aPoly.GetPoint( 3 );
-                    pAry[6] = aPoly.GetPoint( 4 );
-                }
-                else if( pData->mnDirection == CursorDirection::RTL )
-                {
-                    // right-to-left
-                    pAry[0] = aPoly.GetPoint( 0 );
-                    pAry[1] = aPoly.GetPoint( 1 );
-                    pAry[2] = aPoly.GetPoint( 2 );
-                    pAry[3] = aPoly.GetPoint( 3 );
-                    pAry[4] = pAry[0];
-                    pAry[4].AdjustY(delta*2);
-                    pAry[5] =  pAry[0];
-                    pAry[5].AdjustX(-delta);
-                    pAry[5].AdjustY(delta);
-                    pAry[6] = aPoly.GetPoint( 4 );
-                }
-                aPoly = tools::Polygon( 7, pAry);
-            }
-
-            if ( pData->mnOrientation )
-                aPoly.Rotate( pData->maPixRotOff, pData->mnOrientation );
-            pRenderContext->Invert( aPoly, nInvertStyle );
-            aPaintRect = aPoly.GetBoundRect();
-        }
+        pRenderContext->Invert(aPoly, nInvertStyle);
+        aRect = aPoly.GetBoundRect();
     }
     else
     {
-        pRenderContext->Invert( aRect, nInvertStyle );
-        aPaintRect = aRect;
+        aRect = tools::Rectangle(pData->maPixPos, pData->maPixSize);
+        pRenderContext->Invert(aRect, nInvertStyle);
     }
-    pRenderContext->EnableMapMode( bMapMode );
-    return aPaintRect;
+
+    pRenderContext->EnableMapMode(bMapMode);
+    return aRect;
 }
 
 static void ImplCursorInvert(vcl::Window* pWindow, ImplCursorData const * 
pData)
@@ -141,7 +161,7 @@ static void ImplCursorInvert(vcl::Window* pWindow, 
ImplCursorData const * pData)
         pGuard->SetPaintRect(pRenderContext->PixelToLogic(aPaintRect));
 }
 
-bool vcl::Cursor::ImplPrepForDraw(const OutputDevice* pDevice, ImplCursorData& 
rData)
+bool vcl::Cursor::ImplPrepForDraw(const OutputDevice* pDevice, ImplCursorData& 
rData) const
 {
     if (pDevice && !rData.mbCurVisible)
     {
@@ -186,6 +206,14 @@ void vcl::Cursor::DrawToDevice(OutputDevice& 
rRenderContext)
     }
 }
 
+tools::Rectangle vcl::Cursor::GetBoundRect(OutputDevice const& rRenderContext) 
const
+{
+    ImplCursorData aData;
+    if (ImplPrepForDraw(&rRenderContext, aData))
+        return ImplCursorBoundRect(&aData);
+    return {};
+}
+
 void vcl::Cursor::ImplRestore()
 {
     assert( mpData && mpData->mbCurVisible );

Reply via email to