changeset 32334:46871f9a118e

Render cursor at correct position with fractional scaling. * libgui/qterminal/win32/QWinTerminalImpl.cpp (QConsolePrivate::drawTextBackground, QConsolePrivate::drawSelection, QConsolePrivate::drawText, QConsolePrivate::monitorConsole, QConsolePrivate::cursorRect): Multiplying a fixed integer width of a single character might not work correctly if the actual width of a character is fractional. Calculate the width from the start of the line instead of incrementing the cursor position by an integer single-character width times the characters in that line. (QWinTerminalImpl::viewPaintEvent): Update for changed function signatures. (QWinTerminalImpl::boundingRect): New function that returns a rectangle that extends over the entire character. See also: https://octave.discourse.group/t/transition-octave-to-qt6/3139/157
author Markus Mützel <markus.muetzel@gmx.de>
date Mon, 25 Sep 2023 16:35:09 +0200
parents 2c47affeec43
children 2b44805fc139
files libgui/qterminal/libqterminal/win32/QWinTerminalImpl.cpp
diffstat 1 files changed, 107 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- a/libgui/qterminal/libqterminal/win32/QWinTerminalImpl.cpp	Sun Sep 24 17:46:11 2023 +0200
+++ b/libgui/qterminal/libqterminal/win32/QWinTerminalImpl.cpp	Mon Sep 25 16:35:09 2023 +0200
@@ -21,6 +21,7 @@
 */
 
 #include <algorithm>
+#include <cmath>
 #include <csignal>
 #include <cstdio>
 #include <cstdarg>
@@ -191,6 +192,7 @@
   void startCommand (void);
   void sendConsoleText (const QString& s);
   QRect cursorRect (void);
+  QRect boundingRect (void);
   void selectAll();
   void selectWord(const QPoint& cellPos);
   void selectLine(const QPoint& cellPos);
@@ -218,15 +220,13 @@
   void setScrollBufferSize (int value);
 
   void drawTextBackground (QPainter& p, int cx1, int cy1, int cx2, int cy2,
-                           int cw, int ch);
+                           int ch);
 
-  void drawSelection (QPainter& p, int cx1, int cy1, int cx2, int cy2,
-                      int cw, int ch);
+  void drawSelection (QPainter& p, int cx1, int cy1, int cx2, int cy2, int ch);
 
   void drawCursor (QPainter& p);
 
-  void drawText (QPainter& p, int cx1, int cy1, int cx2, int cy2,
-                 int cw, int ch);
+  void drawText (QPainter& p, int cx1, int cy1, int cx2, int cy2, int ch);
 
 private:
   QWinTerminalImpl* q;
@@ -494,6 +494,8 @@
 
 QPoint QConsolePrivate::posToCell (const QPoint& p)
 {
+  // FIXME: This might become inaccurate for very long lines if the actual
+  //        character width is fractional.
   return QPoint (m_consoleRect.left () + p.x () / m_charSize.width (),
                  m_consoleRect.top () + p.y () / m_charSize.height ());
 }
@@ -635,11 +637,15 @@
 }
 
 void QConsolePrivate::drawTextBackground (QPainter& p, int cx1, int cy1,
-                                          int cx2, int cy2, int cw, int ch)
+                                          int cx2, int cy2, int ch)
 {
   p.save ();
 
-  int ascent = p.fontMetrics ().ascent ();
+  QFontMetrics fm = p.fontMetrics ();
+  QString sample ('m');
+  sample = sample.repeated (cx2);
+
+  int ascent = fm.ascent ();
   int stride = m_consoleRect.width ();
   int y = ascent + cy1 * ch;
 
@@ -647,8 +653,9 @@
     {
       int len = 0;
       bool hasChar = false;
-      int x = cx1 * cw;
+      double x = fm.horizontalAdvance (sample, cx1);
       WORD attr = 0;
+      int curr_cx1 = cx1;
 
       for (int i = cx1; i <= cx2; i++)
         {
@@ -663,10 +670,15 @@
                   if (hasChar || (attr & 0x00f0))
                     {
                       if (attr & 0x00f0)
-                        p.fillRect (x, y-ascent, len * cw, ch, p.brush ());
+                        // Try to not paint over parts of the preceeding
+                        // character.
+                        p.fillRect (std::ceil (x), y-ascent,
+                                    fm.horizontalAdvance (sample, len), ch,
+                                    p.brush ());
                     }
 
-                  x += (len * cw);
+                  curr_cx1 += len;
+                  x = fm.horizontalAdvance (sample, curr_cx1);
                   len = 0;
                   hasChar = false;
                 }
@@ -688,18 +700,19 @@
           // for-loop iteration
 
           if (attr & 0x00f0)
-            p.fillRect (x, y-ascent, len * cw, ch, p.brush ());
+            // Try to not paint over parts of the preceeding character.
+            p.fillRect (std::ceil (x), y-ascent,
+                        fm.horizontalAdvance (sample, len), ch, p.brush ());
         }
     }
 
   p.restore ();
 }
 
-void QConsolePrivate::selectAll()
+void QConsolePrivate::selectAll ()
 {
-  m_beginSelection = QPoint (0,0);
-  m_endSelection = QPoint(m_bufferSize.width (),
-                          m_cursorPos.y());
+  m_beginSelection = QPoint (0, 0);
+  m_endSelection = QPoint (m_bufferSize.width (), m_cursorPos.y ());
   updateSelection();
 }
 
@@ -724,28 +737,28 @@
   if (QChar(m_buffer[begin.y ()*stride + begin.x ()].Char.UnicodeChar).isSpace () == false)
   {
     // from current char, go back and fwd to find start and end of block
-    while(begin.x () > 0 &&
-          QChar(m_buffer[begin.y ()*stride + begin.x () -1].Char.UnicodeChar).isSpace() == false)
+    while (begin.x () > 0
+           && ! QChar (m_buffer[begin.y ()*stride + begin.x () -1].Char.UnicodeChar).isSpace ())
     {
-        begin.rx () --;
+      begin.rx () --;
     }
 
-    while(end.x () < m_consoleRect.width () &&
-          QChar(m_buffer[end.y ()*stride + end.x () +1].Char.UnicodeChar).isSpace() == false)
+    while (end.x () < m_consoleRect.width ()
+           && ! QChar (m_buffer[end.y ()*stride + end.x () +1].Char.UnicodeChar).isSpace ())
     {
       end.rx () ++;
     }
   }
   else
   {
-    while(begin.x () > 0 &&
-          QChar(m_buffer[begin.y ()*stride + begin.x () -1].Char.UnicodeChar).isSpace())
+    while (begin.x () > 0
+           && QChar (m_buffer[begin.y ()*stride + begin.x () -1].Char.UnicodeChar).isSpace ())
     {
       begin.rx () --;
     }
 
-    while(end.x () < m_consoleRect.width () &&
-          QChar(m_buffer[end.y ()*stride + end.x () +1].Char.UnicodeChar).isSpace ())
+    while (end.x () < m_consoleRect.width ()
+           && QChar (m_buffer[end.y ()*stride + end.x () +1].Char.UnicodeChar).isSpace ())
     {
       end.rx () ++;
     }
@@ -773,7 +786,7 @@
 
 
 void QConsolePrivate::drawSelection (QPainter& p, int cx1, int cy1,
-                                     int cx2, int cy2, int cw, int ch)
+                                     int cx2, int cy2, int ch)
 {
   p.save ();
 
@@ -794,7 +807,11 @@
   begin.rx () -= horizontalScrollOffset;
   end.rx () -= horizontalScrollOffset;
 
-  int ascent = p.fontMetrics ().ascent ();
+  QFontMetrics fm = p.fontMetrics ();
+  QString sample ('m');
+  sample = sample.repeated (cx2);
+
+  int ascent = fm.ascent ();
   int stride = m_consoleRect.width ();
 
   int y = ascent + cy1 * ch;
@@ -840,8 +857,9 @@
                      ? end.x () - selectionBegin + 1
                      : stride - selectionBegin);
 
-          p.fillRect (selectionBegin * cw, y-ascent, len * cw, ch,
-                      selectionColor ());
+          p.fillRect (std::floor (fm.horizontalAdvance (sample, selectionBegin)),
+                      y-ascent, std::ceil (fm.horizontalAdvance (sample, len)),
+                      ch, selectionColor ());
         }
     }
 
@@ -891,7 +909,7 @@
 }
 
 void QConsolePrivate::drawText (QPainter& p, int cx1, int cy1,
-                                int cx2, int cy2, int cw, int ch)
+                                int cx2, int cy2, int ch)
 {
   p.save ();
 
@@ -901,7 +919,11 @@
   QString s;
   s.reserve (cx2 - cx1 + 1);
 
-  int ascent = p.fontMetrics ().ascent ();
+  QFontMetrics fm = p.fontMetrics ();
+  QString sample ('m');
+  sample = sample.repeated (cx2);
+
+  int ascent = fm.ascent ();
   int stride = m_consoleRect.width ();
 
   int y = ascent + cy1 * ch;
@@ -910,8 +932,9 @@
       // Reset string buffer and starting X coordinate
       s.clear ();
       bool hasChar = false;
-      int x = cx1 * cw;
+      double x = fm.horizontalAdvance (sample, cx1);
       WORD attr = 0;
+      int curr_cx1 = cx1;
 
       for (int i = cx1; i <= cx2; i++)
         {
@@ -926,7 +949,8 @@
                   if (hasChar || (attr & 0x00f0))
                     p.drawText (x, y, s);
 
-                  x += (s.length () * cw);
+                  curr_cx1 += s.length ();
+                  x = fm.horizontalAdvance (sample, curr_cx1);
                   s.clear ();
                   hasChar = false;
                 }
@@ -947,7 +971,7 @@
           // No need to update s or x, they will be reset on the next
           // for-loop iteration
 
-          p.drawText (x, y, s);
+          p.drawText (std::ceil (x), y, s);
         }
     }
 
@@ -1001,8 +1025,8 @@
   m_charSize.rwidth () = fm.averageCharWidth ();
   m_charSize.rheight () = fm.lineSpacing ();
 
-  m_consoleRect.setWidth (winSize.width () / fm.averageCharWidth ());
-  m_consoleRect.setHeight (winSize.height () / fm.lineSpacing ());
+  m_consoleRect.setWidth (winSize.width () / m_charSize.width ());
+  m_consoleRect.setHeight (winSize.height () / m_charSize.height ());
 
   // Don't shrink the size of the buffer.  That way wide lines won't be
   // truncated and will reappear if the window is enlarged again later.
@@ -1264,16 +1288,22 @@
           || m_cursorPos.y () != sbi.dwCursorPosition.Y)
         {
           // Cursor position changed
+          QFontMetrics fm = m_consoleView->fontMetrics ();
+          QString sample ('m');
+          sample = sample.repeated (m_bufferSize.width ());
+          // "over-size" update rectangle for fractional character widths
           m_consoleView->update
-            ((m_cursorPos.x () - sbi.srWindow.Left) * m_charSize.width (),
+            (fm.horizontalAdvance (sample,
+                                   m_cursorPos.x () - sbi.srWindow.Left) - 1,
              (m_cursorPos.y () - sbi.srWindow.Top) * m_charSize.height (),
-             m_charSize.width (), m_charSize.height ());
+             m_charSize.width () + 2, m_charSize.height ());
           m_cursorPos.rx () = sbi.dwCursorPosition.X;
           m_cursorPos.ry () = sbi.dwCursorPosition.Y;
           m_consoleView->update
-            ((m_cursorPos.x () - sbi.srWindow.Left) * m_charSize.width (),
+            (fm.horizontalAdvance (sample,
+                                   m_cursorPos.x () - sbi.srWindow.Left) - 1,
              (m_cursorPos.y () - sbi.srWindow.Top) * m_charSize.height (),
-             m_charSize.width (), m_charSize.height ());
+             m_charSize.width () + 2, m_charSize.height ());
         }
 
       if (m_consoleRect.left () != sbi.srWindow.Left
@@ -1427,14 +1457,46 @@
 QRect
 QConsolePrivate::cursorRect (void)
 {
+  // The actual character width might be fractional (with non-integer scaling -
+  // high DPI).  But m_charSize.width () is integer.
+  // Assume a fixed-width font and calculate cursor position by measuring the
+  // width of characters starting from the first column.
+
+  QFontMetrics fm = m_consoleView->fontMetrics ();
+  QString sample ('m');
+  sample = sample.repeated (m_cursorPos.x () - m_consoleRect.x ());
+
+  // Integer precision is good enough for the line height and for the
+  // dimensions of the marker.
   int cw = m_charSize.width ();
   int ch = m_charSize.height ();
 
-  return QRect ((m_cursorPos.x () - m_consoleRect.x ()) * cw,
+  // Make sure the cursor starts *right* of the previous character.
+  return QRect (std::ceil (fm.horizontalAdvance (sample)),
                 (m_cursorPos.y () - m_consoleRect.y ()) * ch,
                 cw, ch);
 }
 
+QRect
+QConsolePrivate::boundingRect (void)
+{
+  // This might be slightly larger than cursorRect to make sure the entirety
+  // of a character is redrawn.
+
+  QFontMetrics fm = m_consoleView->fontMetrics ();
+  QString sample ('m');
+  sample = sample.repeated (m_cursorPos.x () - m_consoleRect.x ());
+
+  // Integer precision is good enough for the line height and for the
+  // dimensions of the marker.
+  int cw = m_charSize.width ();
+  int ch = m_charSize.height ();
+
+  return QRect (std::floor (fm.horizontalAdvance (sample)),
+                (m_cursorPos.y () - m_consoleRect.y ()) * ch,
+                cw+1, ch);
+}
+
 //////////////////////////////////////////////////////////////////////////////
 
 QWinTerminalImpl::QWinTerminalImpl (QWidget* parent)
@@ -1537,7 +1599,7 @@
   int ch = d->m_charSize.height ();
 
   QRect updateRect = event->rect ();
-  p.fillRect(updateRect, QBrush(d->backgroundColor()));
+  p.fillRect (updateRect, QBrush (d->backgroundColor ()));
 
   int cx1 = updateRect.left () / cw;
   int cy1 = updateRect.top () / ch;
@@ -1548,10 +1610,10 @@
       || cy1 > d->m_consoleRect.height () - 1)
     return;
 
-  d->drawTextBackground (p, cx1, cy1, cx2, cy2, cw, ch);
-  d->drawSelection (p, cx1, cy1, cx2, cy2, cw, ch);
+  d->drawTextBackground (p, cx1, cy1, cx2, cy2, ch);
+  d->drawSelection (p, cx1, cy1, cx2, cy2, ch);
   d->drawCursor (p);
-  d->drawText (p, cx1, cy1, cx2, cy2, cw, ch);
+  d->drawText (p, cx1, cy1, cx2, cy2, ch);
 }
 
 void QWinTerminalImpl::blinkCursorEvent (void)
@@ -1561,7 +1623,7 @@
   else
     d->m_cursorBlinking = false;
 
-  d->m_consoleView->update (d->cursorRect ());
+  d->m_consoleView->update (d->boundingRect ());
 }
 
 void QWinTerminalImpl::setBlinkingCursor (bool blink)