Menu

#1571 Check monospaced and average character width

Initial
open
5
2025-11-27
2025-11-19
Zufu Liu
No

While lookup into https://github.com/zufuliu/notepad4/issues/1118, I found some issues regarding checkMonospaced and aveCharWidth.
The mentioned bug is because I use aveCharWidth instead of monospaceCharacterWidth inside PositionCache::MeasureWidths(), unifont has an unusual tmAveCharWidth value (other than width of x):

1200:Unifont max=8.000000, min=8.000000, avg=16.000000, variance=0.000000
  1. for monospaced check, I think use minWidth to calculate scaledVariance is better, the rational is that when (maxWidth - minWidth)/minWidth < Epsilon, rendering ASCII graphic characters with minWidth will have no visual differences, the calculation has nothing related to aveCharWidth. The calculation can be optimized to use multiplication:
const XYPOSITION variance = maxWidth - minWidth;
constexpr XYPOSITION monospaceWidthEpsilon = 0.000001;
const XYPOSITION scaledVariance = monospaceWidthEpsilon * minWidth;
measurements.monospaceASCII = variance < scaledVariance;
  1. aveCharWidth can be unified to average character with of ASCII graphic characters (same as PlatCocoa), this would reduce platform differences (e.g. between GDI and DirectWrite). currently, platforms other GDI and Qt given AverageCharWidth() with measured string width, they can be unified as follow:
XYPOSITION aveCharWidth = positions.back() / allASCIIGraphic.length();
if (!surface.SupportsFeature(Supports::FractionalStrokeWidth)) {
    aveCharWidth = std::round(aveCharWidth);
}
measurements.aveCharWidth = aveCharWidth;
  1. after aveCharWidth changed to average character with of ASCII graphic characters, using it inside PositionCache::MeasureWidths() is better than monospaceCharacterWidth.
  2. inside ListBoxX::GetDesiredRect(), the local averageCharWidth can be replaced with aveCharWidth field (set by ac.lb->SetAverageCharWidth(aveCharWidth); inside ScintillaBase::AutoCompleteStart()).

Discussion

  • Zufu Liu

    Zufu Liu - 2025-11-20

    std::max_element() + std::min_element() can be merged into single std::minmax_element() as we don't care which one is min/max.

     
    • Zufu Liu

      Zufu Liu - 2025-11-22

      both are slow than a plain loop like following:

      auto it = positions.begin();
      XYPOSITION prev = *it;
      XYPOSITION maxWidth = prev;
      XYPOSITION minWidth = prev;
      while (++it != positions.end()) {
          const XYPOSITION current = *it;
          const XYPOSITION value = current - prev;
          prev = current;
          maxWidth = std::max(maxWidth, value);
          minWidth = std::min(minWidth, value);
      }
      
       
  • Neil Hodgson

    Neil Hodgson - 2025-11-24

    Including punctuation in the calculation of average character width will decrease the value and may lead to unexpected changes that are visible to users. Any change for Unifont should be narrow to avoid the possibility of regressions.

    The mentioned bug is because I use aveCharWidth instead of monospaceCharacterWidth

    That's a little strange. Are you trying to handle bi-width fonts where the Chinese characters should be exactly double the width of Roman characters?

    Using std::minmax_element would be fine although there will have to be an std::abs to avoid a negative value. Its easier to determine the intent of std::max_element / std::min_element than an explicit loop so can afford a tiny speed decrease.

     
  • Zufu Liu

    Zufu Liu - 2025-11-24

    will decrease the value and may lead to unexpected changes that are visible to users.

    Only happens for proportional font, so does SurfaceImpl::AverageCharWidth() in PlatCocoa.mm.

    That's a little strange. Are you trying to handle bi-width fonts where the Chinese characters should be exactly double the width of Roman characters?

    Unifont is ASCII monospaced, but GDI SurfaceGDI::AverageCharWidth() value is 2x wider than ASCII letter, use it (instead of monospaceCharacterWidth) inside PositionCache::MeasureWidths() to compute positions for ASCII characters gives wrong caret position.

    will have to be an std::abs to avoid a negative value.

    positions are increased only (current >= prev).

    than an explicit loop so can afford a tiny speed decrease.

    it also saves code size for MSVC, see _Minmax_element_impl in
    https://github.com/microsoft/STL/blob/main/stl/src/vector_algorithms.cpp#L2270

     
    • Neil Hodgson

      Neil Hodgson - 2025-11-24

      Since it is GDI that is providing poor data, a change to just that platform layer will have less potential to cause unexpected results. Perhaps just limit the return of SurfaceGDI::AverageCharWidth to a maximum of tmMaxCharWidth.

       
  • Zufu Liu

    Zufu Liu - 2025-11-25

    It looks a bug in the Unifont font itself, it's tmAveCharWidth is same as tmMaxCharWidth.

     
    • Neil Hodgson

      Neil Hodgson - 2025-11-27

      If APIs or embedded font metadata are producing bad results then it is hard to fix sensibly. A bug report could be sent to Unifont or a particular version could be recommended if this is present only in some versions.

      https://savannah.gnu.org/projects/unifont/

       

Log in to post a comment.