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 );
