Subject: Re: string-pixel-width limitations (was: bug#51766: 29.0.50; Return value of buffer-chars-modified-tick changes when buffer text is not yet changed before inserting a character for non-latin input methods)

From: Ihor Radchenko <[email-masked]>
Date: Tue, 21 Jun 2022 20:39:10 +0800

Eli Zaretskii <[email-masked]> writes:

>> >> Because string width in different buffers may be different depending on
>> >> the fontification, frame font size, face remapping,
>> >> wrap-prefix/line-prefix string properties (AFAIK, the built-in
>> >> string-pixel-width will return incorrect value on string with such
>> >> properties), invisibility specs in the buffer, line numbers mode, etc
>> >> We have implemented a number of workarounds in org-string-width on main,
>> >> but I am not 100% sure that I covered all the edge cases.
>> >
>> > If you need such high accuracy, may I suggest window-text-pixel-size?
>> 
>> window-text-pixel-size suffers from the same issues with
>> wrap-prefix/line-prefix and line numbers mode.
>
> What issue are those?

The length of line-prefix is added to the return value of
window-text-pixel-size, which makes it wrong when the intention is
measuring the actual string width.

Not that window-text-pixel-size is misbehaving here - line-prefix does
need to be included if the intention is to query the actual full width
of text in buffer. The same goes for line numbers mode - line numbers
are a part of window size.

However, using window-text-pixel-size becomes awkward as the means to
measure expected string width when it is going to be inserted into
current buffer.

>> Also, in order to use it in current buffer on not-yet-inserted string,
>> you need to insert it. That where the issue in valign originated from.
>
> The usual method is to use a temporary buffer.

Yes, and it is not accurate because temporary buffer may not have the
same local environment for face remapping, invisibility specs,
char-property-alias, etc

To show all the trickery, let me share org-string-width I had to
implement for the purposes of Org mode. I did not want this function to
be complex and every single extra LOC there is fixing some edge case,
test failure, or bug report:

(defun org-string-width (string &optional pixels)
  "Return width of STRING when displayed in the current buffer.
Return width in pixels when PIXELS is non-nil."
  (if (and (version< emacs-version "28") (not pixels))
      ;; FIXME: Fallback to old limited version, because
      ;; `window-pixel-width' is buggy in older Emacs.
      (org--string-width-1 string)
    ;; Wrap/line prefix will make `window-text-pizel-size' return too
    ;; large value including the prefix.
    (remove-text-properties 0 (length string)
                            '(wrap-prefix t line-prefix t)
                            string)
    ;; Face should be removed to make sure that all the string symbols
    ;; are using default face with constant width.  Constant char width
    ;; is critical to get right string width from pixel width (not needed
    ;; when PIXELS are requested though).
    (unless pixels
      (remove-text-properties 0 (length string) '(face t) string))
    (let (;; We need to remove the folds to make sure that folded table
          ;; alignment is not messed up.
          (current-invisibility-spec
           (or (and (not (listp buffer-invisibility-spec))
                    buffer-invisibility-spec)
               (let (result)
                 (dolist (el buffer-invisibility-spec)
                   (unless (or (memq el
                                     '(org-fold-drawer
                                       org-fold-block
                                       org-fold-outline))
                               (and (listp el)
                                    (memq (car el)
                                          '(org-fold-drawer
                                            org-fold-block
                                            org-fold-outline))))
                     (push el result)))
                 result)))
          (current-char-property-alias-alist char-property-alias-alist))
      (with-temp-buffer
        (setq-local display-line-numbers nil)
        (setq-local buffer-invisibility-spec
                    (if (listp current-invisibility-spec)
                        (mapcar (lambda (el)
                                  ;; Consider elipsis to have 0 width.
                                  ;; It is what Emacs 28+ does, but we have
                                  ;; to force it in earlier Emacs versions.
                                  (if (and (consp el) (cdr el))
                                      (list (car el))
                                    el))
                                current-invisibility-spec)
                      current-invisibility-spec))
        (setq-local char-property-alias-alist
                    current-char-property-alias-alist)
        (let (pixel-width symbol-width)
          (with-silent-modifications
            (setf (buffer-string) string)
            (setq pixel-width
                  (if (get-buffer-window (current-buffer))
                      (car (window-text-pixel-size
                            nil (line-beginning-position) (point-max)))
                    (set-window-buffer nil (current-buffer))
                    (car (window-text-pixel-size
                          nil (line-beginning-position) (point-max)))))
            (unless pixels
              (setf (buffer-string) "a")
              (setq symbol-width
                    (if (get-buffer-window (current-buffer))
                        (car (window-text-pixel-size
                              nil (line-beginning-position) (point-max)))
                      (set-window-buffer nil (current-buffer))
                      (car (window-text-pixel-size
                            nil (line-beginning-position) (point-max)))))))
          (if pixels
              pixel-width
            (/ pixel-width symbol-width)))))))

Best,
Ihor