freetype-commit
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[freetype2-demos] gsoc-2022-chariri-2 c913540 24/30: [ftinspect] WIP: Ad


From: Werner Lemberg
Subject: [freetype2-demos] gsoc-2022-chariri-2 c913540 24/30: [ftinspect] WIP: Add initial support to "Continuous View".
Date: Mon, 11 Jul 2022 07:17:41 -0400 (EDT)

branch: gsoc-2022-chariri-2
commit c913540245e87620abe00e36742620e0cf948eb6
Author: Charlie Jiang <w@chariri.moe>
Commit: Charlie Jiang <w@chariri.moe>

    [ftinspect] WIP: Add initial support to "Continuous View".
    
    This commit adds a tab to the main GUI named "Continuous View",
    corresponding to the `ftview` tool. Inside the tab is a glyph canvas and
    another tab view. The tab view is used to switch modes and input settings.
    
    The rendering canvas is *not* a `QGraphicsView`, but a simple `QWidget` with
    a custom `paintEvent`. This is because we don't need features like panning
    and zooming, so using `QGraphicsView` will only add complexity (e.g.
    transforming coordination systems).
    
    Code translating FreeType outline to the origin for rendering is extracted
    as a helper function in a new "render utils" file.
    
    The font size selector (size, unit, DPI) in the original `SingularTab` is
    extracted as a new class `FontSizeSelector`.
    
    The "graphics defaults", which are graphics objects like color palettes and
    `QPen`s, are all extracted into a new singleton struct `GraphicsDefaults`,
    so every component can easily obtain them.
    
    Currently there're a lot of settings, status variables and parameters during
    the rendering process. Will write some documentation clearifing them all.
    
    WIP: Only "all glyphs" view is implemented.
    TODO: Fancy, Stroked, Text String mode; LCD Anti-Aliasing; Bitmap font;
    
    The rendering code is adopted from singular rendering, which is different
    from the code in `ftcommon.c` from the legacy tools. In the future we may
    need to re-implement this with code from the latter.
    
    * src/ftinspect/rendering/glyphcontinuous.cpp,
      src/ftinspect/rendering/glyphcontinuous.hpp: New file: the actual
      "rendering canvas" in the "Continuous View". The code was a combination of
      `ftcommon` and the singular view's render views. Mouse scrolling is
      supported for navigation and resizing.
    
    * src/ftinspect/panels/continuous.cpp, src/ftinspect/panels/continuous.hpp:
      New files: the content of the "Continuous View" tab is a `ContinuousTab`.
      It current only support one single mode and submode.
    
      Settings about "All Glyphs" mode is placed in `ContinousAllGlyphsTab`.
    
    * src/ftinspect/engine/engine.cpp, src/ftinspect/engine/engine.hpp: Add
      CharMap handling code, especially `CharMapInfo` struct. `CharMapInfo` can
      provide the name and max index for the charmap. Vector of `CharMapInfo`
      for current font is obtained via `Engine::currentFontCharMaps`.
    
      CharMap mapping is done via `CharMapInfo::glyphIndexFromCharCode`, with
      the added `FTC_CMapCache`.
    
      Add more actions to support continuous view. `reloadFont` and
      `loadGlyphWithoutUpdate` can load fonts and glyphs with minimal overhead.
      `loadGlyphWithoutUpdate` uses `FTC_ImageCache_Lookup`, so the
      `FTC_ImageTypeRec` object is cached.
    
    * src/ftinspect/widgets/fontsizeselector.cpp,
      src/ftinspect/widgets/fontsizeselector.hpp: New file, include the font
      size setting part. It can directly apply settings to the engine via
      `applyToEngine` function, and can handle wheel event from event object or
      by steps count.
    
    * src/ftinspect/widgets/glyphindexselector.cpp,
      src/ftinspect/widgets/glyphindexselector.hpp: Combine `setMax` and
      `setMin` functions, and reduce unnecessary signals by blocking them.
    
      Rename `getCurrentIndex` to `currentIndex`.
    
      Support custom number rendering (for Unicode charcode formatting). Adjust
      layout and disabled wrapping on the spinbox.
    
    * src/ftinspect/rendering/graphicsdefault.cpp,
      src/ftinspect/rendering/graphicsdefault.hpp: New file, including graphics
      initializing code.
    
    * src/ftinspect/rendering/renderutils.cpp,
      src/ftinspect/rendering/renderutils.hpp: New file, including outline
      translating code.
    
    * src/ftinspect/rendering/glyphbitmap.cpp: Move out outline tranlating code.
    
    * src/ftinspect/panels/singular.cpp, src/ftinspect/panels/singular.hpp: Move
      out the font size selector and graphics defaults. Minor change due to
      change in `GlyphIndexSelector`.
    
    * src/ftinspect/maingui.cpp, src/ftinspect/maingui.hpp: Add "Continuous
      View" to tab view.
    
    * src/ftinspect/CMakeLists.txt, src/ftinspect/meson.build: Updated.
---
 src/ftinspect/CMakeLists.txt                 |   5 +
 src/ftinspect/engine/engine.cpp              | 203 +++++++++++++++-
 src/ftinspect/engine/engine.hpp              |  41 +++-
 src/ftinspect/maingui.cpp                    |   8 +
 src/ftinspect/maingui.hpp                    |   2 +
 src/ftinspect/meson.build                    |   8 +
 src/ftinspect/panels/continuous.cpp          | 347 +++++++++++++++++++++++++++
 src/ftinspect/panels/continuous.hpp          | 124 ++++++++++
 src/ftinspect/panels/singular.cpp            | 145 +++--------
 src/ftinspect/panels/singular.hpp            |  29 +--
 src/ftinspect/rendering/glyphbitmap.cpp      |  19 +-
 src/ftinspect/rendering/glyphcontinuous.cpp  | 212 ++++++++++++++++
 src/ftinspect/rendering/glyphcontinuous.hpp  |  77 ++++++
 src/ftinspect/rendering/graphicsdefault.cpp  |  48 ++++
 src/ftinspect/rendering/graphicsdefault.hpp  |  34 +++
 src/ftinspect/rendering/renderutils.cpp      |  34 +++
 src/ftinspect/rendering/renderutils.hpp      |  15 ++
 src/ftinspect/widgets/fontsizeselector.cpp   | 147 ++++++++++++
 src/ftinspect/widgets/fontsizeselector.hpp   |  58 +++++
 src/ftinspect/widgets/glyphindexselector.cpp |  76 ++++--
 src/ftinspect/widgets/glyphindexselector.hpp |  12 +-
 21 files changed, 1454 insertions(+), 190 deletions(-)

diff --git a/src/ftinspect/CMakeLists.txt b/src/ftinspect/CMakeLists.txt
index c0b1340..92d6b34 100644
--- a/src/ftinspect/CMakeLists.txt
+++ b/src/ftinspect/CMakeLists.txt
@@ -27,15 +27,20 @@ add_executable(ftinspect
   "rendering/glyphoutline.cpp"
   "rendering/glyphpointnumbers.cpp"
   "rendering/glyphpoints.cpp"
+  "rendering/glyphcontinuous.cpp"
   "rendering/grid.cpp"
+  "rendering/graphicsdefault.cpp"
+  "rendering/renderutils.cpp"
 
   "widgets/customwidgets.cpp"
   "widgets/glyphindexselector.cpp"
+  "widgets/fontsizeselector.cpp"
 
   "models/ttsettingscomboboxmodel.cpp"
 
   "panels/settingpanel.cpp"
   "panels/singular.cpp"
+  "panels/continuous.cpp"
 )
 target_link_libraries(ftinspect
   Qt5::Core Qt5::Widgets
diff --git a/src/ftinspect/engine/engine.cpp b/src/ftinspect/engine/engine.cpp
index 552b0a8..e4e7e42 100644
--- a/src/ftinspect/engine/engine.cpp
+++ b/src/ftinspect/engine/engine.cpp
@@ -148,6 +148,12 @@ Engine::Engine()
     // XXX error handling
   }
 
+  error = FTC_CMapCache_New(cacheManager_, &cmapCache_);
+  if (error)
+  {
+    // XXX error handling
+  }
+
   queryEngine();
 }
 
@@ -278,6 +284,8 @@ Engine::loadFont(int fontIndex,
     }
   }
 
+  imageType_.face_id = scaler_.face_id;
+
   if (numGlyphs < 0)
   {
     ftSize_ = NULL;
@@ -286,16 +294,22 @@ Engine::loadFont(int fontIndex,
   }
   else
   {
-    curFamilyName_ = QString(ftSize_->face->family_name);
-    curStyleName_ = QString(ftSize_->face->style_name);
+    auto face = ftSize_->face;
+    curFamilyName_ = QString(face->family_name);
+    curStyleName_ = QString(face->style_name);
 
-    const char* moduleName = FT_FACE_DRIVER_NAME( ftSize_->face );
+    const char* moduleName = FT_FACE_DRIVER_NAME( face );
 
     // XXX cover all available modules
     if (!strcmp(moduleName, "cff"))
       fontType_ = FontType_CFF;
     else if (!strcmp(moduleName, "truetype"))
       fontType_ = FontType_TrueType;
+
+    curCharMaps_.clear();
+    curCharMaps_.reserve(face->num_charmaps);
+    for (int i = 0; i < face->num_charmaps; i++)
+      curCharMaps_.append(CharMapInfo(i, face->charmaps[i]));
   }
 
   curNumGlyphs_ = numGlyphs;
@@ -303,6 +317,17 @@ Engine::loadFont(int fontIndex,
 }
 
 
+void
+Engine::reloadFont()
+{
+  update();
+  if (!scaler_.face_id)
+    return;
+  imageType_.face_id = scaler_.face_id;
+  FTC_Manager_LookupSize(cacheManager_, &scaler_, &ftSize_);
+}
+
+
 void
 Engine::removeFont(int fontIndex, bool closeFile)
 {
@@ -331,6 +356,22 @@ Engine::removeFont(int fontIndex, bool closeFile)
 }
 
 
+unsigned
+Engine::glyphIndexFromCharCode(int code, int charMapIndex)
+{
+  if (charMapIndex == -1)
+    return code;
+  return FTC_CMapCache_Lookup(cmapCache_, scaler_.face_id, charMapIndex, code);
+}
+
+
+FT_Size_Metrics const&
+Engine::currentFontMetrics()
+{
+  return ftSize_->metrics;
+}
+
+
 QString
 Engine::glyphName(int index)
 {
@@ -390,6 +431,26 @@ Engine::loadOutline(int glyphIndex)
 }
 
 
+FT_Glyph
+Engine::loadGlyphWithoutUpdate(int glyphIndex)
+{
+  // TODO bitmap fonts? color layered fonts?
+  FT_Glyph glyph;
+  imageType_.flags |= FT_LOAD_NO_BITMAP;
+  if (FTC_ImageCache_Lookup(imageCache_,
+                            &imageType_,
+                            glyphIndex,
+                            &glyph,
+                            NULL))
+  {
+    // XXX error handling?
+    return NULL;
+  }
+
+  return glyph;
+}
+
+
 int
 Engine::numberOfOpenedFonts()
 {
@@ -500,6 +561,10 @@ Engine::update()
     scaler_.x_res = dpi_;
     scaler_.y_res = dpi_;
   }
+  
+  imageType_.width = static_cast<unsigned int>(pixelSize_);
+  imageType_.height = static_cast<unsigned int>(pixelSize_);
+  imageType_.flags = static_cast<int>(loadFlags_);
 }
 
 
@@ -602,4 +667,136 @@ Engine::queryEngine()
   }
 }
 
+
+QHash<FT_Encoding, QString> encodingNamesCache;
+QHash<FT_Encoding, QString>&
+encodingNames()
+{
+  if (encodingNamesCache.empty())
+  {
+    encodingNamesCache[static_cast<FT_Encoding>(FT_ENCODING_OTHER)]
+     = "Unknown Encoding";
+    encodingNamesCache[FT_ENCODING_NONE] = "No Encoding";
+    encodingNamesCache[FT_ENCODING_MS_SYMBOL] = "MS Symbol (symb)";
+    encodingNamesCache[FT_ENCODING_UNICODE] = "Unicode (unic)";
+    encodingNamesCache[FT_ENCODING_SJIS] = "Shift JIS (sjis)";
+    encodingNamesCache[FT_ENCODING_PRC] = "PRC/GB 18030 (gb)";
+    encodingNamesCache[FT_ENCODING_BIG5] = "Big5 (big5)";
+    encodingNamesCache[FT_ENCODING_WANSUNG] = "Wansung (wans)";
+    encodingNamesCache[FT_ENCODING_JOHAB] = "Johab (joha)";
+    encodingNamesCache[FT_ENCODING_ADOBE_STANDARD] = "Adobe Standard (ADOB)";
+    encodingNamesCache[FT_ENCODING_ADOBE_EXPERT] = "Adobe Expert (ADBE)";
+    encodingNamesCache[FT_ENCODING_ADOBE_CUSTOM] = "Adobe Custom (ADBC)";
+    encodingNamesCache[FT_ENCODING_ADOBE_LATIN_1] = "Latin 1 (lat1)";
+    encodingNamesCache[FT_ENCODING_OLD_LATIN_2] = "Latin 2 (lat2)";
+    encodingNamesCache[FT_ENCODING_APPLE_ROMAN] = "Apple Roman (armn)";
+  }
+
+  return encodingNamesCache;
+}
+
+
+CharMapInfo::CharMapInfo(int index, FT_CharMap cmap)
+: index(index), ptr(cmap), encoding(cmap->encoding), maxIndex(-1)
+{
+  auto& names = encodingNames();
+  auto it = names.find(encoding);
+  if (it == names.end())
+    encodingName = &names[static_cast<FT_Encoding>(FT_ENCODING_OTHER)];
+  else
+    encodingName = &it.value();
+
+  if (encoding != FT_ENCODING_OTHER)
+    maxIndex = computeMaxIndex();
+}
+
+
+QString
+CharMapInfo::stringifyIndex(int code, int index)
+{
+  return QString("CharCode: %1 (glyph idx %2)")
+           .arg(stringifyIndexShort(code))
+           .arg(index);
+}
+
+
+QString
+CharMapInfo::stringifyIndexShort(int code)
+{
+  return (encoding == FT_ENCODING_UNICODE ? "U+" : "0x")
+         + QString::number(code, 16).rightJustified(4, '0').toUpper();
+}
+
+
+int
+CharMapInfo::computeMaxIndex()
+{
+  int maxIndex = 0;
+  switch (encoding)
+  {
+  case FT_ENCODING_UNICODE:
+    maxIndex = maxIndexForFaceAndCharMap(ptr, 0x110000) + 1;
+    break;
+
+  case FT_ENCODING_ADOBE_LATIN_1:
+  case FT_ENCODING_ADOBE_STANDARD:
+  case FT_ENCODING_ADOBE_EXPERT:
+  case FT_ENCODING_ADOBE_CUSTOM:
+  case FT_ENCODING_APPLE_ROMAN:
+    maxIndex = 0x100;
+    break;
+
+  /* some fonts use range 0x00-0x100, others have 0xF000-0xF0FF */
+  case FT_ENCODING_MS_SYMBOL:
+    maxIndex = maxIndexForFaceAndCharMap(ptr, 0x10000) + 1;
+    break;
+
+  default:
+    // Some encodings can reach > 0x10000, e.g. GB 18030.
+    maxIndex = maxIndexForFaceAndCharMap(ptr, 0x110000) + 1;
+  }
+  return maxIndex;
+}
+
+
+int
+CharMapInfo::maxIndexForFaceAndCharMap(FT_CharMap charMap,
+                                       unsigned max)
+{
+  // code adopted from `ftcommon.c`
+  FT_ULong min = 0;
+  FT_UInt glyphIndex;
+  FT_Face face = charMap->face;
+
+  if (FT_Set_Charmap(face, charMap))
+    return -1;
+
+  do
+  {
+    FT_ULong mid = (min + max) >> 1;
+    FT_ULong res = FT_Get_Next_Char(face, mid, &glyphIndex);
+
+    if (glyphIndex)
+      min = res;
+    else
+    {
+      max = mid;
+
+      // once moved, it helps to advance min through sparse regions
+      if (min)
+      {
+        res = FT_Get_Next_Char(face, min, &glyphIndex);
+
+        if (glyphIndex)
+          min = res;
+        else
+          max = min; // found it
+      }
+    }
+  } while (max > min);
+
+  return static_cast<int>(max);
+}
+
+
 // end of engine.cpp
diff --git a/src/ftinspect/engine/engine.hpp b/src/ftinspect/engine/engine.hpp
index 7a45094..80ac9ed 100644
--- a/src/ftinspect/engine/engine.hpp
+++ b/src/ftinspect/engine/engine.hpp
@@ -36,6 +36,29 @@ struct FaceID
   bool operator<(const FaceID& other) const;
 };
 
+class Engine;
+
+#define FT_ENCODING_OTHER 0xFFFE
+struct CharMapInfo
+{
+  int index;
+  FT_CharMap ptr;
+  FT_Encoding encoding;
+  QString* encodingName;
+
+  // Actually this shouldn't go here, but for convenience...
+  int maxIndex;
+
+  CharMapInfo(int index, FT_CharMap cmap);
+
+  QString stringifyIndex(int code, int index);
+  QString stringifyIndexShort(int code);
+
+private:
+  int computeMaxIndex();
+  static int maxIndexForFaceAndCharMap(FT_CharMap charMap, unsigned max);
+};
+
 // FreeType specific data.
 
 class Engine
@@ -70,6 +93,13 @@ public:
                int namedInstanceIndex); // return number of glyphs
   FT_Outline* loadOutline(int glyphIndex);
 
+  // Sometimes the engine is already updated, and we want to be faster
+  FT_Glyph loadGlyphWithoutUpdate(int glyphIndex);
+
+  // reload current triplet, but with updated settings, useful for updating
+  // `ftSize_` only
+  void reloadFont(); 
+
   void openFonts(QStringList fontFileNames);
   void removeFont(int fontIndex, bool closeFile = true);
   
@@ -87,14 +117,16 @@ public:
   long numberOfFaces(int fontIndex);
   int numberOfNamedInstances(int fontIndex,
                              long faceIndex);
+  // Note: the current font face must be properly set
+  unsigned glyphIndexFromCharCode(int code, int charMapIndex);
+  FT_Size_Metrics const& currentFontMetrics();
 
-  // XXX We should prepend '_' to all private member variable so we can create
-  // getter without naming conflict... e.g. var named _fontFileManager while
-  // getter named fontFileManager
+  QVector<CharMapInfo>& currentFontCharMaps() { return curCharMaps_; }
   FontFileManager& fontFileManager() { return fontFileManager_; }
   EngineDefaultValues& engineDefaults() { return engineDefaults_; }
   bool antiAliasingEnabled() { return antiAliasingEnabled_; }
 
+
   //////// Setters (direct or indirect)
 
   void setDPI(int d) { dpi_ = d; }
@@ -142,14 +174,17 @@ private:
   QString curFamilyName_;
   QString curStyleName_;
   int curNumGlyphs_ = -1;
+  QVector<CharMapInfo> curCharMaps_;
 
   FT_Library library_;
   FTC_Manager cacheManager_;
   FTC_ImageCache imageCache_;
   FTC_SBitCache sbitsCache_;
+  FTC_CMapCache cmapCache_;
 
   FTC_ScalerRec scaler_;
   FT_Size ftSize_;
+  FTC_ImageTypeRec imageType_;
 
   EngineDefaultValues engineDefaults_;
 
diff --git a/src/ftinspect/maingui.cpp b/src/ftinspect/maingui.cpp
index e1fbf6f..2c78245 100644
--- a/src/ftinspect/maingui.cpp
+++ b/src/ftinspect/maingui.cpp
@@ -15,6 +15,8 @@
 
 #include <freetype/ftdriver.h>
 
+#include "panels/continuous.hpp"
+
 
 MainGUI::MainGUI(Engine* engine)
 : engine_(engine)
@@ -448,12 +450,15 @@ MainGUI::createLayout()
   fontNameLabel_ = new QLabel(this);
 
   singularTab_ = new SingularTab(this, engine_);
+  continuousTab_ = new ContinuousTab(this, engine_);
 
   tabWidget_ = new QTabWidget(this);
 
   // Note those two list must be in sync
   tabs_.append(singularTab_);
   tabWidget_->addTab(singularTab_, tr("Singular Grid View"));
+  tabs_.append(continuousTab_);
+  tabWidget_->addTab(continuousTab_, tr("Continuous View"));
 
   previousFontButton_ = new QPushButton(tr("Previous Font"), this);
   nextFontButton_ = new QPushButton(tr("Next Font"), this);
@@ -504,6 +509,9 @@ MainGUI::createConnections()
   connect(settingPanel_, &SettingPanel::repaintNeeded,
           this, &MainGUI::repaintCurrentTab);
 
+  connect(tabWidget_, &QTabWidget::currentChanged,
+          this, &MainGUI::reloadCurrentTabFont);
+
   connect(previousFontButton_, &QPushButton::clicked,
           this, &MainGUI::previousFont);
   connect(nextFontButton_, &QPushButton::clicked,
diff --git a/src/ftinspect/maingui.hpp b/src/ftinspect/maingui.hpp
index 5e9f9e7..ff08176 100644
--- a/src/ftinspect/maingui.hpp
+++ b/src/ftinspect/maingui.hpp
@@ -11,6 +11,7 @@
 #include "models/ttsettingscomboboxmodel.hpp"
 #include "panels/settingpanel.hpp"
 #include "panels/singular.hpp"
+#include "panels/continuous.hpp"
 
 #include <QAction>
 #include <QCheckBox>
@@ -136,6 +137,7 @@ private:
   QTabWidget* tabWidget_;
   QVector<AbstractTab*> tabs_;
   SingularTab* singularTab_;
+  ContinuousTab* continuousTab_;
 
   void openFonts(QStringList const& fileNames);
 
diff --git a/src/ftinspect/meson.build b/src/ftinspect/meson.build
index de0e7e9..f41e731 100644
--- a/src/ftinspect/meson.build
+++ b/src/ftinspect/meson.build
@@ -27,14 +27,19 @@ if qt5_dep.found()
     'rendering/glyphoutline.cpp',
     'rendering/glyphpointnumbers.cpp',
     'rendering/glyphpoints.cpp',
+    'rendering/glyphcontinuous.cpp',
     'rendering/grid.cpp',
+    'rendering/graphicsdefault.cpp',
+    'rendering/renderutils.cpp',
     'widgets/customwidgets.cpp',
     'widgets/glyphindexselector.cpp',
+    'widgets/fontsizeselector.cpp',
 
     'models/ttsettingscomboboxmodel.cpp',
 
     'panels/settingpanel.cpp',
     'panels/singular.cpp',
+    'panels/continuous.cpp',
 
     'ftinspect.cpp',
     'maingui.cpp',
@@ -46,9 +51,12 @@ if qt5_dep.found()
       'engine/fontfilemanager.hpp',
       'widgets/customwidgets.hpp',
       'widgets/glyphindexselector.hpp',
+      'widgets/fontsizeselector.hpp',
+      'widgets/glyphcontinuous.hpp',
       'models/ttsettingscomboboxmodel.hpp',
       'panels/settingpanel.hpp',
       'panels/singular.hpp',
+      'panels/continuous.hpp',
       'maingui.hpp',
     ],
     dependencies: qt5_dep)
diff --git a/src/ftinspect/panels/continuous.cpp 
b/src/ftinspect/panels/continuous.cpp
new file mode 100644
index 0000000..d24ff87
--- /dev/null
+++ b/src/ftinspect/panels/continuous.cpp
@@ -0,0 +1,347 @@
+// continuous.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "continuous.hpp"
+
+#include <climits>
+#include <QVariant>
+
+
+ContinuousTab::ContinuousTab(QWidget* parent,
+                             Engine* engine)
+: QWidget(parent), engine_(engine)
+{
+  createLayout();
+  createConnections();
+}
+
+
+void
+ContinuousTab::repaintGlyph()
+{
+  sizeSelector_->applyToEngine(engine_);
+  
+  updateFromCurrentSubTab();
+  canvas_->repaint();
+}
+
+
+void
+ContinuousTab::reloadFont()
+{
+  currentGlyphCount_ = engine_->currentFontNumberOfGlyphs();
+  updateCurrentSubTab();
+  repaintGlyph();
+}
+
+
+void
+ContinuousTab::syncSettings()
+{
+}
+
+
+void
+ContinuousTab::changeTab()
+{
+  updateCurrentSubTab();
+  repaintGlyph();
+}
+
+
+void
+ContinuousTab::wheelNavigate(int steps)
+{
+  if (tabWidget_->currentIndex() == AllGlyphs)
+    allGlyphsTab_->setGlyphBeginindex(allGlyphsTab_->glyphBeginindex()
+                                      + steps);
+}
+
+
+void
+ContinuousTab::wheelResize(int steps)
+{
+  sizeSelector_->handleWheelResizeBySteps(steps);
+}
+
+
+void
+ContinuousTab::createLayout()
+{
+  canvas_ = new GlyphContinuous(this, engine_);
+  sizeSelector_ = new FontSizeSelector(this);
+  allGlyphsTab_ = new ContinousAllGlyphsTab(this);
+
+  tabWidget_ = new QTabWidget(this);
+  tabWidget_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
+  // Must be in sync with `Tabs` enum.
+  tabWidget_->addTab(allGlyphsTab_, tr("All Glyphs"));
+
+  mainLayout_ = new QVBoxLayout;
+  mainLayout_->addWidget(canvas_);
+  mainLayout_->addWidget(sizeSelector_);
+  mainLayout_->addWidget(tabWidget_);
+
+  setLayout(mainLayout_);
+}
+
+
+void
+ContinuousTab::createConnections()
+{
+  connect(tabWidget_, &QTabWidget::currentChanged,
+          this, &ContinuousTab::changeTab);
+
+  connect(allGlyphsTab_, &ContinousAllGlyphsTab::changed, 
+          this, &ContinuousTab::repaintGlyph);
+
+  connect(sizeSelector_, &FontSizeSelector::valueChanged,
+          this, &ContinuousTab::repaintGlyph);
+
+  connect(canvas_, &GlyphContinuous::wheelResize, 
+          this, &ContinuousTab::wheelResize);
+  connect(canvas_, &GlyphContinuous::wheelNavigate, 
+          this, &ContinuousTab::wheelNavigate);
+  connect(canvas_, &GlyphContinuous::displayingCountUpdated, 
+          allGlyphsTab_, &ContinousAllGlyphsTab::setDisplayingCount);
+}
+
+
+void
+ContinuousTab::setDefaults()
+{
+}
+
+
+void
+ContinuousTab::updateCurrentSubTab()
+{
+  switch (tabWidget_->currentIndex())
+  {
+  case AllGlyphs:
+    allGlyphsTab_->setGlyphCount(qBound(0, 
+                                        currentGlyphCount_,
+                                        INT_MAX));
+    allGlyphsTab_->setCharMaps(engine_->currentFontCharMaps());
+    break;
+  }
+}
+
+
+void
+ContinuousTab::updateFromCurrentSubTab()
+{
+  switch (tabWidget_->currentIndex())
+  {
+  case AllGlyphs:
+    // Begin index is selected from All Glyphs subtab,
+    // and Limit index is calculated by All Glyphs subtab
+    canvas_->setBeginIndex(allGlyphsTab_->glyphBeginindex());
+    canvas_->setLimitIndex(allGlyphsTab_->glyphLimitIndex());
+    canvas_->setMode(GlyphContinuous::AllGlyphs);
+    canvas_->setSubModeAllGlyphs(allGlyphsTab_->subMode());
+    canvas_->setCharMapIndex(allGlyphsTab_->charMapIndex());
+    break;
+  }
+}
+
+
+ContinousAllGlyphsTab::ContinousAllGlyphsTab(QWidget* parent)
+: QWidget(parent)
+{
+  createLayout();
+
+  QVector<CharMapInfo> tempCharMaps;
+  setCharMaps(tempCharMaps); // pass in an empty one
+
+  createConnections();
+}
+
+
+int
+ContinousAllGlyphsTab::glyphBeginindex()
+{
+  return indexSelector_->currentIndex();
+}
+
+
+int
+ContinousAllGlyphsTab::glyphLimitIndex()
+{
+  return glyphLimitIndex_;
+}
+
+
+GlyphContinuous::SubModeAllGlyphs
+ContinousAllGlyphsTab::subMode()
+{
+  return static_cast<GlyphContinuous::SubModeAllGlyphs>(
+           modeSelector_->currentIndex());
+}
+
+
+int
+ContinousAllGlyphsTab::charMapIndex()
+{
+  auto index = charMapSelector_->currentIndex() - 1;
+  if (index <= -1)
+    return -1;
+  if (index >= charMaps_.size())
+    return -1;
+  return index;
+}
+
+
+void
+ContinousAllGlyphsTab::setGlyphBeginindex(int index)
+{
+  indexSelector_->setCurrentIndex(index);
+  updateCharMapLimit();
+}
+
+
+void
+ContinousAllGlyphsTab::setDisplayingCount(int count)
+{
+  indexSelector_->setShowingCount(count);
+}
+
+
+#define EncodingRole (Qt::UserRole + 10)
+void
+ContinousAllGlyphsTab::setCharMaps(QVector<CharMapInfo>& charMaps)
+{
+  charMaps_ = charMaps;
+  int oldIndex = charMapSelector_->currentIndex();
+  unsigned oldEncoding = 0u;
+
+  // Using additional UserRole to store encoding id
+  auto oldEncodingV = charMapSelector_->itemData(oldIndex, EncodingRole);
+  if (oldEncodingV.isValid() && oldEncodingV.canConvert<unsigned>())
+  {
+    oldEncoding = oldEncodingV.value<unsigned>();
+  }
+
+  {
+    // suppress events during updating
+    QSignalBlocker selectorBlocker(charMapSelector_);
+
+    charMapSelector_->clear();
+    charMapSelector_->addItem(tr("Glyph Order"));
+    charMapSelector_->setItemData(0, 0u, EncodingRole);
+
+    int i = 0;
+    int newIndex = 0;
+    for (auto& map : charMaps)
+    {
+      charMapSelector_->addItem(tr("%1: %2")
+                                .arg(i)
+                                .arg(*map.encodingName));
+      auto encoding = static_cast<unsigned>(map.encoding);
+      charMapSelector_->setItemData(i, encoding, EncodingRole);
+
+      if (encoding == oldEncoding && i == oldIndex)
+        newIndex = i;
+    
+      i++;
+    }
+
+    // this shouldn't emit any event either, because force repainting
+    // will happen later, so embrace it into blocker block
+    charMapSelector_->setCurrentIndex(newIndex);
+  }
+
+  updateCharMapLimit();
+}
+
+
+void
+ContinousAllGlyphsTab::updateCharMapLimit()
+{
+  if (charMapSelector_->currentIndex() <= 0)
+    glyphLimitIndex_ = currentGlyphCount_;
+  else
+    glyphLimitIndex_
+      = charMaps_[charMapSelector_->currentIndex() - 1].maxIndex + 1;
+  indexSelector_->setMinMax(0, glyphLimitIndex_ - 1);
+}
+
+
+void
+ContinousAllGlyphsTab::createLayout()
+{
+  indexSelector_ = new GlyphIndexSelector(this);
+  indexSelector_->setSingleMode(false);
+  indexSelector_->setNumberRenderer([this](int index)
+                                    { return formatIndex(index); });
+
+  modeSelector_ = new QComboBox(this);
+  charMapSelector_ = new QComboBox(this);
+
+  // Note: in sync with the enum!!
+  modeSelector_->insertItem(GlyphContinuous::AG_AllGlyphs, tr("All Glyphs"));
+  modeSelector_->insertItem(GlyphContinuous::AG_Fancy, tr("Fancy"));
+  modeSelector_->insertItem(GlyphContinuous::AG_Stroked, tr("Stroked"));
+  modeSelector_->insertItem(GlyphContinuous::AG_Waterfall, tr("Waterfall"));
+  modeSelector_->setCurrentIndex(GlyphContinuous::AG_AllGlyphs);
+
+  modeLabel_ = new QLabel(tr("Mode:"), this);
+  charMapLabel_ = new QLabel(tr("Char Map:"), this);
+
+  layout_ = new QGridLayout;
+  layout_->addWidget(indexSelector_, 0, 0, 1, 2);
+  layout_->addWidget(modeLabel_, 1, 0);
+  layout_->addWidget(charMapLabel_, 2, 0);
+  layout_->addWidget(modeSelector_, 1, 1);
+  layout_->addWidget(charMapSelector_, 2, 1);
+
+  layout_->setColumnStretch(1, 1);
+
+  setLayout(layout_);
+}
+
+void
+ContinousAllGlyphsTab::createConnections()
+{
+  connect(indexSelector_, &GlyphIndexSelector::currentIndexChanged,
+          this, &ContinousAllGlyphsTab::changed);
+  connect(modeSelector_, QOverload<int>::of(&QComboBox::currentIndexChanged),
+          this, &ContinousAllGlyphsTab::changed);
+  connect(charMapSelector_, 
QOverload<int>::of(&QComboBox::currentIndexChanged),
+          this, &ContinousAllGlyphsTab::charMapChanged);
+}
+
+
+QString
+ContinousAllGlyphsTab::formatIndex(int index)
+{
+  if (charMapSelector_->currentIndex() <= 0) // glyph order
+    return QString::number(index);
+  return charMaps_[charMapSelector_->currentIndex() - 1]
+           .stringifyIndexShort(index);
+}
+
+
+void
+ContinousAllGlyphsTab::charMapChanged()
+{
+  int newIndex = charMapSelector_->currentIndex();
+  if (newIndex != lastCharMapIndex_)
+  {
+    if (newIndex <= 0 || charMaps_.size() <= newIndex - 1)
+      setGlyphBeginindex(0);
+    else if (charMaps_[newIndex - 1].maxIndex <= 20)
+      setGlyphBeginindex(charMaps_[newIndex - 1].maxIndex - 1);
+    else
+      setGlyphBeginindex(0x20);
+  }
+  updateCharMapLimit();
+
+  emit changed();
+
+  lastCharMapIndex_ = newIndex;
+}
+
+
+// end of continuous.cpp
diff --git a/src/ftinspect/panels/continuous.hpp 
b/src/ftinspect/panels/continuous.hpp
new file mode 100644
index 0000000..e13e76d
--- /dev/null
+++ b/src/ftinspect/panels/continuous.hpp
@@ -0,0 +1,124 @@
+// continuous.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include "abstracttab.hpp"
+#include "../widgets/customwidgets.hpp"
+#include "../widgets/glyphindexselector.hpp"
+#include "../widgets/fontsizeselector.hpp"
+#include "../rendering/graphicsdefault.hpp"
+#include "../rendering/glyphcontinuous.hpp"
+#include "../engine/engine.hpp"
+
+#include <QWidget>
+#include <QLabel>
+#include <QComboBox>
+#include <QVector>
+#include <QGridLayout>
+#include <QBoxLayout>
+
+class ContinousAllGlyphsTab;
+
+class ContinuousTab
+: public QWidget, public AbstractTab
+{
+  Q_OBJECT
+public:
+  ContinuousTab(QWidget* parent, Engine* engine);
+  ~ContinuousTab() override = default;
+
+  void repaintGlyph() override;
+  void reloadFont() override;
+  void syncSettings() override;
+  void setDefaults() override;
+
+  // Info about current font (glyph count, charmaps...) is flowed to subtab
+  // via `updateCurrentSubTab`.
+  // Settings and parameters (e.g. mode) are flowed from subtab to `this` via
+  // `updateFromCurrentSubTab`.
+  // SubTabs can notify `this` via signals, see `createConnections`
+  void updateCurrentSubTab();
+  void updateFromCurrentSubTab();
+
+private slots:
+  void changeTab();
+  void wheelNavigate(int steps);
+  void wheelResize(int steps);
+
+private:
+  Engine* engine_;
+
+  int currentGlyphCount_;
+  GlyphContinuous* canvas_;
+  
+  FontSizeSelector* sizeSelector_;
+
+  QTabWidget* tabWidget_;
+  ContinousAllGlyphsTab* allGlyphsTab_;
+
+  enum Tabs
+  {
+    AllGlyphs = 0
+  };
+
+  QVBoxLayout* mainLayout_;
+  
+  void createLayout();
+  void createConnections();
+};
+
+
+class ContinousAllGlyphsTab
+: public QWidget
+{
+  Q_OBJECT
+public:
+  explicit ContinousAllGlyphsTab(QWidget* parent);
+  ~ContinousAllGlyphsTab() override = default;
+
+  int glyphBeginindex();
+  int glyphLimitIndex();
+  GlyphContinuous::SubModeAllGlyphs subMode();
+
+  // -1: Glyph order, otherwise the char map index in the original list
+  int charMapIndex();
+  void setGlyphBeginindex(int index);
+
+  // This doesn't trigger immediate repaint
+  void setGlyphCount(int count) { currentGlyphCount_ = count; }
+  void setDisplayingCount(int count);
+
+  void setCharMaps(QVector<CharMapInfo>& charMaps);
+  // This doesn't trigger either.
+  void updateCharMapLimit();
+
+signals:
+  void changed();
+
+private:
+  int lastCharMapIndex_ = 0;
+  int currentGlyphCount_;
+  int glyphLimitIndex_ = 0;
+
+  GlyphIndexSelector* indexSelector_;
+  QComboBox* modeSelector_;
+  QComboBox* charMapSelector_;
+
+  QLabel* modeLabel_;
+  QLabel* charMapLabel_;
+
+  QGridLayout* layout_;
+
+  QVector<CharMapInfo> charMaps_;
+
+  void createLayout();
+  void createConnections();
+
+  QString formatIndex(int index);
+  void charMapChanged();
+};
+
+
+// end of continuous.hpp
diff --git a/src/ftinspect/panels/singular.cpp 
b/src/ftinspect/panels/singular.cpp
index bf09105..bf7dcc4 100644
--- a/src/ftinspect/panels/singular.cpp
+++ b/src/ftinspect/panels/singular.cpp
@@ -9,15 +9,14 @@
 
 
 SingularTab::SingularTab(QWidget* parent, Engine* engine)
-: QWidget(parent), engine_(engine)
+: QWidget(parent), engine_(engine),
+  graphicsDefault_(GraphicsDefault::deafultInstance())
 {
-  setGraphicsDefaults();
   createLayout();
   createConnections();
 
   currentGlyphIndex_ = 0;
   checkShowPoints();
-  checkUnits();
 }
 
 
@@ -97,30 +96,35 @@ SingularTab::drawGlyph()
       if (!engine_->antiAliasingEnabled())
         pixelMode = FT_PIXEL_MODE_MONO;
 
-      currentGlyphBitmapItem_ = new GlyphBitmap(outline,
-                                               engine_->ftLibrary(),
-                                               pixelMode,
-                                               monoColorTable_,
-                                               grayColorTable_);
+      currentGlyphBitmapItem_
+        = new GlyphBitmap(outline,
+          engine_->ftLibrary(),
+          pixelMode,
+          graphicsDefault_->monoColorTable,
+          graphicsDefault_->grayColorTable);
       glyphScene_->addItem(currentGlyphBitmapItem_);
     }
 
     if (showOutlinesCheckBox_->isChecked())
     {
-      currentGlyphOutlineItem_ = new GlyphOutline(outlinePen_, outline);
+      currentGlyphOutlineItem_ = new 
GlyphOutline(graphicsDefault_->outlinePen, 
+                                                  outline);
       glyphScene_->addItem(currentGlyphOutlineItem_);
     }
 
     if (showPointsCheckBox_->isChecked())
     {
-      currentGlyphPointsItem_ = new GlyphPoints(onPen_, offPen_, outline);
+      currentGlyphPointsItem_ = new GlyphPoints(graphicsDefault_->onPen,
+                                                graphicsDefault_->offPen,
+                                                outline);
       glyphScene_->addItem(currentGlyphPointsItem_);
 
       if (showPointNumbersCheckBox_->isChecked())
       {
-        currentGlyphPointNumbersItem_ = new GlyphPointNumbers(onPen_,
-                                                             offPen_,
-                                                             outline);
+        currentGlyphPointNumbersItem_
+          = new GlyphPointNumbers(graphicsDefault_->onPen,
+                                  graphicsDefault_->offPen,
+                                  outline);
         glyphScene_->addItem(currentGlyphPointNumbersItem_);
       }
     }
@@ -130,29 +134,6 @@ SingularTab::drawGlyph()
 }
 
 
-void
-SingularTab::checkUnits()
-{
-  int index = unitsComboBox_->currentIndex();
-
-  if (index == Units_px)
-  {
-    dpiLabel_->setEnabled(false);
-    dpiSpinBox_->setEnabled(false);
-    sizeDoubleSpinBox_->setSingleStep(1);
-    sizeDoubleSpinBox_->setValue(qRound(sizeDoubleSpinBox_->value()));
-  }
-  else
-  {
-    dpiLabel_->setEnabled(true);
-    dpiSpinBox_->setEnabled(true);
-    sizeDoubleSpinBox_->setSingleStep(0.5);
-  }
-
-  drawGlyph();
-}
-
-
 void
 SingularTab::checkShowPoints()
 {
@@ -219,11 +200,7 @@ SingularTab::wheelZoom(QWheelEvent* event)
 void
 SingularTab::wheelResize(QWheelEvent* event)
 {
-  int numSteps = event->angleDelta().y() / 120;
-  double sizeAfter = sizeDoubleSpinBox_->value() + numSteps * 0.5;
-  sizeAfter = std::max(sizeDoubleSpinBox_->minimum(),
-                       std::min(sizeAfter, sizeDoubleSpinBox_->maximum()));
-  sizeDoubleSpinBox_->setValue(sizeAfter);
+  sizeSelector_->handleWheelResizeFromGrid(event);
 }
 
 
@@ -246,7 +223,8 @@ SingularTab::createLayout()
   glyphView_->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
   glyphView_->setScene(glyphScene_);
 
-  gridItem_ = new Grid(glyphView_, gridPen_, axisPen_);
+  gridItem_ = new Grid(glyphView_, graphicsDefault_->gridPen, 
+                       graphicsDefault_->axisPen);
   glyphScene_->addItem(gridItem_);
 
   // Don't use QGraphicsTextItem: We want this hint to be anchored at the
@@ -271,27 +249,10 @@ SingularTab::createLayout()
   glyphIndexLabel_->setAttribute(Qt::WA_TransparentForMouseEvents, true);
   glyphNameLabel_->setAttribute(Qt::WA_TransparentForMouseEvents, true);
 
-  sizeLabel_ = new QLabel(tr("Size "), this);
-  sizeLabel_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
-  sizeDoubleSpinBox_ = new QDoubleSpinBox;
-  sizeDoubleSpinBox_->setAlignment(Qt::AlignRight);
-  sizeDoubleSpinBox_->setDecimals(1);
-  sizeDoubleSpinBox_->setRange(1, 500);
-  sizeLabel_->setBuddy(sizeDoubleSpinBox_);
-
   indexSelector_ = new GlyphIndexSelector(this);
   indexSelector_->setSingleMode(true);
 
-  unitsComboBox_ = new QComboBox(this);
-  unitsComboBox_->insertItem(Units_px, "px");
-  unitsComboBox_->insertItem(Units_pt, "pt");
-
-  dpiLabel_ = new QLabel(tr("DPI "), this);
-  dpiLabel_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
-  dpiSpinBox_ = new QSpinBox(this);
-  dpiSpinBox_->setAlignment(Qt::AlignRight);
-  dpiSpinBox_->setRange(10, 600);
-  dpiLabel_->setBuddy(dpiSpinBox_);
+  sizeSelector_ = new FontSizeSelector(this);
 
   zoomLabel_ = new QLabel(tr("Zoom Factor"), this);
   zoomLabel_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
@@ -310,12 +271,7 @@ SingularTab::createLayout()
 
   sizeLayout_ = new QHBoxLayout;
   sizeLayout_->addStretch(2);
-  sizeLayout_->addWidget(sizeLabel_);
-  sizeLayout_->addWidget(sizeDoubleSpinBox_);
-  sizeLayout_->addWidget(unitsComboBox_);
-  sizeLayout_->addStretch(1);
-  sizeLayout_->addWidget(dpiLabel_);
-  sizeLayout_->addWidget(dpiSpinBox_);
+  sizeLayout_->addWidget(sizeSelector_, 3);
   sizeLayout_->addStretch(1);
   sizeLayout_->addWidget(zoomLabel_);
   sizeLayout_->addWidget(zoomSpinBox_);
@@ -355,14 +311,10 @@ SingularTab::createLayout()
 void
 SingularTab::createConnections()
 {
+  connect(sizeSelector_, &FontSizeSelector::valueChanged,
+          this, &SingularTab::repaintGlyph);
   connect(indexSelector_, &GlyphIndexSelector::currentIndexChanged, 
           this, &SingularTab::setGlyphIndex);
-  connect(sizeDoubleSpinBox_, 
QOverload<double>::of(&QDoubleSpinBox::valueChanged),
-          this, &SingularTab::drawGlyph);
-  connect(unitsComboBox_, QOverload<int>::of(&QComboBox::currentIndexChanged),
-          this, &SingularTab::checkUnits);
-  connect(dpiSpinBox_, QOverload<int>::of(&QSpinBox::valueChanged),
-          this, &SingularTab::drawGlyph);
 
   connect(zoomSpinBox_, QOverload<int>::of(&QSpinBox::valueChanged),
           this, &SingularTab::zoom);
@@ -389,36 +341,6 @@ SingularTab::createConnections()
 }
 
 
-void
-SingularTab::setGraphicsDefaults()
-{
-  // color tables (with suitable opacity values) for converting
-  // FreeType's pixmaps to something Qt understands
-  monoColorTable_.append(QColor(Qt::transparent).rgba());
-  monoColorTable_.append(QColor(Qt::black).rgba());
-
-  for (int i = 0xFF; i >= 0; i--)
-    grayColorTable_.append(qRgba(i, i, i, 0xFF - i));
-
-  // XXX make this user-configurable
-
-  axisPen_.setColor(Qt::black);
-  axisPen_.setWidth(0);
-  blueZonePen_.setColor(QColor(64, 64, 255, 64)); // light blue
-  blueZonePen_.setWidth(0);
-  gridPen_.setColor(Qt::lightGray);
-  gridPen_.setWidth(0);
-  offPen_.setColor(Qt::darkGreen);
-  offPen_.setWidth(3);
-  onPen_.setColor(Qt::red);
-  onPen_.setWidth(3);
-  outlinePen_.setColor(Qt::red);
-  outlinePen_.setWidth(0);
-  segmentPen_.setColor(QColor(64, 255, 128, 64)); // light green
-  segmentPen_.setWidth(0);
-}
-
-
 void
 SingularTab::repaintGlyph()
 {
@@ -430,9 +352,7 @@ void
 SingularTab::reloadFont()
 {
   currentGlyphCount_ = engine_->currentFontNumberOfGlyphs();
-  indexSelector_->setMin(0);
-  indexSelector_->setMax(currentGlyphCount_);
-  indexSelector_->setCurrentIndex(indexSelector_->getCurrentIndex(), true);
+  indexSelector_->setMinMax(0, currentGlyphCount_);
   drawGlyph();
 }
 
@@ -440,13 +360,7 @@ SingularTab::reloadFont()
 void
 SingularTab::syncSettings()
 {
-  // Spinbox value cannot become negative
-  engine_->setDPI(static_cast<unsigned int>(dpiSpinBox_->value()));
-
-  if (unitsComboBox_->currentIndex() == Units_px)
-    engine_->setSizeByPixel(sizeDoubleSpinBox_->value());
-  else
-    engine_->setSizeByPoint(sizeDoubleSpinBox_->value());
+  sizeSelector_->applyToEngine(engine_);
 }
 
 
@@ -455,14 +369,11 @@ SingularTab::setDefaults()
 {
   currentGlyphIndex_ = 0;
 
-  sizeDoubleSpinBox_->setValue(20);
-  dpiSpinBox_->setValue(96);
   zoomSpinBox_->setValue(20);
   showBitmapCheckBox_->setChecked(true);
   showOutlinesCheckBox_->setChecked(true);
-
-  checkUnits();
-  indexSelector_->setCurrentIndex(indexSelector_->getCurrentIndex(), true);
+  
+  indexSelector_->setCurrentIndex(indexSelector_->currentIndex(), true);
   zoom();
 }
 
diff --git a/src/ftinspect/panels/singular.hpp 
b/src/ftinspect/panels/singular.hpp
index 88df922..88481f8 100644
--- a/src/ftinspect/panels/singular.hpp
+++ b/src/ftinspect/panels/singular.hpp
@@ -7,11 +7,13 @@
 #include "abstracttab.hpp"
 #include "../widgets/customwidgets.hpp"
 #include "../widgets/glyphindexselector.hpp"
+#include "../widgets/fontsizeselector.hpp"
 #include "../rendering/glyphbitmap.hpp"
 #include "../rendering/glyphoutline.hpp"
 #include "../rendering/glyphpointnumbers.hpp"
 #include "../rendering/glyphpoints.hpp"
 #include "../rendering/grid.hpp"
+#include "../rendering/graphicsdefault.hpp"
 #include "../engine/engine.hpp"
 #include "../models/ttsettingscomboboxmodel.hpp"
 
@@ -45,8 +47,7 @@ public:
 private slots:
   void setGlyphIndex(int);
   void drawGlyph();
-
-  void checkUnits();
+  
   void checkShowPoints();
 
   void zoom();
@@ -71,13 +72,9 @@ private:
   QLabel* mouseUsageHint_;
 
   GlyphIndexSelector* indexSelector_;
-  QLabel* dpiLabel_;
-  QLabel* sizeLabel_;
+  FontSizeSelector* sizeSelector_;
   QLabel* zoomLabel_;
-  QSpinBox* dpiSpinBox_;
   ZoomSpinBox* zoomSpinBox_;
-  QComboBox* unitsComboBox_;
-  QDoubleSpinBox* sizeDoubleSpinBox_;
   QPushButton* centerGridButton_;
 
   QLabel* glyphIndexLabel_;
@@ -94,26 +91,10 @@ private:
   QGridLayout* glyphOverlayLayout_;
   QHBoxLayout* glyphOverlayIndexLayout_;
 
-  QPen axisPen_;
-  QPen blueZonePen_;
-  QPen gridPen_;
-  QPen offPen_;
-  QPen onPen_;
-  QPen outlinePen_;
-  QPen segmentPen_;
-
-  QVector<QRgb> grayColorTable_;
-  QVector<QRgb> monoColorTable_;
-
-  enum Units
-  {
-    Units_px,
-    Units_pt
-  };
+  GraphicsDefault* graphicsDefault_;
 
   void createLayout();
   void createConnections();
-  void setGraphicsDefaults();
   
   void updateGrid();
 };
diff --git a/src/ftinspect/rendering/glyphbitmap.cpp 
b/src/ftinspect/rendering/glyphbitmap.cpp
index dcef3ee..20bf92e 100644
--- a/src/ftinspect/rendering/glyphbitmap.cpp
+++ b/src/ftinspect/rendering/glyphbitmap.cpp
@@ -5,6 +5,8 @@
 
 #include "glyphbitmap.hpp"
 
+#include "renderutils.hpp"
+
 #include <cmath>
 #include <QPainter>
 #include <QStyleOptionGraphicsItem>
@@ -21,23 +23,8 @@ GlyphBitmap::GlyphBitmap(FT_Outline* outline,
   grayColorTable_(grayColorTbl)
 {
   // make a copy of the outline since we are going to manipulate it
-  FT_Outline_New(library_,
-                 static_cast<unsigned int>(outline->n_points),
-                 outline->n_contours,
-                 &transformed_);
-  FT_Outline_Copy(outline, &transformed_);
-
   FT_BBox cbox;
-  FT_Outline_Get_CBox(outline, &cbox);
-
-  cbox.xMin &= ~63;
-  cbox.yMin &= ~63;
-  cbox.xMax = (cbox.xMax + 63) & ~63;
-  cbox.yMax = (cbox.yMax + 63) & ~63;
-
-  // we shift the outline to the origin for rendering later on
-  FT_Outline_Translate(&transformed_, -cbox.xMin, -cbox.yMin);
-
+  transformed_ = transformOutlineToOrigin(lib, outline, &cbox);
   boundingRect_.setCoords(cbox.xMin / 64, -cbox.yMax / 64,
                   cbox.xMax / 64, -cbox.yMin / 64);
 }
diff --git a/src/ftinspect/rendering/glyphcontinuous.cpp 
b/src/ftinspect/rendering/glyphcontinuous.cpp
new file mode 100644
index 0000000..524af4c
--- /dev/null
+++ b/src/ftinspect/rendering/glyphcontinuous.cpp
@@ -0,0 +1,212 @@
+// glyphcontinuous.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "glyphcontinuous.hpp"
+
+#include <cmath>
+#include <QPainter>
+#include <QWheelEvent>
+
+#include "../engine/engine.hpp"
+#include "../rendering/renderutils.hpp"
+
+
+GlyphContinuous::GlyphContinuous(QWidget* parent, Engine* engine)
+: QWidget(parent), engine_(engine)
+{
+  setAcceptDrops(false);
+  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+  graphicsDefault_ = GraphicsDefault::deafultInstance();
+}
+
+
+void
+GlyphContinuous::paintEvent(QPaintEvent* event)
+{
+  QPainter painter;
+  painter.begin(this);
+  painter.fillRect(rect(), Qt::white);
+
+  if (limitIndex_ > 0)
+  {
+    prePaint();
+
+    switch (mode_)
+    {
+    case AllGlyphs:
+      switch (modeAG_)
+      {
+      case AG_AllGlyphs:
+        paintAGAllGlyphs(&painter);
+        break;
+        // TODO more modes
+      case AG_Fancy:
+        break;
+      case AG_Stroked:
+        break;
+      case AG_Waterfall:
+        break;
+      }
+      break;
+    case TextString:
+      break;
+    }
+    emit displayingCountUpdated(displayingCount_);
+  }
+
+  painter.end();
+}
+
+
+void
+GlyphContinuous::wheelEvent(QWheelEvent* event)
+{
+  int numSteps = event->angleDelta().y() / 120;
+  if (event->modifiers() & Qt::ShiftModifier)
+    emit wheelResize(numSteps);
+  else if (event->modifiers() == 0)
+    emit wheelNavigate(-numSteps);
+}
+
+
+void
+GlyphContinuous::paintAGAllGlyphs(QPainter* painter)
+{
+  for (int i = beginIndex_; i < limitIndex_; i++)
+  {
+    unsigned index = i;
+    if (charMapIndex_ >= 0)
+      index = engine_->glyphIndexFromCharCode(i, charMapIndex_);
+
+    if (!paintChar(painter, index))
+      break;
+
+    displayingCount_++;
+  }
+}
+
+
+void
+GlyphContinuous::prePaint()
+{
+  displayingCount_ = 0;
+  engine_->reloadFont();
+  metrics_ = engine_->currentFontMetrics();
+  x_ = 0;
+  // See ftview.c:42
+  y_ = ((metrics_.ascender - metrics_.descender + 63) >> 6) + 4;
+  stepY_ = ((metrics_.height + 63) >> 6) + 4;
+}
+
+
+bool
+GlyphContinuous::paintChar(QPainter* painter,
+                           int index)
+{
+  auto glyph = engine_->loadGlyphWithoutUpdate(index);
+  if (!glyph)
+    return false;
+
+  // ftview.c:557
+  int width = glyph->advance.x ? glyph->advance.x >> 16
+                               : metrics_.y_ppem / 2;
+
+  if (!checkFitX(x_ + width))
+  {
+    x_ = 0;
+    y_ += stepY_;
+
+    if (!checkFitY(y_))
+      return false;
+  }
+
+  x_++; // extra space
+  if (glyph->advance.x == 0)
+  {
+    // Draw a red square to indicate
+      painter->fillRect(x_, y_ - width, width, width,
+                        Qt::red);
+    x_ += width;
+  }
+
+  // The real drawing part
+  // XXX: this is different from what's being done in
+  // `ftcommon.c`:FTDemo_Draw_Slot: is this correct??
+
+  // First translate the outline
+
+  if (glyph->format != FT_GLYPH_FORMAT_OUTLINE)
+    return true; // XXX only outline is supported - need to impl others later
+
+  FT_BBox cbox;
+  // Don't forget to free this when returning
+  auto outline = transformOutlineToOrigin(
+                   engine_->ftLibrary(),
+                   &reinterpret_cast<FT_OutlineGlyph>(glyph)->outline,
+                   &cbox);
+  
+  auto outlineWidth = (cbox.xMax - cbox.xMin) / 64;
+  auto outlineHeight = (cbox.yMax - cbox.yMin) / 64;
+
+  // Then convert to bitmap
+  FT_Bitmap bitmap;
+  QImage::Format format = QImage::Format_Indexed8;
+  auto aaEnabled = engine_->antiAliasingEnabled();
+
+  // TODO cover LCD and color
+  if (!aaEnabled)
+    format = QImage::Format_Mono;
+
+  // TODO optimization: reuse QImage?
+  QImage image(QSize(outlineWidth, outlineHeight), format);
+
+  if (!aaEnabled)
+    image.setColorTable(graphicsDefault_->monoColorTable);
+  else
+    image.setColorTable(graphicsDefault_->grayColorTable);
+
+  image.fill(0);
+
+  bitmap.rows = static_cast<unsigned int>(outlineHeight);
+  bitmap.width = static_cast<unsigned int>(outlineWidth);
+  bitmap.buffer = image.bits();
+  bitmap.pitch = image.bytesPerLine();
+  bitmap.pixel_mode = aaEnabled ? FT_PIXEL_MODE_GRAY : FT_PIXEL_MODE_MONO;
+
+  FT_Error error = FT_Outline_Get_Bitmap(engine_->ftLibrary(),
+                                         &outline,
+                                         &bitmap);
+  if (error)
+  {
+    // XXX error handling
+    FT_Outline_Done(engine_->ftLibrary(), &outline);
+    return true;
+  }
+
+  painter->drawImage(
+      QPoint(x_ + cbox.xMin / 64, y_ + (-cbox.yMax / 64)),
+      image.convertToFormat(QImage::Format_ARGB32_Premultiplied));
+
+  x_ += width;
+
+  FT_Outline_Done(engine_->ftLibrary(), &outline);
+  return true;
+}
+
+
+bool
+GlyphContinuous::checkFitX(int x)
+{
+  return x < width() - 3;
+}
+
+
+bool
+GlyphContinuous::checkFitY(int y)
+{
+  return y < height() - 3;
+}
+
+
+// end of glyphcontinuous.cpp
diff --git a/src/ftinspect/rendering/glyphcontinuous.hpp 
b/src/ftinspect/rendering/glyphcontinuous.hpp
new file mode 100644
index 0000000..9a87e7e
--- /dev/null
+++ b/src/ftinspect/rendering/glyphcontinuous.hpp
@@ -0,0 +1,77 @@
+// glyphcontinuous.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include "graphicsdefault.hpp"
+#include <QWidget>
+#include <freetype/freetype.h>
+
+class Engine;
+class GlyphContinuous
+: public QWidget
+{
+  Q_OBJECT
+public:
+  GlyphContinuous(QWidget* parent, Engine* engine);
+  ~GlyphContinuous() override = default;
+
+  enum Mode : int
+  {
+    AllGlyphs,
+    TextString
+  };
+
+  enum SubModeAllGlyphs : int
+  {
+    AG_AllGlyphs,
+    AG_Fancy,
+    AG_Stroked,
+    AG_Waterfall
+  };
+
+  int displayingCount() { return displayingCount_; }
+
+  // all those setters don't trigger repaint.
+  void setBeginIndex(int index) { beginIndex_ = index; }
+  void setLimitIndex(int index) { limitIndex_ = index; }
+  void setCharMapIndex(int index) { charMapIndex_ = index; }
+  void setMode(Mode mode) { mode_ = mode; }
+  void setSubModeAllGlyphs(SubModeAllGlyphs modeAg) { modeAG_ = modeAg; }
+
+signals:
+  void wheelNavigate(int steps);
+  void wheelResize(int steps);
+  void displayingCountUpdated(int newCount);
+
+protected:
+  void paintEvent(QPaintEvent* event) override;
+  void wheelEvent(QWheelEvent* event) override;
+
+private:
+  Engine* engine_;
+  GraphicsDefault* graphicsDefault_;
+
+  int beginIndex_;
+  int limitIndex_;
+  int charMapIndex_;
+  Mode mode_ = AllGlyphs;
+  SubModeAllGlyphs modeAG_ = AG_AllGlyphs;
+
+  int displayingCount_ = 0;
+  FT_Size_Metrics metrics_;
+  int x_ = 0, y_ = 0;
+  int stepY_ = 0;
+
+  void paintAGAllGlyphs(QPainter* painter);
+  void prePaint();
+  // return if there's enough space to paint the current char
+  bool paintChar(QPainter* painter, int index);
+
+  bool checkFitX(int x);
+  bool checkFitY(int y);
+};
+
+
+// end of glyphcontinuous.hpp
diff --git a/src/ftinspect/rendering/graphicsdefault.cpp 
b/src/ftinspect/rendering/graphicsdefault.cpp
new file mode 100644
index 0000000..4e80b22
--- /dev/null
+++ b/src/ftinspect/rendering/graphicsdefault.cpp
@@ -0,0 +1,48 @@
+// graphicsdefault.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "graphicsdefault.hpp"
+
+GraphicsDefault* GraphicsDefault::instance_ = NULL;
+
+GraphicsDefault::GraphicsDefault()
+{
+  // color tables (with suitable opacity values) for converting
+  // FreeType's pixmaps to something Qt understands
+  monoColorTable.append(QColor(Qt::transparent).rgba());
+  monoColorTable.append(QColor(Qt::black).rgba());
+
+  for (int i = 0xFF; i >= 0; i--)
+    grayColorTable.append(qRgba(i, i, i, 0xFF - i));
+
+  // XXX make this user-configurable
+
+  axisPen.setColor(Qt::black);
+  axisPen.setWidth(0);
+  blueZonePen.setColor(QColor(64, 64, 255, 64)); // light blue
+  blueZonePen.setWidth(0);
+  gridPen.setColor(Qt::lightGray);
+  gridPen.setWidth(0);
+  offPen.setColor(Qt::darkGreen);
+  offPen.setWidth(3);
+  onPen.setColor(Qt::red);
+  onPen.setWidth(3);
+  outlinePen.setColor(Qt::red);
+  outlinePen.setWidth(0);
+  segmentPen.setColor(QColor(64, 255, 128, 64)); // light green
+  segmentPen.setWidth(0);
+}
+
+
+GraphicsDefault*
+GraphicsDefault::deafultInstance()
+{
+  if (!instance_)
+    instance_ = new GraphicsDefault;
+
+  return instance_;
+}
+
+
+// end of graphicsdefault.cpp
diff --git a/src/ftinspect/rendering/graphicsdefault.hpp 
b/src/ftinspect/rendering/graphicsdefault.hpp
new file mode 100644
index 0000000..4b8e588
--- /dev/null
+++ b/src/ftinspect/rendering/graphicsdefault.hpp
@@ -0,0 +1,34 @@
+// graphicsdefault.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include <QVector>
+#include <QRgb>
+#include <QPen>
+
+// This is default graphics objects fed into render functions.
+struct GraphicsDefault
+{
+  QVector<QRgb> grayColorTable;
+  QVector<QRgb> monoColorTable;
+
+  QPen axisPen;
+  QPen blueZonePen;
+  QPen gridPen;
+  QPen offPen;
+  QPen onPen;
+  QPen outlinePen;
+  QPen segmentPen;
+
+  GraphicsDefault();
+
+  static GraphicsDefault* deafultInstance();
+
+private:
+  static GraphicsDefault* instance_;
+};
+
+
+// end of graphicsdefault.hpp
diff --git a/src/ftinspect/rendering/renderutils.cpp 
b/src/ftinspect/rendering/renderutils.cpp
new file mode 100644
index 0000000..9866db8
--- /dev/null
+++ b/src/ftinspect/rendering/renderutils.cpp
@@ -0,0 +1,34 @@
+// renderutils.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "renderutils.hpp"
+
+FT_Outline
+transformOutlineToOrigin(FT_Library library, 
+                         FT_Outline* outline,
+                         FT_BBox* outControlBox)
+{
+  FT_Outline transformed;
+  FT_Outline_New(library,
+                 static_cast<unsigned int>(outline->n_points),
+                 outline->n_contours, &transformed);
+  FT_Outline_Copy(outline, &transformed);
+
+  FT_BBox cbox;
+  FT_Outline_Get_CBox(outline, &cbox);
+
+  cbox.xMin &= ~63;
+  cbox.yMin &= ~63;
+  cbox.xMax = (cbox.xMax + 63) & ~63;
+  cbox.yMax = (cbox.yMax + 63) & ~63;
+  // we shift the outline to the origin for rendering later on
+  FT_Outline_Translate(&transformed, -cbox.xMin, -cbox.yMin);
+
+  if (outControlBox)
+    *outControlBox = cbox;
+  return transformed;
+}
+
+
+// end of renderutils.cpp
diff --git a/src/ftinspect/rendering/renderutils.hpp 
b/src/ftinspect/rendering/renderutils.hpp
new file mode 100644
index 0000000..ba4caf4
--- /dev/null
+++ b/src/ftinspect/rendering/renderutils.hpp
@@ -0,0 +1,15 @@
+// renderutils.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include <freetype/ftoutln.h>
+
+// The constructed `outline` must be freed by the caller
+FT_Outline transformOutlineToOrigin(FT_Library library, 
+                                    FT_Outline* outline,
+                                    FT_BBox* outControlBox);
+
+
+// end of renderutils.hpp
diff --git a/src/ftinspect/widgets/fontsizeselector.cpp 
b/src/ftinspect/widgets/fontsizeselector.cpp
new file mode 100644
index 0000000..ed04970
--- /dev/null
+++ b/src/ftinspect/widgets/fontsizeselector.cpp
@@ -0,0 +1,147 @@
+// fontsizeselector.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "fontsizeselector.hpp"
+
+#include "../engine/engine.hpp"
+
+FontSizeSelector::FontSizeSelector(QWidget* parent)
+: QWidget(parent)
+{
+  createLayout();
+  createConnections();
+  setDefaults();
+}
+
+
+double
+FontSizeSelector::selectedSize()
+{
+  return sizeDoubleSpinBox_->value();
+}
+
+
+FontSizeSelector::Units
+FontSizeSelector::selectedUnit()
+{
+  return static_cast<Units>(unitsComboBox_->currentIndex());
+}
+
+
+void
+FontSizeSelector::applyToEngine(Engine* engine)
+{
+  // Spinbox value cannot become negative
+  engine->setDPI(dpiSpinBox_->value());
+
+  if (unitsComboBox_->currentIndex() == Units_px)
+    engine->setSizeByPixel(sizeDoubleSpinBox_->value());
+  else
+    engine->setSizeByPoint(sizeDoubleSpinBox_->value());
+}
+
+
+void
+FontSizeSelector::handleWheelResizeBySteps(int steps)
+{
+  double sizeAfter = sizeDoubleSpinBox_->value() + steps * 0.5;
+  sizeAfter = std::max(sizeDoubleSpinBox_->minimum(),
+                       std::min(sizeAfter, sizeDoubleSpinBox_->maximum()));
+  sizeDoubleSpinBox_->setValue(sizeAfter);
+}
+
+
+void
+FontSizeSelector::handleWheelResizeFromGrid(QWheelEvent* event)
+{
+  int numSteps = event->angleDelta().y() / 120;
+  handleWheelResizeBySteps(numSteps);
+}
+
+
+void
+FontSizeSelector::checkUnits()
+{
+  int index = unitsComboBox_->currentIndex();
+
+  if (index == Units_px)
+  {
+    dpiLabel_->setEnabled(false);
+    dpiSpinBox_->setEnabled(false);
+    sizeDoubleSpinBox_->setSingleStep(1);
+
+    QSignalBlocker blocker(sizeDoubleSpinBox_);
+    sizeDoubleSpinBox_->setValue(qRound(sizeDoubleSpinBox_->value()));
+  }
+  else
+  {
+    dpiLabel_->setEnabled(true);
+    dpiSpinBox_->setEnabled(true);
+    sizeDoubleSpinBox_->setSingleStep(0.5);
+  }
+
+  emit valueChanged();
+}
+
+
+void
+FontSizeSelector::createLayout()
+{
+  sizeLabel_ = new QLabel(tr("Size "), this);
+  sizeLabel_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
+  sizeDoubleSpinBox_ = new QDoubleSpinBox;
+  sizeDoubleSpinBox_->setAlignment(Qt::AlignRight);
+  sizeDoubleSpinBox_->setDecimals(1);
+  sizeDoubleSpinBox_->setRange(1, 500);
+  sizeLabel_->setBuddy(sizeDoubleSpinBox_);
+
+  unitsComboBox_ = new QComboBox(this);
+  unitsComboBox_->insertItem(Units_px, "px");
+  unitsComboBox_->insertItem(Units_pt, "pt");
+
+  dpiLabel_ = new QLabel(tr("DPI "), this);
+  dpiLabel_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
+  dpiSpinBox_ = new QSpinBox(this);
+  dpiSpinBox_->setAlignment(Qt::AlignRight);
+  dpiSpinBox_->setRange(10, 600);
+  dpiLabel_->setBuddy(dpiSpinBox_);
+
+  layout_ = new QHBoxLayout;
+
+  layout_->addStretch(1);
+  layout_->addWidget(sizeLabel_);
+  layout_->addWidget(sizeDoubleSpinBox_);
+  layout_->addWidget(unitsComboBox_);
+  layout_->addStretch(1);
+  layout_->addWidget(dpiLabel_);
+  layout_->addWidget(dpiSpinBox_);
+  layout_->addStretch(1);
+
+  setLayout(layout_);
+  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
+}
+
+
+void
+FontSizeSelector::createConnections()
+{
+  connect(sizeDoubleSpinBox_, 
QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+          this, &FontSizeSelector::valueChanged);
+  connect(unitsComboBox_, QOverload<int>::of(&QComboBox::currentIndexChanged),
+          this, &FontSizeSelector::checkUnits);
+  connect(dpiSpinBox_, QOverload<int>::of(&QSpinBox::valueChanged),
+          this, &FontSizeSelector::valueChanged);
+}
+
+
+void
+FontSizeSelector::setDefaults()
+{
+  sizeDoubleSpinBox_->setValue(20);
+  dpiSpinBox_->setValue(96);
+  checkUnits();
+}
+
+
+// end of fontsizeselector.cpp
diff --git a/src/ftinspect/widgets/fontsizeselector.hpp 
b/src/ftinspect/widgets/fontsizeselector.hpp
new file mode 100644
index 0000000..581c860
--- /dev/null
+++ b/src/ftinspect/widgets/fontsizeselector.hpp
@@ -0,0 +1,58 @@
+// fontsizeselector.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include <QComboBox>
+#include <QDoubleSpinBox>
+#include <QLabel>
+#include <QWidget>
+#include <QBoxLayout>
+#include <QWheelEvent>
+
+class Engine;
+class FontSizeSelector : public QWidget
+{
+  Q_OBJECT
+
+public:
+  FontSizeSelector(QWidget* parent);
+  ~FontSizeSelector() override = default;
+
+  enum Units : int
+  {
+    Units_px,
+    Units_pt
+  };
+
+  double selectedSize();
+  Units selectedUnit();
+
+  void applyToEngine(Engine* engine);
+  void handleWheelResizeBySteps(int steps);
+  void handleWheelResizeFromGrid(QWheelEvent* event);
+
+signals:
+  void valueChanged();
+
+private slots:
+  void checkUnits();
+
+private:
+  QLabel* sizeLabel_;
+  QLabel* dpiLabel_;
+
+  QDoubleSpinBox* sizeDoubleSpinBox_;
+  QComboBox* unitsComboBox_;
+  QSpinBox* dpiSpinBox_;
+
+  QHBoxLayout* layout_;
+
+  void createLayout();
+  void createConnections();
+  void setDefaults();
+};
+
+
+// end of fontsizeselector.hpp
diff --git a/src/ftinspect/widgets/glyphindexselector.cpp 
b/src/ftinspect/widgets/glyphindexselector.cpp
index 13697f4..d926f53 100644
--- a/src/ftinspect/widgets/glyphindexselector.cpp
+++ b/src/ftinspect/widgets/glyphindexselector.cpp
@@ -6,33 +6,33 @@
 
 #include "../uihelper.hpp"
 
+#include <climits>
+
 GlyphIndexSelector::GlyphIndexSelector(QWidget* parent)
 : QWidget(parent)
 {
+  numberRenderer_ = &GlyphIndexSelector::renderNumberDefault;
+
   createLayout();
   createConnections();
+  showingCount_ = 0;
 }
 
 
 void
-GlyphIndexSelector::setMin(int min)
+GlyphIndexSelector::setMinMax(int min,
+                              int max)
 {
+  // Don't emit events during setting
+  auto eventState = blockSignals(true);
   indexSpinBox_->setMinimum(min);
+  indexSpinBox_->setMaximum(qBound(0, max, INT_MAX));
   indexSpinBox_->setValue(qBound(indexSpinBox_->minimum(),
                                  indexSpinBox_->value(),
                                  indexSpinBox_->maximum()));
-  // spinBoxChanged will be automatically called
-}
+  blockSignals(eventState);
 
-
-void
-GlyphIndexSelector::setMax(int max)
-{
-  indexSpinBox_->setMaximum(max);
-  indexSpinBox_->setValue(qBound(indexSpinBox_->minimum(),
-                                 indexSpinBox_->value(),
-                                 indexSpinBox_->maximum()));
-  // spinBoxChanged will be automatically called
+  updateLabel();
 }
 
 
@@ -55,26 +55,41 @@ GlyphIndexSelector::setSingleMode(bool singleMode)
 void
 GlyphIndexSelector::setCurrentIndex(int index, bool forceUpdate)
 {
+  // to avoid unnecessary update, if force update is enabled
+  // then the `setValue` shouldn't trigger update signal from `this`
+  // but we still need `updateLabel`, so block `this` only
+  auto state = blockSignals(forceUpdate);
   indexSpinBox_->setValue(index);
-  updateLabel();
+  blockSignals(state);
+  
   if (forceUpdate)
     emit currentIndexChanged(indexSpinBox_->value());
 }
 
 
 int
-GlyphIndexSelector::getCurrentIndex()
+GlyphIndexSelector::currentIndex()
 {
   return indexSpinBox_->value();
 }
 
 
+void
+GlyphIndexSelector::setNumberRenderer(std::function<QString(int)> renderer)
+{
+  numberRenderer_ = std::move(renderer);
+}
+
+
 void
 GlyphIndexSelector::adjustIndex(int delta)
 {
-  indexSpinBox_->setValue(qBound(indexSpinBox_->minimum(),
-                                 indexSpinBox_->value() + delta,
-                                 indexSpinBox_->maximum()));
+  {
+    QSignalBlocker blocker(this);
+    indexSpinBox_->setValue(qBound(indexSpinBox_->minimum(),
+                                   indexSpinBox_->value() + delta,
+                                   indexSpinBox_->maximum()));
+  }
   emitValueChanged();
 }
 
@@ -92,13 +107,17 @@ GlyphIndexSelector::updateLabel()
 {
   if (singleMode_)
     indexLabel_->setText(QString("%1\nLimit: %2")
-                             .arg(indexSpinBox_->value())
-                             .arg(indexSpinBox_->maximum()));
+                           .arg(numberRenderer_(indexSpinBox_->value()))
+                           .arg(numberRenderer_(indexSpinBox_->maximum())));
   else
-    indexLabel_->setText(QString("%1~%2\nCount: %3\nLimit: %4")
-                             .arg(indexSpinBox_->value())
-                             .arg(indexSpinBox_->value() + showingCount_ - 1)
-                             .arg(showingCount_, indexSpinBox_->maximum()));
+    indexLabel_->setText(
+      QString("%1~%2\nCount: %3\nLimit: %4")
+        .arg(numberRenderer_(indexSpinBox_->value()))
+        .arg(numberRenderer_(
+          qBound(indexSpinBox_->value(),
+                 indexSpinBox_->value() + showingCount_ - 1, INT_MAX)))
+        .arg(showingCount_)
+        .arg(numberRenderer_(indexSpinBox_->maximum())));
 }
 
 
@@ -121,9 +140,10 @@ GlyphIndexSelector::createLayout()
   indexSpinBox_->setButtonSymbols(QAbstractSpinBox::NoButtons);
   indexSpinBox_->setRange(0, 0);
   indexSpinBox_->setFixedWidth(80);
-  indexSpinBox_->setWrapping(true);
+  indexSpinBox_->setWrapping(false);
 
   indexLabel_ = new QLabel("0\nCount: 0\nLimit: 0");
+  indexLabel_->setMinimumWidth(200);
 
   setButtonNarrowest(toStartButton_);
   setButtonNarrowest(toM1000Button_);
@@ -155,6 +175,7 @@ GlyphIndexSelector::createLayout()
   navigationLayout_->addWidget(indexLabel_);
   navigationLayout_->addStretch(3);
 
+  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
   setLayout(navigationLayout_);
 }
 
@@ -201,4 +222,11 @@ GlyphIndexSelector::createConnections()
 }
 
 
+QString
+GlyphIndexSelector::renderNumberDefault(int i)
+{
+  return QString::number(i);
+}
+
+
 // end of glyphindexselector.cpp
diff --git a/src/ftinspect/widgets/glyphindexselector.hpp 
b/src/ftinspect/widgets/glyphindexselector.hpp
index ddd5ecf..17a531b 100644
--- a/src/ftinspect/widgets/glyphindexselector.hpp
+++ b/src/ftinspect/widgets/glyphindexselector.hpp
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <functional>
 #include <QWidget>
 #include <QPushButton>
 #include <QSpinBox>
@@ -19,13 +20,15 @@ public:
   GlyphIndexSelector(QWidget* parent);
   ~GlyphIndexSelector() override = default;
 
-  void setMin(int min);
-  void setMax(int max);
+  // Will never trigger repaint!
+  void setMinMax(int min, int max);
   void setShowingCount(int showingCount);
   void setSingleMode(bool singleMode);
 
   void setCurrentIndex(int index, bool forceUpdate = false);
-  int getCurrentIndex();
+  int currentIndex();
+
+  void setNumberRenderer(std::function<QString(int)> renderer);
 
 signals:
   void currentIndexChanged(int index);
@@ -38,6 +41,7 @@ private slots:
 private:
   bool singleMode_ = true;
   int showingCount_;
+  std::function<QString(int)> numberRenderer_;
 
   // min, max and current status are held by `indexSpinBox_`
 
@@ -61,6 +65,8 @@ private:
 
   void createLayout();
   void createConnections();
+
+  static QString renderNumberDefault(int i);
 };
 
 



reply via email to

[Prev in Thread] Current Thread [Next in Thread]