freetype-commit
[Top][All Lists]
Advanced

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

[freetype2-demos] master eb1faf5 10/41: [ftinspect] Add the `SingularTab


From: Werner Lemberg
Subject: [freetype2-demos] master eb1faf5 10/41: [ftinspect] Add the `SingularTab` and related widgets.
Date: Mon, 3 Oct 2022 11:27:01 -0400 (EDT)

branch: master
commit eb1faf5922ee15391d9751498a9094dcd7b9f58d
Author: Charlie Jiang <w@chariri.moe>
Commit: Werner Lemberg <wl@gnu.org>

    [ftinspect] Add the `SingularTab` and related widgets.
    
    This introduces the new singular tab. However, because the new tab heavily
    depend on the new engine structure, it's current not functional. No bitmap
    or outline will be displayed. This would be fixed after the `Engine` was
    refactored in the next commit.
    
    The new singular tab has the size and glyph index selector moved out as
    modular widgets to be reused.
    
    The new scroll and shortcut behaviours are introduced in this commit, which
    depend on scroll events introduced in the custom `QGraphicsViewx`.
    
    The infinity panning of grid is implemented mainly via
    `SingularTab::updateGrid` and `Grid::updateRect`.
    
    This commit is introducing new features since it would be unfavorable to
    "backport" old version of glyph components, and add those new features in
    future commits - this is way too complex.
    
    * src/ftinspect/engine/engine.cpp, src/ftinspect/engine/engine.hpp:
      Add `currentFontNumberOfGlyphs` and `dpi` functions.
    
    * src/ftinspect/widgets/fontsizeselector.cpp,
      src/ftinspect/widgets/fontsizeselector.hpp:
      This is the new font size selector to replace the old size/DPI/zoom boxes.
      This widget is capable of handling wheel and key events delegated from
      other widgets.
      The support for fixed sizes and bitmap-only font is not yet added.
    
    * src/ftinspect/widgets/glyphindexselector.cpp,
      src/ftinspect/widgets/glyphindexselector.hpp:
      This is the new glyph index selector to replace the old navi buttons.
      This selector is aware of index min/max, consists of a group of navi
      buttons, a text box (actually a spin box without buttons) to directly
      input glyph index.
    
    * src/ftinspect/glyphcomponents/graphicsdefault.cpp,
      src/ftinspect/glyphcomponents/graphicsdefault.hpp:
      This struct contains all default graphical settings (mainly for singular
      view, e.g. the grid line color).
    
    * src/ftinspect/maingui.cpp, src/ftinspect/maingui.hpp:
      Add the new tab into the main window.
    
    * src/ftinspect/glyphcomponents/glyphbitmap.cpp,
      src/ftinspect/glyphcomponents/glyphbitmap.hpp:
      This will now delegate rendering to the engine instead of doing rendering
      itself. However, since the rendering part of the `Engine` is not
      implemented, code initializing `image_` is left commented.
      Also add another constructor for initializing directly from a `QImage`.
    
    * src/ftinspect/glyphcomponents/glyphoutline.cpp,
      src/ftinspect/glyphcomponents/glyphoutline.hpp,
      src/ftinspect/glyphcomponents/glyphpointnumbers.cpp,
      src/ftinspect/glyphcomponents/glyphpointnumbers.hpp,
      src/ftinspect/glyphcomponents/glyphpoints.cpp,
      src/ftinspect/glyphcomponents/glyphpoints.hpp:
      Constructors of those items now accept a `FT_Glyph` instead of
      `FT_Outline`. The conversion is done inside the view, and the view won't
      be displayed if the glyph isn't outline glyph.
      This simplifies the code of `SingularTab`.
    
    * src/ftinspect/CMakeLists.txt, src/ftinspect/meson.build: Updated.
---
 src/ftinspect/CMakeLists.txt                       |   4 +
 src/ftinspect/engine/engine.cpp                    |   1 +
 src/ftinspect/engine/engine.hpp                    |   6 +
 src/ftinspect/glyphcomponents/glyphbitmap.cpp      | 102 ++---
 src/ftinspect/glyphcomponents/glyphbitmap.hpp      |  27 +-
 src/ftinspect/glyphcomponents/glyphoutline.cpp     |  16 +-
 src/ftinspect/glyphcomponents/glyphoutline.hpp     |   7 +-
 .../glyphcomponents/glyphpointnumbers.cpp          |  14 +-
 .../glyphcomponents/glyphpointnumbers.hpp          |   7 +-
 src/ftinspect/glyphcomponents/glyphpoints.cpp      |  15 +-
 src/ftinspect/glyphcomponents/glyphpoints.hpp      |   7 +-
 src/ftinspect/glyphcomponents/graphicsdefault.cpp  |  46 ++
 src/ftinspect/glyphcomponents/graphicsdefault.hpp  |  34 ++
 src/ftinspect/glyphcomponents/grid.cpp             | 147 +++---
 src/ftinspect/glyphcomponents/grid.hpp             |  21 +-
 src/ftinspect/maingui.cpp                          |   8 +-
 src/ftinspect/maingui.hpp                          |   2 +
 src/ftinspect/meson.build                          |   7 +
 src/ftinspect/panels/singular.cpp                  | 492 +++++++++++++++++++++
 src/ftinspect/panels/singular.hpp                  | 114 +++++
 src/ftinspect/widgets/fontsizeselector.cpp         | 299 +++++++++++++
 src/ftinspect/widgets/fontsizeselector.hpp         |  82 ++++
 src/ftinspect/widgets/glyphindexselector.cpp       | 260 +++++++++++
 src/ftinspect/widgets/glyphindexselector.hpp       |  78 ++++
 24 files changed, 1635 insertions(+), 161 deletions(-)

diff --git a/src/ftinspect/CMakeLists.txt b/src/ftinspect/CMakeLists.txt
index 418dbed..db3f93a 100644
--- a/src/ftinspect/CMakeLists.txt
+++ b/src/ftinspect/CMakeLists.txt
@@ -28,13 +28,17 @@ add_executable(ftinspect
   "glyphcomponents/glyphpointnumbers.cpp"
   "glyphcomponents/glyphpoints.cpp"
   "glyphcomponents/grid.cpp"
+  "glyphcomponents/graphicsdefault.cpp"
 
   "widgets/customwidgets.cpp"
   "widgets/tripletselector.cpp"
+  "widgets/glyphindexselector.cpp"
+  "widgets/fontsizeselector.cpp"
 
   "models/customcomboboxmodels.cpp"
 
   "panels/settingpanel.cpp"
+  "panels/singular.cpp"
 )
 target_link_libraries(ftinspect
   Qt5::Core Qt5::Widgets
diff --git a/src/ftinspect/engine/engine.cpp b/src/ftinspect/engine/engine.cpp
index fc4d3d6..b0801f8 100644
--- a/src/ftinspect/engine/engine.cpp
+++ b/src/ftinspect/engine/engine.cpp
@@ -343,6 +343,7 @@ Engine::loadFont(int fontIndex,
       fontType_ = FontType_TrueType;
   }
 
+  curNumGlyphs_ = numGlyphs;
   return numGlyphs;
 }
 
diff --git a/src/ftinspect/engine/engine.hpp b/src/ftinspect/engine/engine.hpp
index 351cf5b..7021c77 100644
--- a/src/ftinspect/engine/engine.hpp
+++ b/src/ftinspect/engine/engine.hpp
@@ -89,12 +89,17 @@ public:
   int currentFontType() const { return fontType_; }
   const QString& currentFamilyName() { return curFamilyName_; }
   const QString& currentStyleName() { return curStyleName_; }
+  int currentFontNumberOfGlyphs() { return curNumGlyphs_; }
+
   QString glyphName(int glyphIndex);
   long numberOfFaces(int fontIndex);
   int numberOfNamedInstances(int fontIndex,
                              long faceIndex);
   QString namedInstanceName(int fontIndex, long faceIndex, int index);
 
+  // (settings)
+  int dpi() { return dpi_; }
+
   //////// Setters (direct or indirect)
 
   void setDPI(int d) { dpi_ = d; }
@@ -140,6 +145,7 @@ private:
 
   QString curFamilyName_;
   QString curStyleName_;
+  int curNumGlyphs_ = -1;
 
   FT_Library library_;
   FTC_Manager cacheManager_;
diff --git a/src/ftinspect/glyphcomponents/glyphbitmap.cpp 
b/src/ftinspect/glyphcomponents/glyphbitmap.cpp
index dcef3ee..0479807 100644
--- a/src/ftinspect/glyphcomponents/glyphbitmap.cpp
+++ b/src/ftinspect/glyphcomponents/glyphbitmap.cpp
@@ -5,47 +5,44 @@
 
 #include "glyphbitmap.hpp"
 
+#include "../engine/engine.hpp"
+
 #include <cmath>
+#include <utility>
+#include <qevent.h>
 #include <QPainter>
 #include <QStyleOptionGraphicsItem>
+#include <freetype/ftbitmap.h>
 
 
-GlyphBitmap::GlyphBitmap(FT_Outline* outline,
-                         FT_Library lib,
-                         FT_Pixel_Mode pxlMode,
-                         const QVector<QRgb>& monoColorTbl,
-                         const QVector<QRgb>& grayColorTbl)
-: library_(lib),
-  pixelMode_(pxlMode),
-  monoColorTable_(monoColorTbl),
-  grayColorTable_(grayColorTbl)
+GlyphBitmap::GlyphBitmap(QImage* image,
+                         QRect rect)
+: image_(image),
+  boundingRect_(rect)
 {
-  // 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);
-
-  boundingRect_.setCoords(cbox.xMin / 64, -cbox.yMax / 64,
-                  cbox.xMax / 64, -cbox.yMin / 64);
+
+}
+
+
+GlyphBitmap::GlyphBitmap(int glyphIndex, 
+                         FT_Glyph glyph,
+                         Engine* engine)
+{
+  QRect bRect;
+  image_ = NULL; // TODO: refactr Engine
+  //image_ = engine->renderingEngine()->tryDirectRenderColorLayers(glyphIndex,
+  //                                                               &bRect, 
true);
+
+  //if (!image_)
+  //  image_ = engine->renderingEngine()->convertGlyphToQImage(glyph, &bRect, 
+  //                                                           true);
+  boundingRect_ = bRect; // QRect to QRectF
 }
 
 
 GlyphBitmap::~GlyphBitmap()
 {
-  FT_Outline_Done(library_, &transformed_);
+  delete image_;
 }
 
 QRectF
@@ -60,40 +57,9 @@ GlyphBitmap::paint(QPainter* painter,
                    const QStyleOptionGraphicsItem* option,
                    QWidget*)
 {
-  FT_Bitmap bitmap;
-
-  int height = static_cast<int>(ceil(boundingRect_.height()));
-  int width = static_cast<int>(ceil(boundingRect_.width()));
-  QImage::Format format = QImage::Format_Indexed8;
-
-  // XXX cover LCD and color
-  if (pixelMode_ == FT_PIXEL_MODE_MONO)
-    format = QImage::Format_Mono;
-
-  QImage image(QSize(width, height), format);
-
-  if (pixelMode_ == FT_PIXEL_MODE_MONO)
-    image.setColorTable(monoColorTable_);
-  else
-    image.setColorTable(grayColorTable_);
-
-  image.fill(0);
-
-  bitmap.rows = static_cast<unsigned int>(height);
-  bitmap.width = static_cast<unsigned int>(width);
-  bitmap.buffer = image.bits();
-  bitmap.pitch = image.bytesPerLine();
-  bitmap.pixel_mode = pixelMode_;
-
-  FT_Error error = FT_Outline_Get_Bitmap(library_,
-                                         &transformed_,
-                                         &bitmap);
-  if (error)
-  {
-    // XXX error handling
+  if (!image_)
     return;
-  }
-
+  
   // `drawImage' doesn't work as expected:
   // the larger the zoom, the more the pixel rectangle positions
   // deviate from the grid lines
@@ -102,16 +68,16 @@ GlyphBitmap::paint(QPainter* painter,
                      image.convertToFormat(
                        QImage::Format_ARGB32_Premultiplied));
 #else
-  const qreal lod = option->levelOfDetailFromTransform(
-                              painter->worldTransform());
+  const qreal lod = QStyleOptionGraphicsItem::levelOfDetailFromTransform(
+    painter->worldTransform());
 
   painter->setPen(Qt::NoPen);
 
-  for (int x = 0; x < image.width(); x++)
-    for (int y = 0; y < image.height(); y++)
+  for (int x = 0; x < image_->width(); x++)
+    for (int y = 0; y < image_->height(); y++)
     {
       // be careful not to lose the alpha channel
-      QRgb p = image.pixel(x, y);
+      QRgb p = image_->pixel(x, y);
       painter->fillRect(QRectF(x + boundingRect_.left() - 1 / lod / 2,
                                y + boundingRect_.top() - 1 / lod / 2,
                                1 + 1 / lod,
diff --git a/src/ftinspect/glyphcomponents/glyphbitmap.hpp 
b/src/ftinspect/glyphcomponents/glyphbitmap.hpp
index 092dccc..d2dafd7 100644
--- a/src/ftinspect/glyphcomponents/glyphbitmap.hpp
+++ b/src/ftinspect/glyphcomponents/glyphbitmap.hpp
@@ -7,33 +7,34 @@
 
 #include <QGraphicsItem>
 #include <QPen>
+#include <QPaintEvent>
+#include <QWidget>
 
 #include <ft2build.h>
 #include <freetype/freetype.h>
+#include <freetype/ftglyph.h>
 #include <freetype/ftoutln.h>
 
 
+class Engine;
+
 class GlyphBitmap
 : public QGraphicsItem
 {
 public:
-  GlyphBitmap(FT_Outline* outline,
-              FT_Library library,
-              FT_Pixel_Mode pixelMode,
-              const QVector<QRgb>& monoColorTable,
-              const QVector<QRgb>& grayColorTable);
-  ~GlyphBitmap();
-  QRectF boundingRect() const;
+  GlyphBitmap(QImage* image,
+              QRect rect);
+  GlyphBitmap(int glyphIndex,
+              FT_Glyph glyph,
+              Engine* engine);
+  ~GlyphBitmap() override;
+  QRectF boundingRect() const override;
   void paint(QPainter* painter,
              const QStyleOptionGraphicsItem* option,
-             QWidget* widget);
+             QWidget* widget) override;
 
 private:
-  FT_Outline transformed_;
-  FT_Library library_;
-  unsigned char pixelMode_;
-  const QVector<QRgb>& monoColorTable_;
-  const QVector<QRgb>& grayColorTable_;
+  QImage* image_ = NULL;
   QRectF boundingRect_;
 };
 
diff --git a/src/ftinspect/glyphcomponents/glyphoutline.cpp 
b/src/ftinspect/glyphcomponents/glyphoutline.cpp
index ad5d25c..2e13a31 100644
--- a/src/ftinspect/glyphcomponents/glyphoutline.cpp
+++ b/src/ftinspect/glyphcomponents/glyphoutline.cpp
@@ -87,11 +87,17 @@ static FT_Outline_Funcs outlineFuncs =
 } // extern "C"
 
 
-GlyphOutline::GlyphOutline(const QPen& outlineP,
-                           FT_Outline* outln)
-: outlinePen_(outlineP),
-  outline_(outln)
+GlyphOutline::GlyphOutline(const QPen& pen,
+                           FT_Glyph glyph)
+: outlinePen_(pen)
 {
+  if (glyph->format != FT_GLYPH_FORMAT_OUTLINE)
+  {
+    outline_ = NULL;
+    return;
+  }
+  outline_ = &reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;
+
   FT_BBox cbox;
 
   qreal halfPenWidth = outlinePen_.widthF();
@@ -117,6 +123,8 @@ GlyphOutline::paint(QPainter* painter,
                     const QStyleOptionGraphicsItem*,
                     QWidget*)
 {
+  if (!outline_)
+    return;
   painter->setPen(outlinePen_);
 
   QPainterPath path;
diff --git a/src/ftinspect/glyphcomponents/glyphoutline.hpp 
b/src/ftinspect/glyphcomponents/glyphoutline.hpp
index fa448f5..37c53a3 100644
--- a/src/ftinspect/glyphcomponents/glyphoutline.hpp
+++ b/src/ftinspect/glyphcomponents/glyphoutline.hpp
@@ -10,6 +10,7 @@
 
 #include <ft2build.h>
 #include <freetype/freetype.h>
+#include <freetype/ftglyph.h>
 #include <freetype/ftoutln.h>
 
 
@@ -18,11 +19,11 @@ class GlyphOutline
 {
 public:
   GlyphOutline(const QPen& pen,
-               FT_Outline* outline);
-  QRectF boundingRect() const;
+               FT_Glyph glyph);
+  QRectF boundingRect() const override;
   void paint(QPainter* painter,
              const QStyleOptionGraphicsItem* option,
-             QWidget* widget);
+             QWidget* widget) override;
 
 private:
   QPen outlinePen_;
diff --git a/src/ftinspect/glyphcomponents/glyphpointnumbers.cpp 
b/src/ftinspect/glyphcomponents/glyphpointnumbers.cpp
index bddee89..f52ed9d 100644
--- a/src/ftinspect/glyphcomponents/glyphpointnumbers.cpp
+++ b/src/ftinspect/glyphcomponents/glyphpointnumbers.cpp
@@ -12,11 +12,17 @@
 
 GlyphPointNumbers::GlyphPointNumbers(const QPen& onP,
                                      const QPen& offP,
-                                     FT_Outline* outln)
+                                     FT_Glyph glyph)
 : onPen_(onP),
-  offPen_(offP),
-  outline_(outln)
+  offPen_(offP)
 {
+  if (glyph->format != FT_GLYPH_FORMAT_OUTLINE)
+  {
+    outline_ = NULL;
+    return;
+  }
+  outline_ = &reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;
+
   FT_BBox cbox;
 
   FT_Outline_Get_CBox(outline_, &cbox);
@@ -41,6 +47,8 @@ GlyphPointNumbers::paint(QPainter* painter,
                          const QStyleOptionGraphicsItem* option,
                          QWidget*)
 {
+  if (!outline_)
+    return;
   const qreal lod = option->levelOfDetailFromTransform(
                               painter->worldTransform());
 
diff --git a/src/ftinspect/glyphcomponents/glyphpointnumbers.hpp 
b/src/ftinspect/glyphcomponents/glyphpointnumbers.hpp
index 61feaf0..ded1ce1 100644
--- a/src/ftinspect/glyphcomponents/glyphpointnumbers.hpp
+++ b/src/ftinspect/glyphcomponents/glyphpointnumbers.hpp
@@ -10,6 +10,7 @@
 
 #include <ft2build.h>
 #include <freetype/freetype.h>
+#include <freetype/ftglyph.h>
 #include <freetype/ftoutln.h>
 
 
@@ -19,11 +20,11 @@ class GlyphPointNumbers
 public:
   GlyphPointNumbers(const QPen& onPen,
                     const QPen& offPen,
-                    FT_Outline* outline);
-  QRectF boundingRect() const;
+                    FT_Glyph glyph);
+  QRectF boundingRect() const override;
   void paint(QPainter* painter,
              const QStyleOptionGraphicsItem* option,
-             QWidget* widget);
+             QWidget* widget) override;
 
 private:
   QPen onPen_;
diff --git a/src/ftinspect/glyphcomponents/glyphpoints.cpp 
b/src/ftinspect/glyphcomponents/glyphpoints.cpp
index 08960e6..4b35670 100644
--- a/src/ftinspect/glyphcomponents/glyphpoints.cpp
+++ b/src/ftinspect/glyphcomponents/glyphpoints.cpp
@@ -11,11 +11,17 @@
 
 GlyphPoints::GlyphPoints(const QPen& onP,
                          const QPen& offP,
-                         FT_Outline* outln)
+                         FT_Glyph glyph)
 : onPen_(onP),
-  offPen_(offP),
-  outline_(outln)
+  offPen_(offP)
 {
+  if (glyph->format != FT_GLYPH_FORMAT_OUTLINE)
+  {
+    outline_ = NULL;
+    return;
+  }
+  outline_ = &reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;
+
   FT_BBox cbox;
 
   qreal halfPenWidth = qMax(onPen_.widthF(), offPen_.widthF()) / 2;
@@ -41,6 +47,9 @@ GlyphPoints::paint(QPainter* painter,
                    const QStyleOptionGraphicsItem* option,
                    QWidget*)
 {
+  if (!outline_)
+    return;
+
   const qreal lod = option->levelOfDetailFromTransform(
                               painter->worldTransform());
 
diff --git a/src/ftinspect/glyphcomponents/glyphpoints.hpp 
b/src/ftinspect/glyphcomponents/glyphpoints.hpp
index d964826..d0f5d66 100644
--- a/src/ftinspect/glyphcomponents/glyphpoints.hpp
+++ b/src/ftinspect/glyphcomponents/glyphpoints.hpp
@@ -10,6 +10,7 @@
 
 #include <ft2build.h>
 #include <freetype/freetype.h>
+#include <freetype/ftglyph.h>
 #include <freetype/ftoutln.h>
 
 
@@ -19,11 +20,11 @@ class GlyphPoints
 public:
   GlyphPoints(const QPen& onPen,
               const QPen& offPen,
-              FT_Outline* outline);
-  QRectF boundingRect() const;
+              FT_Glyph glyph);
+  QRectF boundingRect() const override;
   void paint(QPainter* painter,
              const QStyleOptionGraphicsItem* option,
-             QWidget* widget);
+             QWidget* widget) override;
 
 private:
   QPen onPen_;
diff --git a/src/ftinspect/glyphcomponents/graphicsdefault.cpp 
b/src/ftinspect/glyphcomponents/graphicsdefault.cpp
new file mode 100644
index 0000000..ad5ae00
--- /dev/null
+++ b/src/ftinspect/glyphcomponents/graphicsdefault.cpp
@@ -0,0 +1,46 @@
+// graphicsdefault.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "graphicsdefault.hpp"
+
+GraphicsDefault* GraphicsDefault::instance_ = NULL;
+
+GraphicsDefault::GraphicsDefault()
+{
+  // XXX make this user-configurable
+
+  axisPen.setColor(Qt::black);
+  axisPen.setWidth(0);
+  blueZonePen.setColor(QColor(64, 64, 255, 64)); // light blue
+  blueZonePen.setWidth(0);
+  // Don't make this solid
+  gridPen.setColor(QColor(0, 0, 0, 255 - QColor(Qt::lightGray).red()));
+  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);
+
+  advanceAuxPen.setColor(QColor(110, 52, 235)); // kind of blue
+  advanceAuxPen.setWidth(0);
+  ascDescAuxPen.setColor(QColor(255, 0, 0)); // red
+  ascDescAuxPen.setWidth(0);
+}
+
+
+GraphicsDefault*
+GraphicsDefault::deafultInstance()
+{
+  if (!instance_)
+    instance_ = new GraphicsDefault;
+
+  return instance_;
+}
+
+
+// end of graphicsdefault.cpp
diff --git a/src/ftinspect/glyphcomponents/graphicsdefault.hpp 
b/src/ftinspect/glyphcomponents/graphicsdefault.hpp
new file mode 100644
index 0000000..991771c
--- /dev/null
+++ b/src/ftinspect/glyphcomponents/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
+{
+  QPen axisPen;
+  QPen blueZonePen;
+  QPen gridPen;
+  QPen offPen;
+  QPen onPen;
+  QPen outlinePen;
+  QPen segmentPen;
+
+  QPen advanceAuxPen;
+  QPen ascDescAuxPen;
+
+  GraphicsDefault();
+
+  static GraphicsDefault* deafultInstance();
+
+private:
+  static GraphicsDefault* instance_;
+};
+
+
+// end of graphicsdefault.hpp
diff --git a/src/ftinspect/glyphcomponents/grid.cpp 
b/src/ftinspect/glyphcomponents/grid.cpp
index 01b17cb..1ab37bb 100644
--- a/src/ftinspect/glyphcomponents/grid.cpp
+++ b/src/ftinspect/glyphcomponents/grid.cpp
@@ -5,88 +5,131 @@
 
 #include "grid.hpp"
 
+#include "graphicsdefault.hpp"
+
 #include <QPainter>
 #include <QStyleOptionGraphicsItem>
+#include <QGraphicsWidget>
+#include <QGraphicsView>
 
 
-Grid::Grid(const QPen& gridP,
-           const QPen& axisP)
-: gridPen_(gridP),
-  axisPen_(axisP)
+Grid::Grid(QGraphicsView* parentView)
+:  parentView_(parentView)
 {
  // empty
+  updateRect();
 }
 
 
 QRectF
 Grid::boundingRect() const
 {
-  // XXX fix size
+  return rect_;
+}
+
+
+void
+Grid::updateRect()
+{
+  auto viewport = parentView_->mapToScene(parentView_->viewport()->geometry())
+                         .boundingRect()
+                         .toRect();
+  int minX = std::min(viewport.left() - 10, -100);
+  int minY = std::min(viewport.top() - 10, -100);
+  int maxX = std::max(viewport.right() + 10, 100);
+  int maxY = std::max(viewport.bottom() + 10, 100);
+
+  auto newSceneRect = QRectF(QPointF(minX - 20, minY - 20), 
+                             QPointF(maxX + 20, maxY + 20));
+  if (sceneRect_ != newSceneRect && scene())
+  {
+    scene()->setSceneRect(newSceneRect);
+    sceneRect_ = newSceneRect;
+  }
 
   // no need to take care of pen width
-  return QRectF(-100, -100,
-                200, 200);
+  rect_ = QRectF(QPointF(minX, minY), 
+                QPointF(maxX, maxY));
 }
 
 
-// XXX call this in a `myQDraphicsView::drawBackground' derived method
-//     to always fill the complete viewport
-
 void
 Grid::paint(QPainter* painter,
             const QStyleOptionGraphicsItem* option,
-            QWidget*)
+            QWidget* widget)
 {
+  auto gb = GraphicsDefault::deafultInstance();
+  auto br = boundingRect().toRect();
+  int minX = br.left();
+  int minY = br.top();
+  int maxX = br.right();
+  int maxY = br.bottom();
+
   const qreal lod = option->levelOfDetailFromTransform(
                               painter->worldTransform());
-
-  painter->setPen(gridPen_);
-
-  // don't mark pixel center with a cross if magnification is too small
-  if (lod > 20)
+  if (showGrid_)
   {
-    int halfLength = 1;
-
-    // cf. QSpinBoxx
-    if (lod > 640)
-      halfLength = 6;
-    else if (lod > 320)
-      halfLength = 5;
-    else if (lod > 160)
-      halfLength = 4;
-    else if (lod > 80)
-      halfLength = 3;
-    else if (lod > 40)
-      halfLength = 2;
-
-    for (qreal x = -100; x < 100; x++)
-      for (qreal y = -100; y < 100; y++)
-      {
-        painter->drawLine(QLineF(x + 0.5, y + 0.5 - halfLength / lod,
-                                 x + 0.5, y + 0.5 + halfLength / lod));
-        painter->drawLine(QLineF(x + 0.5 - halfLength / lod, y + 0.5,
-                                 x + 0.5 + halfLength / lod, y + 0.5));
-      }
+    painter->setPen(gb->gridPen);
+    
+    // don't mark pixel center with a cross if magnification is too small
+    if (lod > 20)
+    {
+      int halfLength = 1;
+    
+      // cf. QSpinBoxx
+      if (lod > 640)
+        halfLength = 6;
+      else if (lod > 320)
+        halfLength = 5;
+      else if (lod > 160)
+        halfLength = 4;
+      else if (lod > 80)
+        halfLength = 3;
+      else if (lod > 40)
+        halfLength = 2;
+    
+      for (qreal x = minX; x < maxX; x++)
+        for (qreal y = minY; y < maxY; y++)
+        {
+          painter->drawLine(QLineF(x + 0.5, y + 0.5 - halfLength / lod,
+                                   x + 0.5, y + 0.5 + halfLength / lod));
+          painter->drawLine(QLineF(x + 0.5 - halfLength / lod, y + 0.5,
+                                   x + 0.5 + halfLength / lod, y + 0.5));
+        }
+    }
+    
+    // don't draw grid if magnification is too small
+    if (lod >= 5)
+    {
+      for (int x = minX; x <= maxX; x++)
+        painter->drawLine(x, minY,
+                          x, maxY);
+      for (int y = minY; y <= maxY; y++)
+        painter->drawLine(minX, y,
+                          maxX, y);
+    }
+    
+    painter->setPen(gb->axisPen);
+    
+    painter->drawLine(0, minY,
+                      0, maxY);
+    painter->drawLine(minX, 0,
+                      maxX, 0);
   }
 
-  // don't draw grid if magnification is too small
-  if (lod >= 5)
+  if (showAuxLines_)
   {
-    // XXX fix size
-    for (int x = -100; x <= 100; x++)
-      painter->drawLine(x, -100,
-                        x, 100);
-    for (int y = -100; y <= 100; y++)
-      painter->drawLine(-100, y,
-                        100, y);
+    // TODO: impl
   }
+}
 
-  painter->setPen(axisPen_);
 
-  painter->drawLine(0, -100,
-                    0, 100);
-  painter->drawLine(-100, 0,
-                    100, 0);
+void
+Grid::setShowGrid(bool showGrid, bool showAuxLines)
+{
+  showGrid_ = showGrid;
+  showAuxLines_ = showAuxLines;
+  update();
 }
 
 
diff --git a/src/ftinspect/glyphcomponents/grid.hpp 
b/src/ftinspect/glyphcomponents/grid.hpp
index 9740c17..529d041 100644
--- a/src/ftinspect/glyphcomponents/grid.hpp
+++ b/src/ftinspect/glyphcomponents/grid.hpp
@@ -6,23 +6,30 @@
 #pragma once
 
 #include <QGraphicsItem>
+#include <QGraphicsView>
 #include <QPen>
 
-
 class Grid
 : public QGraphicsItem
 {
 public:
-  Grid(const QPen& gridPen,
-       const QPen& axisPen);
-  QRectF boundingRect() const;
+  Grid(QGraphicsView* parentView);
+  QRectF boundingRect() const override;
   void paint(QPainter* painter,
              const QStyleOptionGraphicsItem* option,
-             QWidget* widget);
+             QWidget* widget) override;
+
+  void setShowGrid(bool showGrid, bool showAuxLines);
+
+  void updateRect(); // there's no signal/slots for QGraphicsItem.
 
 private:
-  QPen gridPen_;
-  QPen axisPen_;
+  QGraphicsView* parentView_;
+  QRectF rect_;
+  QRectF sceneRect_;
+
+  bool showGrid_ = true;
+  bool showAuxLines_ = false;
 };
 
 
diff --git a/src/ftinspect/maingui.cpp b/src/ftinspect/maingui.cpp
index 5b62e2e..db13014 100644
--- a/src/ftinspect/maingui.cpp
+++ b/src/ftinspect/maingui.cpp
@@ -170,14 +170,18 @@ MainGUI::createLayout()
   leftWidget_->setMaximumWidth(400);
 
   // right side
-  // TODO: create tabs here
+  singularTab_ = new SingularTab(this, engine_);
 
   tabWidget_ = new QTabWidget(this);
   tabWidget_->setObjectName("mainTab"); // for stylesheet
 
   // Note those two list must be in sync
-  // TODO: add tabs and tooltips here
+  tabs_.push_back(singularTab_);
+  tabWidget_->addTab(singularTab_, tr("Singular Grid View"));
+  lastTab_ = singularTab_;
   
+  tabWidget_->setTabToolTip(0, tr("View single glyph in grid view.\n"
+                                  "For pixelwise inspection of the glyphs."));
   tripletSelector_ = new TripletSelector(this, engine_);
 
   rightLayout_ = new QVBoxLayout;
diff --git a/src/ftinspect/maingui.hpp b/src/ftinspect/maingui.hpp
index a5a5397..cfe03aa 100644
--- a/src/ftinspect/maingui.hpp
+++ b/src/ftinspect/maingui.hpp
@@ -9,6 +9,7 @@
 #include "widgets/tripletselector.hpp"
 #include "panels/settingpanel.hpp"
 #include "panels/abstracttab.hpp"
+#include "panels/singular.hpp"
 
 #include <vector>
 #include <QAction>
@@ -84,6 +85,7 @@ private:
 
   QTabWidget* tabWidget_;
   std::vector<AbstractTab*> tabs_;
+  SingularTab* singularTab_;
   QWidget* lastTab_ = NULL;
 
   void openFonts(QStringList const& fileNames);
diff --git a/src/ftinspect/meson.build b/src/ftinspect/meson.build
index 5ea2197..6329b6e 100644
--- a/src/ftinspect/meson.build
+++ b/src/ftinspect/meson.build
@@ -29,13 +29,17 @@ if qt5_dep.found()
     'glyphcomponents/glyphpointnumbers.cpp',
     'glyphcomponents/glyphpoints.cpp',
     'glyphcomponents/grid.cpp',
+    'glyphcomponents/graphicsdefault.cpp',
 
     'widgets/customwidgets.cpp',
     'widgets/tripletselector.cpp',
+    'widgets/glyphindexselector.cpp',
+    'widgets/fontsizeselector.cpp',
 
     'models/customcomboboxmodels.cpp',
 
     'panels/settingpanel.cpp',
+    'panels/singular.cpp',
 
     'ftinspect.cpp',
     'maingui.cpp',
@@ -47,9 +51,12 @@ if qt5_dep.found()
       'engine/fontfilemanager.hpp',
       'widgets/customwidgets.hpp',
       'widgets/tripletselector.hpp',
+      'widgets/glyphindexselector.hpp',
+      'widgets/fontsizeselector.hpp',
       'maingui.hpp',
       'models/customcomboboxmodels.hpp',
       'panels/settingpanel.hpp',
+      'panels/singular.hpp',
     ],
     dependencies: qt5_dep)
 
diff --git a/src/ftinspect/panels/singular.cpp 
b/src/ftinspect/panels/singular.cpp
new file mode 100644
index 0000000..c8c925c
--- /dev/null
+++ b/src/ftinspect/panels/singular.cpp
@@ -0,0 +1,492 @@
+// singular.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "singular.hpp"
+
+#include <QSizePolicy>
+#include <QToolTip>
+#include <QWheelEvent>
+
+
+SingularTab::SingularTab(QWidget* parent, Engine* engine)
+: QWidget(parent), engine_(engine),
+  graphicsDefault_(GraphicsDefault::deafultInstance())
+{
+  createLayout();
+  createConnections();
+
+  currentGlyphIndex_ = 0;
+  setDefaults();
+  checkShowPoints();
+}
+
+
+SingularTab::~SingularTab()
+{
+  delete gridItem_;
+  gridItem_ = NULL;
+}
+
+
+void
+SingularTab::setGlyphIndex(int index)
+{
+  // only adjust current glyph index if we have a valid font
+  if (currentGlyphCount_ <= 0)
+    return;
+
+  currentGlyphIndex_ = qBound(0, index, currentGlyphCount_ - 1);
+
+  QString upperHex = QString::number(currentGlyphIndex_, 16).toUpper();
+  glyphIndexLabel_->setText(
+      QString("%1 (0x%2)").arg(currentGlyphIndex_).arg(upperHex));
+  glyphNameLabel_->setText(engine_->glyphName(currentGlyphIndex_));
+
+  drawGlyph();
+}
+
+
+void
+SingularTab::drawGlyph()
+{
+  // the call to `engine->loadOutline' updates FreeType's load flags
+
+  if (!engine_)
+    return;
+
+  if (currentGlyphBitmapItem_)
+  {
+    glyphScene_->removeItem(currentGlyphBitmapItem_);
+    delete currentGlyphBitmapItem_;
+
+    currentGlyphBitmapItem_ = NULL;
+  }
+
+  if (currentGlyphOutlineItem_)
+  {
+    glyphScene_->removeItem(currentGlyphOutlineItem_);
+    delete currentGlyphOutlineItem_;
+
+    currentGlyphOutlineItem_ = NULL;
+  }
+
+  if (currentGlyphPointsItem_)
+  {
+    glyphScene_->removeItem(currentGlyphPointsItem_);
+    delete currentGlyphPointsItem_;
+
+    currentGlyphPointsItem_ = NULL;
+  }
+
+  if (currentGlyphPointNumbersItem_)
+  {
+    glyphScene_->removeItem(currentGlyphPointNumbersItem_);
+    delete currentGlyphPointNumbersItem_;
+
+    currentGlyphPointNumbersItem_ = NULL;
+  }
+
+  // TODO refactor engine.
+  //glyphView_->setBackgroundBrush(
+  //  QColor(engine_->renderingEngine()->background()));
+
+  applySettings();
+  FT_Glyph glyph = NULL; // TODO: refactor engine!
+  //FT_Glyph glyph = engine_->loadGlyph(currentGlyphIndex_);
+  if (glyph)
+  {
+    if (showBitmapCheckBox_->isChecked())
+    {
+      currentGlyphBitmapItem_
+        = new GlyphBitmap(currentGlyphIndex_, 
+                          glyph,
+                          engine_);
+      currentGlyphBitmapItem_->setZValue(-1);
+      glyphScene_->addItem(currentGlyphBitmapItem_);
+    }
+
+    if (showOutlinesCheckBox_->isChecked())
+    {
+      currentGlyphOutlineItem_ = new 
GlyphOutline(graphicsDefault_->outlinePen, 
+                                                  glyph);
+      currentGlyphOutlineItem_->setZValue(1);
+      glyphScene_->addItem(currentGlyphOutlineItem_);
+    }
+
+    if (showPointsCheckBox_->isChecked())
+    {
+      currentGlyphPointsItem_ = new GlyphPoints(graphicsDefault_->onPen,
+                                                graphicsDefault_->offPen,
+                                                glyph);
+      currentGlyphPointsItem_->setZValue(1);
+      glyphScene_->addItem(currentGlyphPointsItem_);
+
+      if (showPointNumbersCheckBox_->isChecked())
+      {
+        currentGlyphPointNumbersItem_
+          = new GlyphPointNumbers(graphicsDefault_->onPen,
+                                  graphicsDefault_->offPen,
+                                  glyph);
+        currentGlyphPointNumbersItem_->setZValue(1);
+        glyphScene_->addItem(currentGlyphPointNumbersItem_);
+      }
+    }
+  }
+
+  glyphScene_->update();
+}
+
+
+void
+SingularTab::checkShowPoints()
+{
+  if (showPointsCheckBox_->isChecked())
+    showPointNumbersCheckBox_->setEnabled(true);
+  else
+    showPointNumbersCheckBox_->setEnabled(false);
+  drawGlyph();
+}
+
+
+void
+SingularTab::zoom()
+{
+  int scale = static_cast<int>(sizeSelector_->zoomFactor());
+
+  QTransform transform;
+  transform.scale(scale, scale);
+
+  // we want horizontal and vertical 1px lines displayed with full pixels;
+  // we thus have to shift the coordinate system accordingly, using a value
+  // that represents 0.5px (i.e., half the 1px line width) after the scaling
+  qreal shift = 0.5 / scale;
+  transform.translate(shift, shift);
+
+  glyphView_->setTransform(transform);
+  updateGrid();
+}
+
+
+void
+SingularTab::backToCenter()
+{
+  glyphView_->centerOn(0, 0);
+  if (currentGlyphBitmapItem_)
+    glyphView_->ensureVisible(currentGlyphBitmapItem_);
+  else if (currentGlyphPointsItem_)
+    glyphView_->ensureVisible(currentGlyphPointsItem_);
+
+  updateGrid();
+}
+
+
+void
+SingularTab::updateGrid()
+{
+  if (gridItem_)
+    gridItem_->updateRect();
+}
+
+
+void
+SingularTab::wheelZoom(QWheelEvent* event)
+{
+  int numSteps = event->angleDelta().y() / 120;
+  sizeSelector_->handleWheelZoomBySteps(numSteps);
+  // TODO: Zoom relative to viewport left-bottom?
+}
+
+
+void
+SingularTab::wheelResize(QWheelEvent* event)
+{
+  sizeSelector_->handleWheelResizeFromGrid(event);
+}
+
+
+void
+SingularTab::setGridVisible()
+{
+  gridItem_->setShowGrid(showGridCheckBox_->isChecked(),
+                         showAuxLinesCheckBox_->isChecked());
+}
+
+
+void
+SingularTab::showToolTip()
+{
+  QToolTip::showText(mapToGlobal(helpButton_->pos()),
+                     tr("Scroll: Grid Up/Down\n"
+                        "Alt + Scroll: Grid Left/Right\n"
+                        "Ctrl + Scroll: Adjust Zoom (Relative to cursor)\n"
+                        "Shift + Scroll: Adjust Font Size\n"
+                        "Shift + Plus/Minus: Adjust Font Size\n"
+                        "Shift + 0: Reset Font Size to Default"),
+                     helpButton_);
+}
+
+
+bool
+SingularTab::eventFilter(QObject* watched,
+                         QEvent* event)
+{
+  if (event->type() == QEvent::KeyPress)
+  {
+    auto keyEvent = dynamic_cast<QKeyEvent*>(event);
+    if (sizeSelector_->handleKeyEvent(keyEvent))
+      return true;
+  }
+  return false;
+}
+
+
+void
+SingularTab::resizeEvent(QResizeEvent* event)
+{
+  QWidget::resizeEvent(event);
+
+  // Tricky part: when loading, this method will be called twice. Only at the
+  // second time the initial layouting is done, thus the result of `centerOn`
+  // can be valid.
+  // We want to only center the midpoint of the size when the program starts up
+  // so we use a counter to track the status.
+  if (initialPositionSetCount_ <= 0)
+    return;
+  initialPositionSetCount_--;
+
+  // The code below mainly:
+  // 1. Center the grid on the the center point of the ppem x ppem bbox.
+  // 2. Adjust the viewport zoom to fit the ppem x ppem bbox
+  updateGeometry();
+  auto size = sizeSelector_->selectedSize();
+  auto unit = sizeSelector_->selectedUnit();
+  if (unit == FontSizeSelector::Units_pt)
+  {
+    sizeSelector_->applyToEngine(engine_);
+    auto dpi = engine_->dpi();
+    size = size * dpi / 72.0;
+  }
+  glyphView_->centerOn(size / 2, -size / 2);
+
+  auto viewSize = glyphView_->size();
+  auto minViewSide = std::min(viewSize.height(), viewSize.width());
+  sizeSelector_->setZoomFactor(static_cast<int>(minViewSide / size * 0.7));
+}
+
+
+void
+SingularTab::createLayout()
+{
+  glyphScene_ = new QGraphicsScene(this);
+
+  currentGlyphBitmapItem_ = NULL;
+  currentGlyphOutlineItem_ = NULL;
+  currentGlyphPointsItem_ = NULL;
+  currentGlyphPointNumbersItem_ = NULL;
+
+  glyphView_ = new QGraphicsViewx(this);
+  glyphView_->setRenderHint(QPainter::Antialiasing, true);
+  glyphView_->setAcceptDrops(false);
+  glyphView_->setDragMode(QGraphicsView::ScrollHandDrag);
+  glyphView_->setOptimizationFlags(QGraphicsView::DontSavePainterState);
+  glyphView_->setViewportUpdateMode(QGraphicsView::SmartViewportUpdate);
+  glyphView_->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
+  glyphView_->setScene(glyphScene_);
+  glyphView_->setBackgroundBrush(Qt::white);
+
+  gridItem_ = new Grid(glyphView_);
+  glyphScene_->addItem(gridItem_);
+
+  // Don't use QGraphicsTextItem: We want this hint to be anchored at the
+  // top-left corner.
+  auto overlayFont = font();
+  overlayFont.setPixelSize(24);
+
+  glyphIndexLabel_ = new QLabel(glyphView_);
+  glyphNameLabel_ = new QLabel(glyphView_);
+  glyphIndexLabel_->setFont(overlayFont);
+  glyphNameLabel_->setFont(overlayFont);
+  glyphIndexLabel_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+  glyphNameLabel_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+  glyphIndexLabel_->setAttribute(Qt::WA_TransparentForMouseEvents, true);
+  glyphNameLabel_->setAttribute(Qt::WA_TransparentForMouseEvents, true);
+
+  glyphIndexLabel_->setStyleSheet("QLabel { color : black; }");
+  glyphNameLabel_->setStyleSheet("QLabel { color : black; }");
+
+  indexSelector_ = new GlyphIndexSelector(this);
+  indexSelector_->setSingleMode(true);
+
+  sizeSelector_ = new FontSizeSelector(this, false, false);
+
+  centerGridButton_ = new QPushButton("Go Back to Grid Center", this);
+  helpButton_ = new QPushButton("?", this);
+  helpButton_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+  showBitmapCheckBox_ = new QCheckBox(tr("Show Bitmap"), this);
+  showPointsCheckBox_ = new QCheckBox(tr("Show Points"), this);
+  showPointNumbersCheckBox_ = new QCheckBox(tr("Show Point Numbers"), this);
+  showOutlinesCheckBox_ = new QCheckBox(tr("Show Outlines"), this);
+  showGridCheckBox_ = new QCheckBox(tr("Show Grid"), this);
+  showAuxLinesCheckBox_ = new QCheckBox(tr("Show Aux. Lines"), this);
+
+  // Tooltips
+  centerGridButton_->setToolTip(tr(
+    "Move the viewport so the origin point is at the center of the view."));
+  showBitmapCheckBox_->setToolTip(tr("Show glyph bitmap."));
+  showPointsCheckBox_->setToolTip(
+    tr("Show control points (only valid when the glyph is an outline 
glyph)."));
+  showPointNumbersCheckBox_->setToolTip(
+    tr("Show point numbers (only available when points are shown)."));
+  showOutlinesCheckBox_->setToolTip(tr("Show (vector) outline (only valid when 
"
+                                       "the glyph is an outline glyph)."));
+  showGridCheckBox_->setToolTip(tr("Show grid lines (x axis: baseline)."));
+  showBitmapCheckBox_->setToolTip(
+    tr("Show auxiliary lines (blue: y-advance; red: ascender/descender)."));
+  helpButton_->setToolTip(tr("View scroll help"));
+
+  // Layouting
+  indexHelpLayout_ = new QHBoxLayout;
+  indexHelpLayout_->addWidget(indexSelector_, 1);
+  indexHelpLayout_->addWidget(helpButton_);
+
+  sizeLayout_ = new QHBoxLayout;
+  sizeLayout_->addStretch(2);
+  sizeLayout_->addWidget(sizeSelector_, 4);
+  sizeLayout_->addStretch(1);
+  sizeLayout_->addWidget(centerGridButton_);
+  sizeLayout_->addStretch(2);
+
+  checkBoxesLayout_ = new QHBoxLayout;
+  checkBoxesLayout_->setSpacing(10);
+  checkBoxesLayout_->addWidget(showBitmapCheckBox_);
+  checkBoxesLayout_->addWidget(showPointsCheckBox_);
+  checkBoxesLayout_->addWidget(showPointNumbersCheckBox_);
+  checkBoxesLayout_->addWidget(showOutlinesCheckBox_);
+  checkBoxesLayout_->addWidget(showGridCheckBox_);
+  checkBoxesLayout_->addWidget(showAuxLinesCheckBox_);
+
+  glyphOverlayIndexLayout_ = new QHBoxLayout;
+  glyphOverlayIndexLayout_->addWidget(glyphIndexLabel_);
+  glyphOverlayIndexLayout_->addWidget(glyphNameLabel_);
+  glyphOverlayLayout_ = new QGridLayout; // use a grid layout to align
+  glyphOverlayLayout_->addLayout(glyphOverlayIndexLayout_, 0, 1,
+                                 Qt::AlignTop | Qt::AlignRight);
+  glyphView_->setLayout(glyphOverlayLayout_);
+
+  mainLayout_ = new QVBoxLayout;
+  mainLayout_->addWidget(glyphView_);
+  mainLayout_->addLayout(indexHelpLayout_);
+  mainLayout_->addSpacing(10);
+  mainLayout_->addLayout(sizeLayout_);
+  mainLayout_->addLayout(checkBoxesLayout_);
+  mainLayout_->addSpacing(10);
+
+  setLayout(mainLayout_);
+}
+
+
+void
+SingularTab::createConnections()
+{
+  connect(sizeSelector_, &FontSizeSelector::valueChanged,
+          this, &SingularTab::repaintGlyph);
+  connect(indexSelector_, &GlyphIndexSelector::currentIndexChanged, 
+          this, &SingularTab::setGlyphIndex);
+  
+  connect(glyphView_, &QGraphicsViewx::shiftWheelEvent, 
+          this, &SingularTab::wheelResize);
+  connect(glyphView_, &QGraphicsViewx::ctrlWheelEvent, 
+          this, &SingularTab::wheelZoom);
+  // Use `updateGrid` to support infinite panning.
+  connect(glyphView_->horizontalScrollBar(), &QScrollBar::valueChanged,
+          this, &SingularTab::updateGrid);
+  connect(glyphView_->verticalScrollBar(), &QScrollBar::valueChanged, 
+          this, &SingularTab::updateGrid);
+
+  connect(centerGridButton_, &QPushButton::clicked,
+          this, &SingularTab::backToCenter);
+  connect(helpButton_, &QPushButton::clicked,
+          this, &SingularTab::showToolTip);
+
+  connect(showBitmapCheckBox_, &QCheckBox::clicked,
+          this, &SingularTab::drawGlyph);
+  connect(showPointsCheckBox_, &QCheckBox::clicked, 
+          this, &SingularTab::checkShowPoints);
+  connect(showPointNumbersCheckBox_, &QCheckBox::clicked,
+          this, &SingularTab::drawGlyph);
+  connect(showOutlinesCheckBox_, &QCheckBox::clicked,
+          this, &SingularTab::drawGlyph);
+  connect(showGridCheckBox_, &QCheckBox::clicked,
+          this, &SingularTab::setGridVisible);
+  connect(showAuxLinesCheckBox_, &QCheckBox::clicked,
+          this, &SingularTab::setGridVisible);
+
+  sizeSelector_->installEventFilterForWidget(glyphView_);
+  sizeSelector_->installEventFilterForWidget(this);
+}
+
+
+void
+SingularTab::repaintGlyph()
+{
+  zoom();
+  drawGlyph();
+}
+
+
+void
+SingularTab::reloadFont()
+{
+  currentGlyphCount_ = engine_->currentFontNumberOfGlyphs();
+  indexSelector_->setMinMax(0, currentGlyphCount_);
+  {
+    QSignalBlocker blocker(sizeSelector_);
+    sizeSelector_->reloadFromFont(engine_);
+  }
+  drawGlyph();
+}
+
+
+void
+SingularTab::setCurrentGlyphAndSize(int glyphIndex,
+                                    double sizePoint)
+{
+  if (sizePoint >= 0)
+    sizeSelector_->setSizePoint(sizePoint);
+  indexSelector_->setCurrentIndex(glyphIndex); // this will auto trigger update
+}
+
+
+int
+SingularTab::currentGlyph()
+{
+  return indexSelector_->currentIndex();
+}
+
+
+void
+SingularTab::applySettings()
+{
+  sizeSelector_->applyToEngine(engine_);
+}
+
+
+void
+SingularTab::setDefaults()
+{
+  currentGlyphIndex_ = 0;
+
+  showBitmapCheckBox_->setChecked(true);
+  showOutlinesCheckBox_->setChecked(true);
+  showGridCheckBox_->setChecked(true);
+  showAuxLinesCheckBox_->setChecked(true);
+  gridItem_->setShowGrid(true, true);
+
+  indexSelector_->setCurrentIndex(indexSelector_->currentIndex(), true);
+  zoom();
+}
+
+
+// end of singular.cpp
diff --git a/src/ftinspect/panels/singular.hpp 
b/src/ftinspect/panels/singular.hpp
new file mode 100644
index 0000000..6a1a5d4
--- /dev/null
+++ b/src/ftinspect/panels/singular.hpp
@@ -0,0 +1,114 @@
+// singular.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 "../glyphcomponents/glyphbitmap.hpp"
+#include "../glyphcomponents/glyphoutline.hpp"
+#include "../glyphcomponents/glyphpointnumbers.hpp"
+#include "../glyphcomponents/glyphpoints.hpp"
+#include "../glyphcomponents/grid.hpp"
+#include "../glyphcomponents/graphicsdefault.hpp"
+#include "../engine/engine.hpp"
+#include "../models/customcomboboxmodels.hpp"
+
+#include <QWidget>
+#include <QPushButton>
+#include <QSpinBox>
+#include <QGraphicsScene>
+#include <QGraphicsView>
+#include <QScrollBar>
+#include <QLabel>
+#include <QComboBox>
+#include <QPen>
+#include <QCheckBox>
+#include <QVector>
+#include <QGridLayout>
+#include <QBoxLayout>
+
+class SingularTab
+: public QWidget, public AbstractTab
+{
+  Q_OBJECT
+public:
+  SingularTab(QWidget* parent, Engine* engine);
+  ~SingularTab() override;
+
+  void repaintGlyph() override;
+  void reloadFont() override;
+  // when sizePoint <= 0, the size remains unchanged.
+  void setCurrentGlyphAndSize(int glyphIndex, double sizePoint);
+  int currentGlyph();
+
+private slots:
+  void setGlyphIndex(int);
+  void drawGlyph();
+  
+  void checkShowPoints();
+
+  void zoom();
+  void backToCenter();
+  void wheelZoom(QWheelEvent* event);
+  void wheelResize(QWheelEvent* event);
+  void setGridVisible();
+  void showToolTip();
+
+protected:
+  bool eventFilter(QObject* watched, QEvent* event) override;
+  void resizeEvent(QResizeEvent* event) override;
+
+private:
+  int currentGlyphIndex_;
+  int currentGlyphCount_;
+
+  Engine* engine_;
+
+  QGraphicsScene* glyphScene_;
+  QGraphicsViewx* glyphView_;
+
+  GlyphOutline* currentGlyphOutlineItem_;
+  GlyphPoints* currentGlyphPointsItem_;
+  GlyphPointNumbers* currentGlyphPointNumbersItem_;
+  GlyphBitmap* currentGlyphBitmapItem_;
+  Grid* gridItem_ = NULL;
+
+  GlyphIndexSelector* indexSelector_;
+  FontSizeSelector* sizeSelector_;
+  QPushButton* centerGridButton_;
+  QPushButton* helpButton_;
+
+  QLabel* glyphIndexLabel_;
+  QLabel* glyphNameLabel_;
+
+  QCheckBox* showBitmapCheckBox_;
+  QCheckBox* showOutlinesCheckBox_;
+  QCheckBox* showPointNumbersCheckBox_;
+  QCheckBox* showPointsCheckBox_;
+  QCheckBox* showGridCheckBox_;
+  QCheckBox* showAuxLinesCheckBox_;
+
+  QVBoxLayout* mainLayout_;
+  QHBoxLayout* checkBoxesLayout_;
+  QHBoxLayout* indexHelpLayout_;
+  QHBoxLayout* sizeLayout_;
+  QGridLayout* glyphOverlayLayout_;
+  QHBoxLayout* glyphOverlayIndexLayout_;
+
+  GraphicsDefault* graphicsDefault_;
+
+  int initialPositionSetCount_ = 2; // see `resizeEvent`
+
+  void createLayout();
+  void createConnections();
+  
+  void updateGrid();
+  void applySettings();
+  void setDefaults();
+};
+
+// end of singular.hpp
diff --git a/src/ftinspect/widgets/fontsizeselector.cpp 
b/src/ftinspect/widgets/fontsizeselector.cpp
new file mode 100644
index 0000000..58daa2e
--- /dev/null
+++ b/src/ftinspect/widgets/fontsizeselector.cpp
@@ -0,0 +1,299 @@
+// fontsizeselector.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "fontsizeselector.hpp"
+
+#include "../engine/engine.hpp"
+
+#include <algorithm>
+
+FontSizeSelector::FontSizeSelector(QWidget* parent, 
+                                   bool zoomNewLine,
+                                   bool continuousView)
+: QWidget(parent), continuousView_(continuousView)
+{
+  createLayout(zoomNewLine);
+  createConnections();
+  setDefaults();
+}
+
+
+double
+FontSizeSelector::selectedSize()
+{
+  return sizeDoubleSpinBox_->value();
+}
+
+
+FontSizeSelector::Units
+FontSizeSelector::selectedUnit()
+{
+  return static_cast<Units>(unitsComboBox_->currentIndex());
+}
+
+
+double
+FontSizeSelector::zoomFactor()
+{
+  if (continuousView_)
+    return zoomSpinBox_->value();
+  return static_cast<int>(zoomSpinBox_->value());
+}
+
+
+void
+FontSizeSelector::setSizePixel(int sizePixel)
+{
+  sizeDoubleSpinBox_->setValue(sizePixel);
+  unitsComboBox_->setCurrentIndex(Units_px);
+}
+
+
+void
+FontSizeSelector::setSizePoint(double sizePoint)
+{
+  sizeDoubleSpinBox_->setValue(sizePoint);
+  unitsComboBox_->setCurrentIndex(Units_pt);
+}
+
+
+void
+FontSizeSelector::setZoomFactor(double zoomFactor)
+{
+  if (continuousView_)
+    zoomSpinBox_->setValue(zoomFactor);
+  zoomSpinBox_->setValue(static_cast<int>(zoomFactor));
+}
+
+
+void
+FontSizeSelector::reloadFromFont(Engine* engine)
+{
+  // TODO: update available sizes.
+  checkFixedSizeAndEmit();
+}
+
+
+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 * sizeDoubleSpinBox_->singleStep();
+  sizeAfter = std::max(sizeDoubleSpinBox_->minimum(),
+                       std::min(sizeAfter, sizeDoubleSpinBox_->maximum()));
+  sizeDoubleSpinBox_->setValue(sizeAfter);
+}
+
+
+void
+FontSizeSelector::handleWheelZoomBySteps(int steps)
+{
+  double zoomAfter = zoomSpinBox_->value()
+                     + steps * zoomSpinBox_->singleStep();
+  zoomAfter = std::max(zoomSpinBox_->minimum(),
+                       std::min(zoomAfter, zoomSpinBox_->maximum()));
+  zoomSpinBox_->setValue(zoomAfter);
+}
+
+
+void
+FontSizeSelector::handleWheelResizeFromGrid(QWheelEvent* event)
+{
+  int numSteps = event->angleDelta().y() / 120;
+  handleWheelResizeBySteps(numSteps);
+}
+
+
+bool
+FontSizeSelector::handleKeyEvent(QKeyEvent const* keyEvent)
+{
+  if (!keyEvent)
+    return false;
+  auto modifiers = keyEvent->modifiers();
+  auto key = keyEvent->key();
+  if ((modifiers == Qt::ShiftModifier
+       || modifiers == (Qt::ShiftModifier | Qt::KeypadModifier))
+      && (key == Qt::Key_Plus 
+          || key == Qt::Key_Minus
+          || key == Qt::Key_Underscore
+          || key == Qt::Key_Equal
+          || key == Qt::Key_ParenRight))
+  {
+    if (key == Qt::Key_Plus || key == Qt::Key_Equal)
+      handleWheelResizeBySteps(1);
+    else if (key == Qt::Key_Minus
+             || key == Qt::Key_Underscore)
+      handleWheelResizeBySteps(-1);
+    else if (key == Qt::Key_ParenRight)
+      setDefaults(true);
+    return true;
+  }
+  return false;
+}
+
+
+void
+FontSizeSelector::installEventFilterForWidget(QWidget* widget)
+{
+  widget->installEventFilter(this);
+}
+
+
+bool
+FontSizeSelector::eventFilter(QObject* watched,
+                              QEvent* event)
+{
+  if (event->type() == QEvent::KeyPress)
+  {
+    auto keyEvent = dynamic_cast<QKeyEvent*>(event);
+    if (handleKeyEvent(keyEvent))
+      return true;
+  }
+  return QWidget::eventFilter(watched, event);
+}
+
+
+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(bool zoomNewLine)
+{
+  sizeLabel_ = new QLabel(tr("Size "), this);
+  sizeLabel_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
+  sizeDoubleSpinBox_ = new QDoubleSpinBox(this);
+  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_);
+
+  zoomLabel_ = new QLabel(tr("Zoom Factor "), this);
+  zoomLabel_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
+  zoomSpinBox_ = new ZoomSpinBox(this, continuousView_);
+  zoomSpinBox_->setAlignment(Qt::AlignRight);
+  zoomLabel_->setBuddy(zoomSpinBox_);
+
+  // Tooltips
+  sizeDoubleSpinBox_->setToolTip(
+    tr("Size value (will be limited to available sizes if\nthe current font "
+       "is not scalable)."));
+  unitsComboBox_->setToolTip(tr("Unit for the size value (force to pixel if\n"
+                                "the current font is not scalable)."));
+  dpiSpinBox_->setToolTip(
+    tr("DPI for the point size value (only valid when the unit is point)."));
+  zoomSpinBox_->setToolTip(tr("Adjust zoom."));
+
+  // Layouting
+  mainLayout_ = new QVBoxLayout;
+  upLayout_ = new QHBoxLayout;
+  upLayout_->addStretch(1);
+  upLayout_->addWidget(sizeLabel_);
+  upLayout_->addWidget(sizeDoubleSpinBox_);
+  upLayout_->addWidget(unitsComboBox_);
+  upLayout_->addStretch(1);
+  upLayout_->addWidget(dpiLabel_);
+  upLayout_->addWidget(dpiSpinBox_);
+  upLayout_->addStretch(1);
+  if (!zoomNewLine)
+  {
+    upLayout_->addWidget(zoomLabel_);
+    upLayout_->addWidget(zoomSpinBox_);
+    upLayout_->addStretch(1);
+    mainLayout_->addLayout(upLayout_);
+  }
+  else
+  {
+    downLayout_ = new QHBoxLayout;
+    downLayout_->addWidget(zoomLabel_);
+    downLayout_->addWidget(zoomSpinBox_, 1);
+    mainLayout_->addLayout(upLayout_);
+    mainLayout_->addLayout(downLayout_);
+  }
+
+  setLayout(mainLayout_);
+  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
+}
+
+
+void
+FontSizeSelector::createConnections()
+{
+  connect(sizeDoubleSpinBox_, 
QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+          this, &FontSizeSelector::checkFixedSizeAndEmit);
+  connect(unitsComboBox_, QOverload<int>::of(&QComboBox::currentIndexChanged),
+          this, &FontSizeSelector::checkUnits);
+  connect(dpiSpinBox_, QOverload<int>::of(&QSpinBox::valueChanged),
+          this, &FontSizeSelector::checkFixedSizeAndEmit);
+  connect(zoomSpinBox_, QOverload<double>::of(&ZoomSpinBox::valueChanged),
+          this, &FontSizeSelector::valueChanged);
+}
+
+
+void
+FontSizeSelector::setDefaults(bool sizeOnly)
+{
+  lastValue_ = 20;
+  sizeDoubleSpinBox_->setValue(lastValue_);
+  if (sizeOnly)
+    return;
+  dpiSpinBox_->setValue(96);
+  checkUnits();
+}
+
+
+void
+FontSizeSelector::checkFixedSizeAndEmit()
+{
+  // TODO: check fixed sizes, coerce to available sizes.
+  emit valueChanged();
+}
+
+
+// end of fontsizeselector.cpp
diff --git a/src/ftinspect/widgets/fontsizeselector.hpp 
b/src/ftinspect/widgets/fontsizeselector.hpp
new file mode 100644
index 0000000..9694110
--- /dev/null
+++ b/src/ftinspect/widgets/fontsizeselector.hpp
@@ -0,0 +1,82 @@
+// fontsizeselector.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include "customwidgets.hpp"
+
+#include <QComboBox>
+#include <QDoubleSpinBox>
+#include <QLabel>
+#include <QWidget>
+#include <QBoxLayout>
+#include <QWheelEvent>
+
+class Engine;
+class FontSizeSelector : public QWidget
+{
+  Q_OBJECT
+
+public:
+  // For the continuous view mode, see `ZoomSpinBox`'s documentation.
+  FontSizeSelector(QWidget* parent, bool zoomNewLine, bool continuousView);
+  ~FontSizeSelector() override = default;
+
+  enum Units : int
+  {
+    Units_px,
+    Units_pt
+  };
+
+  //////// Getters
+  double selectedSize();
+  Units selectedUnit();
+  double zoomFactor();
+  //////// Setters
+  void setSizePixel(int sizePixel);
+  void setSizePoint(double sizePoint);
+  void setZoomFactor(double zoomFactor);
+
+  void reloadFromFont(Engine* engine);
+  void applyToEngine(Engine* engine);
+  void handleWheelResizeBySteps(int steps);
+  void handleWheelZoomBySteps(int steps);
+  void handleWheelResizeFromGrid(QWheelEvent* event);
+  bool handleKeyEvent(QKeyEvent const* keyEvent);
+  void installEventFilterForWidget(QWidget* widget);
+
+protected:
+  bool eventFilter(QObject* watched, QEvent* event) override;
+
+signals:
+  void valueChanged();
+
+private:
+  QLabel* sizeLabel_;
+  QLabel* dpiLabel_;
+  QLabel* zoomLabel_;
+
+  QDoubleSpinBox* sizeDoubleSpinBox_;
+  QComboBox* unitsComboBox_;
+  QSpinBox* dpiSpinBox_;
+  ZoomSpinBox* zoomSpinBox_;
+
+  // Sometimes we need to split 2 lines
+  QHBoxLayout* upLayout_;
+  QHBoxLayout* downLayout_;
+  QVBoxLayout* mainLayout_;
+
+  bool continuousView_;
+  double lastValue_;
+
+  void createLayout(bool zoomNewLine);
+  void createConnections();
+  void setDefaults(bool sizeOnly = false);
+
+  void checkUnits();
+  void checkFixedSizeAndEmit();
+};
+
+
+// end of fontsizeselector.hpp
diff --git a/src/ftinspect/widgets/glyphindexselector.cpp 
b/src/ftinspect/widgets/glyphindexselector.cpp
new file mode 100644
index 0000000..3b7abcc
--- /dev/null
+++ b/src/ftinspect/widgets/glyphindexselector.cpp
@@ -0,0 +1,260 @@
+// glyphindexselector.cpp
+
+// Copyright (C) 2022 Charlie Jiang.
+
+#include "glyphindexselector.hpp"
+
+#include "../uihelper.hpp"
+
+#include <climits>
+
+GlyphIndexSelector::GlyphIndexSelector(QWidget* parent)
+: QWidget(parent)
+{
+  numberRenderer_ = &GlyphIndexSelector::renderNumberDefault;
+
+  createLayout();
+  createConnections();
+  showingCount_ = 0;
+}
+
+
+void
+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()));
+  blockSignals(eventState);
+
+  updateLabel();
+}
+
+
+void
+GlyphIndexSelector::setShowingCount(int showingCount)
+{
+  showingCount_ = showingCount;
+  updateLabel();
+}
+
+
+void
+GlyphIndexSelector::setSingleMode(bool singleMode)
+{
+  singleMode_ = singleMode;
+  updateLabel();
+}
+
+
+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);
+  blockSignals(state);
+  
+  if (forceUpdate)
+    emit currentIndexChanged(indexSpinBox_->value());
+}
+
+
+int
+GlyphIndexSelector::currentIndex()
+{
+  return indexSpinBox_->value();
+}
+
+
+void
+GlyphIndexSelector::setNumberRenderer(std::function<QString(int)> renderer)
+{
+  numberRenderer_ = std::move(renderer);
+}
+
+
+void
+GlyphIndexSelector::resizeEvent(QResizeEvent* event)
+{
+  QWidget::resizeEvent(event);
+  auto minimumWidth = minimumSizeHint().width();
+  if (toEndButton_->isVisible())
+  {
+    if (width() < minimumWidth)
+      navigationWidget_->setVisible(false);
+  }
+  else if (navigationWidget_->minimumSizeHint().width() + minimumWidth 
+           <= width())
+    navigationWidget_->setVisible(true);
+}
+
+
+void
+GlyphIndexSelector::adjustIndex(int delta)
+{
+  {
+    QSignalBlocker blocker(this);
+    indexSpinBox_->setValue(qBound(indexSpinBox_->minimum(),
+                                   indexSpinBox_->value() + delta,
+                                   indexSpinBox_->maximum()));
+  }
+  emitValueChanged();
+}
+
+
+void
+GlyphIndexSelector::emitValueChanged()
+{
+  emit currentIndexChanged(indexSpinBox_->value());
+  updateLabel();
+}
+
+
+void
+GlyphIndexSelector::updateLabel()
+{
+  if (singleMode_)
+    indexLabel_->setText(QString("%1\nLimit: %2")
+                           .arg(numberRenderer_(indexSpinBox_->value()))
+                           .arg(numberRenderer_(indexSpinBox_->maximum())));
+  else
+    indexLabel_->setText(
+      QString("%1~%2\nLimit: %4")
+        .arg(numberRenderer_(indexSpinBox_->value()))
+        .arg(numberRenderer_(
+          qBound(indexSpinBox_->value(),
+                 indexSpinBox_->value() + showingCount_ - 1, INT_MAX)))
+        .arg(numberRenderer_(indexSpinBox_->maximum())));
+}
+
+
+void
+GlyphIndexSelector::createLayout()
+{
+  navigationWidget_ = new QWidget(this);
+  toStartButton_ = new QPushButton("|<", this);
+  toM1000Button_ = new QPushButton("-1000", this);
+  toM100Button_ = new QPushButton("-100", this);
+  toM10Button_ = new QPushButton("-10", this);
+  toM1Button_ = new QPushButton("-1", this);
+  toP1Button_ = new QPushButton("+1", this);
+  toP10Button_ = new QPushButton("+10", this);
+  toP100Button_ = new QPushButton("+100", this);
+  toP1000Button_ = new QPushButton("+1000", this);
+  toEndButton_ = new QPushButton(">|", this);
+  
+  indexSpinBox_ = new QSpinBox(this);
+  indexSpinBox_->setCorrectionMode(QAbstractSpinBox::CorrectToNearestValue);
+  indexSpinBox_->setButtonSymbols(QAbstractSpinBox::NoButtons);
+  indexSpinBox_->setRange(0, 0);
+  indexSpinBox_->setFixedWidth(80);
+  indexSpinBox_->setWrapping(false);
+  indexSpinBox_->setKeyboardTracking(false);
+
+  indexLabel_ = new QLabel("0\nLimit: 0");
+  indexLabel_->setMinimumWidth(200);
+
+  setButtonNarrowest(toStartButton_);
+  setButtonNarrowest(toM1000Button_);
+  setButtonNarrowest(toM100Button_);
+  setButtonNarrowest(toM10Button_);
+  setButtonNarrowest(toM1Button_);
+  setButtonNarrowest(toP1Button_);
+  setButtonNarrowest(toP10Button_);
+  setButtonNarrowest(toP100Button_);
+  setButtonNarrowest(toP1000Button_);
+  setButtonNarrowest(toEndButton_);
+
+  // Toltips
+  indexSpinBox_->setToolTip("Current glyph index.");
+  indexLabel_->setToolTip("Current glyph index/range and the max index.");
+
+  // Layouting
+  navigationLayout_ = new QHBoxLayout;
+  navigationLayout_->setSpacing(0);
+  navigationLayout_->addWidget(toStartButton_);
+  navigationLayout_->addWidget(toM1000Button_);
+  navigationLayout_->addWidget(toM100Button_);
+  navigationLayout_->addWidget(toM10Button_);
+  navigationLayout_->addWidget(toM1Button_);
+  navigationLayout_->addWidget(toP1Button_);
+  navigationLayout_->addWidget(toP10Button_);
+  navigationLayout_->addWidget(toP100Button_);
+  navigationLayout_->addWidget(toP1000Button_);
+  navigationLayout_->addWidget(toEndButton_);
+  navigationWidget_->setLayout(navigationLayout_);
+
+  layout_ = new QHBoxLayout;
+  layout_->setSpacing(0);
+  layout_->addStretch(3);
+  layout_->addWidget(navigationWidget_);
+  layout_->addStretch(1);
+  layout_->addWidget(indexSpinBox_);
+  layout_->addStretch(1);
+  layout_->addWidget(indexLabel_);
+  layout_->addStretch(3);
+
+  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
+  setLayout(layout_);
+}
+
+void
+GlyphIndexSelector::createConnections()
+{
+  connect(indexSpinBox_, QOverload<int>::of(&QSpinBox::valueChanged), 
+          this, &GlyphIndexSelector::emitValueChanged);
+
+  glyphNavigationMapper_ = new QSignalMapper(this);
+  connect(glyphNavigationMapper_, &QSignalMapper::mappedInt,
+          this, &GlyphIndexSelector::adjustIndex);
+
+  connect(toStartButton_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toM1000Button_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toM100Button_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toM10Button_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toM1Button_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toP1Button_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toP10Button_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toP100Button_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toP1000Button_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+  connect(toEndButton_, &QPushButton::clicked,
+          glyphNavigationMapper_, QOverload<>::of(&QSignalMapper::map));
+
+  glyphNavigationMapper_->setMapping(toStartButton_, -0x10000);
+  glyphNavigationMapper_->setMapping(toM1000Button_, -1000);
+  glyphNavigationMapper_->setMapping(toM100Button_, -100);
+  glyphNavigationMapper_->setMapping(toM10Button_, -10);
+  glyphNavigationMapper_->setMapping(toM1Button_, -1);
+  glyphNavigationMapper_->setMapping(toP1Button_, 1);
+  glyphNavigationMapper_->setMapping(toP10Button_, 10);
+  glyphNavigationMapper_->setMapping(toP100Button_, 100);
+  glyphNavigationMapper_->setMapping(toP1000Button_, 1000);
+  glyphNavigationMapper_->setMapping(toEndButton_, 0x10000);
+}
+
+
+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
new file mode 100644
index 0000000..9ef1d08
--- /dev/null
+++ b/src/ftinspect/widgets/glyphindexselector.hpp
@@ -0,0 +1,78 @@
+// glyphindexselector.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include <functional>
+#include <QWidget>
+#include <QPushButton>
+#include <QSpinBox>
+#include <QSignalMapper>
+#include <QHBoxLayout>
+#include <QLabel>
+
+class GlyphIndexSelector
+: public QWidget
+{
+  Q_OBJECT
+public:
+  GlyphIndexSelector(QWidget* parent);
+  ~GlyphIndexSelector() override = default;
+
+  // Will never trigger repaint!
+  void setMinMax(int min, int max);
+  void setShowingCount(int showingCount);
+
+  // Single mode will display single glyph index instead of a range.
+  void setSingleMode(bool singleMode);
+
+  void setCurrentIndex(int index, bool forceUpdate = false);
+  int currentIndex();
+
+  void setNumberRenderer(std::function<QString(int)> renderer);
+
+signals:
+  void currentIndexChanged(int index);
+
+protected:
+  void resizeEvent(QResizeEvent* event) override;
+  
+private:
+  bool singleMode_ = true;
+  int showingCount_;
+  std::function<QString(int)> numberRenderer_;
+
+  // min, max and current status are held by `indexSpinBox_`
+  QWidget* navigationWidget_;
+  QPushButton* toEndButton_;
+  QPushButton* toM1000Button_;
+  QPushButton* toM100Button_;
+  QPushButton* toM10Button_;
+  QPushButton* toM1Button_;
+  QPushButton* toP1000Button_;
+  QPushButton* toP100Button_;
+  QPushButton* toP10Button_;
+  QPushButton* toP1Button_;
+  QPushButton* toStartButton_;
+
+  QLabel* indexLabel_;
+  QSpinBox* indexSpinBox_;
+
+  QHBoxLayout* navigationLayout_;
+  QHBoxLayout* layout_;
+
+  QSignalMapper* glyphNavigationMapper_;
+
+  void createLayout();
+  void createConnections();
+
+  void adjustIndex(int delta);
+  void emitValueChanged();
+  void updateLabel();
+
+  static QString renderNumberDefault(int i);
+};
+
+
+// end of glyphindexselector.hpp



reply via email to

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