domtronn / all-the-icons.el

A utility package to collect various Icon Fonts and propertize them within Emacs.
MIT License
1.48k stars 177 forks source link

Feature Request: Scaling icons to avoid fixed width violations (i.e. no "wide" characters) #82

Open ebpa opened 7 years ago

ebpa commented 7 years ago

Like others, I've encountered issues with inserted icons throwing off alignment of text because icons are occasionally wider than fixed width characters. You could argue that this is a bug in Emacs and should already be scaled (although the current behavior is arguably desirable in many cases as well). For all-the-icons icons I have used the following logic to generate scaling coefficients for all icons in advance, so that when inserted they perfectly fill a fixed width character. It renders and measures each icon in a buffer to get each icon's dimensions.

(defvar all-the-icons--size-data
  (make-hash-table :test #'equal)
  "Icon size data (WIDTH . HEIGHT).  Keys formatted as (FAMILY . ICON-NAME)")

(defun build-icon-dimension-data ()
  "Build a database of rendered icon dimensions."
  (interactive)
  (require 'all-the-icons)
  (save-current-buffer
    (let ((buffer (get-buffer-create "*icon-indexing*")))
      (switch-to-buffer buffer)
      (delete-region (point-min) (point-max))
      (let ((line-height (line-pixel-height))
            (char-width (progn (insert " ")
                               (segment-pixel-width (point-min) (point-max)))))
        (mapc
         (lambda (family)
           (let ((family-name (funcall (all-the-icons--family-name family)))
                 (data-alist (funcall (all-the-icons--data-name family))))
             (mapcar
              (-lambda ((name . icon))
                (insert (propertize icon 'font-lock-ignore t 'face '(:height 4.0)))
                (puthash (cons family name)
                         (cons (/ (segment-pixel-width (point-min) (point-max)) (* char-width 4.0))
                               (/ (line-pixel-height) (* line-height 4.0)))
                         all-the-icons--size-data)
                (delete-region (point-min) (point-max)))
              data-alist)))
         all-the-icons-font-families))
      (kill-buffer buffer))))

(defun segment-pixel-width (start end)
  "Calculate and return the width (in pixels) of the segment between START and END."
  (save-current-buffer
    (save-excursion
      (when (markerp start)
        (switch-to-buffer (marker-buffer start))
        (set-buffer (marker-buffer start)))
      (-let* ((start-x (window-x-pixel-position start))
              (end-x (window-x-pixel-position end)))
        (when (and start-x end-x)
          (- end-x start-x))))))

(defun segment-pixel-height (start end)
  "Calculate and return the height (in pixels) of the segment between START and END."
  (save-current-buffer
    (save-excursion
      (when (markerp start)
        (switch-to-buffer (marker-buffer start))
        (set-buffer (marker-buffer start)))
      (-let* ((start-y (window-y-pixel-position start))
              (end-y (window-y-pixel-position end)))
        (when (and start-y end-y)
          (- end-y start-y))))))

(defun window-x-pixel-position (pos)
  (car (progn (goto-char pos)
              (or (window-absolute-pixel-position pos)
                  (progn (redisplay)
                         (window-absolute-pixel-position pos))))))

(defun window-y-pixel-position (pos)
  (cdr (progn (goto-char pos)
              (or (window-absolute-pixel-position pos)
                  (progn (redisplay)
                         (window-absolute-pixel-position pos))))))

A basic function to insert the icon:

(defun my/insert-icon (icon-set icon-name)
  "Insert ICON-NAME (ex: \"barcode\") from ICON-SET (ex: 'faicon)"
  (-let* (((scale-width . scale-height) (gethash (cons icon-set icon-name) all-the-icons--size-data)))
    ;; TODO: revise scaling logic
    ;;   - avoid making tall icons (use fixed-width w<1 and h/w>1)
    (propertize 
     (assoc-default icon-name (funcall (all-the-icons--data-name icon-set)))
     'font-lock-ignore t
     'face `((:family ,(funcall (all-the-icons--family-name icon-set))
                      :height ,(/ 1 scale-width))))))

I think this method could also be applied to overly-wide unicode characters as well, but have not done so yet.

domtronn commented 7 years ago

Hey @ebpa! I really like the sound of this - Thanks for all the hard work you've put into this.

I'll have a play with it when I get some spare time because this sounds like it would solve a lot of people's issues with icon fonts :)

Linuus commented 6 years ago

Any progress here? :)

wyuenho commented 5 years ago

@ebpa Care to open a PR? How's the perf of these functions? How much slower will icon insertion be?

ebpa commented 5 years ago

@wyuenho It takes less than three seconds on my machine to calculate scaling factors for 2853 icons. Emacs performance may have changed in the time since creating this ticket (I seem to remember it taking longer originally). In my local setup I persist these scaling factors to disk and reload them. Relevant code is in the dev branch of my tui package: https://github.com/ebpa/tui.el/blob/77c016c25664ff62206bbfe73414a137c351aa93/components/tui-icon.el#L15-L64

Disk persistence is probably unnecessary complexity. It makes sense for all-the-icons to just have a single memoized scaling factor calculation function (maybe: (all-the-icons--icon-scaling-factor family-name icon-name)).

I'd be willing to create a PR to include the relevant bit of logic I've written, but it's pretty quick-and-dirty as it stands. Aspects for improvement:

All that aside, a trivial implementation: A calculated scaling factor could optionally (per config) be used in place of the current combination of all-the-icons-scale-factor and the default parameter height of 1.0.

What do you think, @domtronn ? I've other API design thoughts if you're interested.

goranmoomin commented 5 years ago

Any updates?̊̈ I really would appreciate this... BTW if some icons are sufficiently wide, what about making all icons to two-character width?

goranmoomin commented 5 years ago

Okay, I was so annoyed with this that I tried to 'fix' this based on the snippet @ebpa has proposed: Evaluating this code snippet in your *scratch*will

  1. Show you an animation of icons (If you don't want this, delete the (redisplay) from build-icon-dimention-data) emacs-icon-animation-crop opt

  2. Fix most issues with width (but this makes height different between icons :-(). This is a sample screenshot with all-the-icons-dired-mode:

    스크린샷 2019-07-31 오전 2 13 30

Beware: the code taked about a minute to execute (removing redisplay will make this half, but 30s is still too much) so putting this into init file is discouraged.

(require 'all-the-icons)

(defvar all-the-icons--size-data
  (make-hash-table :test #'equal)
  "Icon size data (WIDTH . HEIGHT).  Keys formatted as (FAMILY . ICON-NAME)")

(defun segment-pixel-width (start end)
  "Calculate and return the width (in pixels) of the segment between START and END."
  (save-current-buffer
    (save-excursion
      (when (markerp start)
        (switch-to-buffer (marker-buffer start))
        (set-buffer (marker-buffer start)))
      (-let* ((start-x (window-x-pixel-position start))
              (end-x (window-x-pixel-position end)))
        (when (and start-x end-x)
          (- end-x start-x))))))

(defun segment-pixel-height (start end)
  "Calculate and return the height (in pixels) of the segment between START and END."
  (save-current-buffer
    (save-excursion
      (when (markerp start)
        (switch-to-buffer (marker-buffer start))
        (set-buffer (marker-buffer start)))
      (-let* ((start-y (window-y-pixel-position start))
              (end-y (window-y-pixel-position end)))
        (when (and start-y end-y)
          (- end-y start-y))))))

(defun window-x-pixel-position (pos))
  (car (progn (goto-char pos)
              (or (window-absolute-pixel-position pos)
                  (progn (redisplay)
                         (window-absolute-pixel-position pos)))))

(defun window-y-pixel-position (pos)
  (cdr (progn (goto-char pos)
              (or (window-absolute-pixel-position pos)
                  (progn (redisplay)
                         (window-absolute-pixel-position pos))))))

(defun build-icon-dimension-data ()
  "Build a database of rendered icon dimensions."
  (interactive)
  (save-current-buffer
    (let ((buffer (get-buffer-create "*icon-indexing*")))
      (switch-to-buffer buffer)
      (delete-region (point-min) (point-max))
      (let ((line-height (line-pixel-height)) ; 15
            (char-width (progn (insert " ") (segment-pixel-width (point-min) (point-max))))) ; 7
        (mapc
         (lambda (family)
           (let ((family-name (funcall (all-the-icons--family-name family)))
                 (data-alist (funcall (all-the-icons--data-name family))))
             (mapcar
              (-lambda ((name . icon))
                (delete-region (point-min) (point-max))
                (insert (propertize icon 'font-lock-ignore t 'face `(:family ,family-name :height 4.0)))
                (redisplay)
                (puthash (cons family name)
                         (cons (/ (segment-pixel-width (point-min) (point-max)) (* char-width 4.0))
                               (/ (line-pixel-height) (* line-height 4.0)))
                         all-the-icons--size-data))
              data-alist)))
         all-the-icons-font-families))
      (kill-buffer buffer))))

(build-icon-dimension-data)

(defmacro define-icon (name alist family &optional font-name)
  "Macro to generate functions for inserting icons for icon set NAME.

NAME defines is the name of the iconset and will produce a
function of the for `all-the-icons-NAME'.

ALIST is the alist containing maps between icon names and the
UniCode for the character.  All of these can be found in the data
directory of this package.

FAMILY is the font family to use for the icons.
FONT-NAME is the name of the .ttf file providing the font, defaults to FAMILY."
  `(progn
     (add-to-list 'all-the-icons-font-families (quote ,name))
     (add-to-list 'all-the-icons-font-names (quote ,(downcase (format "%s.ttf" (or font-name family)))))
     (defun ,(all-the-icons--family-name name) () ,family)
     (defun ,(all-the-icons--data-name name) () ,alist)
     (defun ,(all-the-icons--function-name name) (icon-name &rest args)
       (let* ((icon (cdr (assoc icon-name ,alist)))
              (other-face (when all-the-icons-color-icons (plist-get args :face)))
              (scale (car (gethash (cons ',name icon-name) all-the-icons--size-data)))
              (height (/ (if (> scale 1.2) 2 1) scale))
              (v-adjust (/ (* (if (> scale 1.2) 2 1) (or (plist-get args :v-adjust) all-the-icons-default-adjust)) scale))
              (family ,family))
         (unless icon
           (error (format "Unable to find icon with name `%s' in icon set `%s'" icon-name (quote ,name))))
         (let ((face (if other-face
                         `(:family ,family :height ,height :inherit ,other-face)
                       `(:family ,family :height ,height))))
           (propertize icon
                       'face face           ;so that this works without `font-lock-mode' enabled
                       'font-lock-face face ;so that `font-lock-mode' leaves this alone
                       'display `(raise ,v-adjust)
                       'rear-nonsticky t))))
     (defun ,(all-the-icons--insert-function-name name) (&optional arg)
       ,(format "Insert a %s icon at point." family)
       (interactive "P")
       (all-the-icons-insert arg (quote ,name)))))

(define-icon alltheicon all-the-icons-data/alltheicons-alist "all-the-icons")
(define-icon fileicon all-the-icons-data/file-icon-alist "file-icons")
(define-icon faicon all-the-icons-data/fa-icon-alist "FontAwesome")
(define-icon octicon all-the-icons-data/octicons-alist "github-octicons" "octicons")
(define-icon wicon all-the-icons-data/weather-icons-alist "Weather Icons" "weathericons")
(define-icon material all-the-icons-data/material-icons-alist "Material Icons" "material-design-icons")
wedens commented 4 years ago

Is there a font-level (a script that modifies icon fonts) fix that does something similar to elisp code above?

goranmoomin commented 4 years ago

@wedens No, there isn’t for now — I think you might do something similar with fontforge as AFAIK it has a python API? Unfortunately, I don’t use this package anymore, so I’m not going to take my time for this.