From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp0 ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms0.migadu.com with LMTPS id WH3vDd+Sn2CdRgEAgWs5BA (envelope-from ) for ; Sat, 15 May 2021 11:22:39 +0200 Received: from aspmx1.migadu.com ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp0 with LMTPS id yJOfCd+Sn2BbIQAA1q6Kng (envelope-from ) for ; Sat, 15 May 2021 09:22:39 +0000 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 274C8220DF for ; Sat, 15 May 2021 11:22:38 +0200 (CEST) Received: from localhost ([::1]:47496 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1lhqVJ-0001nM-B5 for larch@yhetil.org; Sat, 15 May 2021 05:22:37 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:56058) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1lhqUl-0001lz-CP for emacs-orgmode@gnu.org; Sat, 15 May 2021 05:22:03 -0400 Received: from mail-wm1-x330.google.com ([2a00:1450:4864:20::330]:37584) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1lhqUi-0004hG-8t for emacs-orgmode@gnu.org; Sat, 15 May 2021 05:22:03 -0400 Received: by mail-wm1-x330.google.com with SMTP id k5-20020a05600c4785b0290174b7945d7eso497393wmo.2 for ; Sat, 15 May 2021 02:21:59 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:subject:in-reply-to:references:date:message-id:mime-version; bh=CeOuAx5kl45ftjnrNb0mWugyYn0ehqvnpsNPJJAV7Wc=; b=I0+2a4JJ4RqPXoXtAdJjzrf7c7IHPg9XjXXSyX33Z2DubaRi3lS9KJrHusUb5GHMPP GLSXq4wSbKi9wzRM6ClQFkkjiffKs5I3jXnGFh8GQX7Go0+Rju/685NzrXm6B2IwcTc/ lbsDsA+W7Pfn/RJyaMZqDmHNtfVOtNiCA0i8ZR2yaK+RUiK5b4TFP7Y89jei6AwR5OFg +lxIDi3La0u2cwqXGyTFJF41oAQeotq6C+prRa0ZnY72l289kFLdA6kmT+uaRNogCjsW A1hiJs0q5sXRXHS91C9OUPm2pnz6usxYSscEWKqNNOEdszSoSEZ72MJ/eL/BmDI12+Ni Li9Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:subject:in-reply-to:references:date :message-id:mime-version; bh=CeOuAx5kl45ftjnrNb0mWugyYn0ehqvnpsNPJJAV7Wc=; b=GUQtyXy55jzEpASYlTKsZZTgy2uqKqCTOZnoQvtbstazSwZ2HyK1+opVce02tZZ8Y7 qlRqeZ58S04Sh550rcY2R3z4eCkXviRL7HwBOXWggEM/nlkJ23yowZeRdqEJ0hxv0ulb RUXhsFk0vs5u5hqYwT3eeBFp+Dk+hI3rXpjp42ahiRr1TWhK388NRYnjPzT6Nq/Oa+EB JzE4dd7oqRtfPuyxwUIOv9VBTWPSyZ4ouvYr+OkstJmrXLuqZvQqa73cfvSrP82ERRy3 IjKzXZIqOJVsE8w33Tj5yVw+0jb7Kzox61zGUc/jo/cQrWwBcTmrXYI2WxJS/hyWeTtk BI3Q== X-Gm-Message-State: AOAM533ViMDrI7vIARYCrGsoSJjkNa953lZJMBciKCFTpZiZCd1sugIA FMEGFupJHBZlE7ZOcL6iYE8= X-Google-Smtp-Source: ABdhPJxj6eOeppKCKMmqqSupME3MB60fxKxaZy2DC44SvS0xd3ekUdWQaZTtZ6iH+P3UrGdYdI2U5A== X-Received: by 2002:a05:600c:2290:: with SMTP id 16mr52528243wmf.80.1621070518160; Sat, 15 May 2021 02:21:58 -0700 (PDT) Received: from localhost ([141.105.67.194]) by smtp.gmail.com with ESMTPSA id y11sm995022wrh.54.2021.05.15.02.21.56 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 15 May 2021 02:21:57 -0700 (PDT) From: Ihor Radchenko To: fr ml , emacs-orgmode@gnu.org Subject: [PATCH] Pixel-wise table alignment In-Reply-To: <2c2f2204-14b1-49b2-658b-2b8b75ab86b9@t-online.de> References: <023ea6f8-958c-32cc-3d51-de66d52df091@t-online.de> <87im3ubnt9.fsf@localhost> <87czu17hus.fsf@localhost> <5ebfdc6f-6e40-1470-4379-4a5a2b666aa7@t-online.de> <87czu1pivb.fsf@localhost> <6c4c8084-3800-c085-b724-f5653d7c20bd@t-online.de> <87eeeh17fq.fsf@localhost> <8975eded-5a22-cb9c-b85e-f3b523f16411@t-online.de> <871ragff66.fsf@localhost> <84936763-9384-7e6c-5304-b94217298b9a@t-online.de> <87lf8o9pgn.fsf@localhost> <87im3s9ie8.fsf@localhost> <2c2f2204-14b1-49b2-658b-2b8b75ab86b9@t-online.de> X-Woof-Help: Help testing table alignment with variable-pitch/unicode fonts Date: Sat, 15 May 2021 17:26:44 +0800 Message-ID: <8735uos5yj.fsf@localhost> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Received-SPF: pass client-ip=2a00:1450:4864:20::330; envelope-from=yantar92@gmail.com; helo=mail-wm1-x330.google.com X-Spam_score_int: -17 X-Spam_score: -1.8 X-Spam_bar: - X-Spam_report: (-1.8 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_ENVFROM_END_DIGIT=0.25, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-orgmode@gnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: "General discussions about Org-mode." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-orgmode-bounces+larch=yhetil.org@gnu.org Sender: "Emacs-orgmode" X-Migadu-Flow: FLOW_IN ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=yhetil.org; s=key1; t=1621070558; h=from:from:sender:sender:reply-to:subject:subject:date:date: message-id:message-id:to:to:cc:mime-version:mime-version: content-type:content-type:in-reply-to:in-reply-to: references:references:list-id:list-help:list-unsubscribe: list-subscribe:list-post:dkim-signature; bh=CeOuAx5kl45ftjnrNb0mWugyYn0ehqvnpsNPJJAV7Wc=; b=eC/z9DA3LGzpNJvq7+8B9BW28VApia2Ydy1I07Ltk81/YnkU0YoZm2GALcqML4btb6Z0MS EsUf8IRvjYUYo2F8GcXtxWuArcs3vXdMRc1FLQPA4jyL6zlFW8na4hr8w/d0M28rlDoz+D IqzLtqH9OThyFt+XKLGltRJPMmln8vLmH4r3fjP/Rjahv/LY5JLzTriClMSK7qWK9c9wKJ EHnqPxYyKj9QanVIrgmsLfjXNIuinc0ppdnkr+KF0dpmXKOPShYEaOtp1eJWLlmO1tptwL BS+Iuc6ixUAFP9jJkWFXjh/+QCYWk7WwQMFTOyKy2vQJT7jOh8Tk1+JW76h8oA== ARC-Seal: i=1; s=key1; d=yhetil.org; t=1621070558; a=rsa-sha256; cv=none; b=aJs58enjUthKRmNbH2bzcv/XvqRQx61pIIwn4DFvlW2Zlp7R62GJzESyllvm9ycI4R5xxY hveC3UihWigGbTlZKSf3GT7/7NZPlkTmIctkgxxjaiQSTwbM1nl8soXKGelVdhouvDmCSC H5s6VtOP+lWENATDVo+vQFD1IcvzbBk9caZWUoqL7aFGj9PoS9pEXRoBRk1zu/xdtZgmeL tLGcVraqWpc3IFNwcyXfktpvECeTWXzcjdpQgBRAJw+KV+yNrs4IsfJuXrGGKS2fvmmlHz n3yFL2zqB8wkqyCRuSPBwPawliiNsWkRQHHQKOuTll2Z9WbjG8oVViUemnoqtQ== ARC-Authentication-Results: i=1; aspmx1.migadu.com; dkim=pass header.d=gmail.com header.s=20161025 header.b=I0+2a4JJ; dmarc=pass (policy=none) header.from=gmail.com; spf=pass (aspmx1.migadu.com: domain of emacs-orgmode-bounces@gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=emacs-orgmode-bounces@gnu.org X-Migadu-Spam-Score: -2.65 Authentication-Results: aspmx1.migadu.com; dkim=pass header.d=gmail.com header.s=20161025 header.b=I0+2a4JJ; dmarc=pass (policy=none) header.from=gmail.com; spf=pass (aspmx1.migadu.com: domain of emacs-orgmode-bounces@gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=emacs-orgmode-bounces@gnu.org X-Migadu-Queue-Id: 274C8220DF X-Spam-Score: -2.65 X-Migadu-Scanner: scn0.migadu.com X-TUID: U199OUKeJzPO --=-=-= Content-Type: text/plain For everyone interested in mode precise table alignment, The attached patch should allow table columns to be aligned pixel-wise. It means, that tables should look much better with variable-pitch fonts and unicode symbols. I would like to get more feedback about how the patch works for different setups. It appears to work flawlessly for me, yet Frank reported issues (even with emacs -Q). I need more datapoints to figure out how to fix the patch. An example table that requires pixel alignment is attached. Please, try to align the table and report if there are any issues. For me, the aligned table looks like in test_screenshot2.png. It should not look like test_screenshot.png. If it is, I would like to know the Emacs version, OS, and results using emacs -Q, if possible. fr ml writes: > I'm using Linux. I've added an org file with a screenshot of it. Thanks! Best, Ihor --=-=-= Content-Type: application/vnd.lotus-organizer; charset=utf-8 Content-Disposition: inline; filename=test.org Content-Transfer-Encoding: base64 fCAgMSB8IDEyIHwKfCAgMiB8INip2KkgfAp8ICAzIHwg2YjZiCB8CnwgIDQgfCDYodihIHwKfCAg NSB8INix2LEgfAp8ICA2IHwg2LLYsiB8CnwgIDcgfCDZhNijIHwKfCAgOCB8INmE2KcgfAp8ICA5 IHwg2YTYoiB8CnwgMTAgfCDZhNilIHwKfCAxMSB8IGFiIHwK --=-=-= Content-Type: image/png Content-Disposition: attachment; filename=test_screenshot2.png Content-Transfer-Encoding: base64 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 --=-=-= Content-Type: image/png Content-Disposition: attachment; filename=test_screenshot.png Content-Transfer-Encoding: base64 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 --=-=-= Content-Type: text/x-diff Content-Disposition: inline; filename=0001-Align-table-columns-pixel-wise.patch >From 8743cee4d7ee266076cfcccca0a2772aac597ee0 Mon Sep 17 00:00:00 2001 Message-Id: <8743cee4d7ee266076cfcccca0a2772aac597ee0.1621069602.git.yantar92@gmail.com> From: Ihor Radchenko Date: Sat, 15 May 2021 16:58:54 +0800 Subject: [PATCH] Align table columns pixel-wise * lisp/org-macs.el (org-string-width): Rewrite manual width calculation using `window-text-pixel-size'. Add extra optional argument to get string width in pixels. (org--string-from-props): Removed, as it is no longer needed for `org-string-width'. * lisp/org-table.el (org-table--align-field): Align field pixel-wise. The WIDTH argument should now be in pixel units. The alignment is done by setting 'display text property. (org-table-align): Align fields pixel-wise using new `org-table--align-field' and the same ideas with 'display property. --- lisp/org-macs.el | 121 ++++++++++++++++++++++------------------------ lisp/org-table.el | 69 ++++++++++++++++++++------ 2 files changed, 112 insertions(+), 78 deletions(-) diff --git a/lisp/org-macs.el b/lisp/org-macs.el index cd9fd1d83..ae79ab16c 100644 --- a/lisp/org-macs.el +++ b/lisp/org-macs.el @@ -868,71 +868,64 @@ (defun org-split-string (string &optional separators) results ;skip trailing separator (cons (substring string i) results))))))) -(defun org--string-from-props (s property beg end) - "Return the visible part of string S. -Visible part is determined according to text PROPERTY, which is -either `invisible' or `display'. BEG and END are 0-indices -delimiting S." - (let ((width 0) - (cursor beg)) - (while (setq beg (text-property-not-all beg end property nil s)) - (let* ((next (next-single-property-change beg property s end)) - (props (text-properties-at beg s)) - (spec (plist-get props property)) - (value - (pcase property - (`invisible - ;; If `invisible' property in PROPS means text is to - ;; be invisible, return 0. Otherwise return nil so - ;; as to resume search. - (and (or (eq t buffer-invisibility-spec) - (assoc-string spec buffer-invisibility-spec)) - 0)) - (`display - (pcase spec - (`nil nil) - (`(space . ,props) - (let ((width (plist-get props :width))) - (and (wholenump width) width))) - (`(image . ,_) - (and (fboundp 'image-size) - (ceiling (car (image-size spec))))) - ((pred stringp) - ;; Displayed string could contain invisible parts, - ;; but no nested display. - (org--string-from-props spec 'invisible 0 (length spec))) - (_ - ;; Un-handled `display' value. Ignore it. - ;; Consider the original string instead. - nil))) - (_ (error "Unknown property: %S" property))))) - (when value - (cl-incf width - ;; When looking for `display' parts, we still need - ;; to look for `invisible' property elsewhere. - (+ (cond ((eq property 'display) - (org--string-from-props s 'invisible cursor beg)) - ((= cursor beg) 0) - (t (string-width (substring s cursor beg)))) - value)) - (setq cursor next)) - (setq beg next))) - (+ width - ;; Look for `invisible' property in the last part of the - ;; string. See above. - (cond ((eq property 'display) - (org--string-from-props s 'invisible cursor end)) - ((= cursor end) 0) - (t (string-width (substring s cursor end))))))) - -(defun org-string-width (string) +(defun org-string-width (string &optional pixels) "Return width of STRING when displayed in the current buffer. -Unlike `string-width', this function takes into consideration -`invisible' and `display' text properties. It supports the -latter in a limited way, mostly for combinations used in Org. -Results may be off sometimes if it cannot handle a given -`display' value." - (org--string-from-props string 'display 0 (length string))) +Return width in pixels when PIXELS is non-nil." + ;; Wrap/line prefix will make `window-text-pizel-size' return too + ;; large value including the prefix. + ;; 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. + (remove-text-properties 0 (length string) + '(wrap-prefix t line-prefix t 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 + 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)))))) (defun org-not-nil (v) "If V not nil, and also not the string \"nil\", then return V. diff --git a/lisp/org-table.el b/lisp/org-table.el index cc69542f9..e8b5add8a 100644 --- a/lisp/org-table.el +++ b/lisp/org-table.el @@ -4313,12 +4313,34 @@ (defun org-table--align-field (field width align) "Format FIELD according to column WIDTH and alignment ALIGN. FIELD is a string. WIDTH is a number. ALIGN is either \"c\", \"l\" or\"r\"." - (let* ((spaces (- width (org-string-width field))) + (let* ((spaces (- width (org-string-width field 'pixels))) + (symbol-width (org-string-width " " 'pixels)) + (right-spaces (/ spaces symbol-width)) + (right-pixels (- spaces (* symbol-width right-spaces))) + (centered-spaces (/ (/ spaces 2) symbol-width)) + (centered-pixels (- (/ spaces 2) (* symbol-width centered-spaces))) (prefix (pcase align ("l" "") - ("r" (make-string spaces ?\s)) - ("c" (make-string (/ spaces 2) ?\s)))) - (suffix (make-string (- spaces (length prefix)) ?\s))) + ("r" (concat (make-string right-spaces ?\s) + ;; Align to non-fixed width. + (if (zerop right-pixels) "" + (propertize " " + 'display + `(space . (:width (,right-pixels))))))) + ("c" (concat (make-string centered-spaces ?\s) + ;; Align to non-fixed width. + (if (zerop centered-pixels) "" + (propertize " " + 'display + `(space . (:width (,centered-pixels))))))))) + (suffix-spaces (/ (- spaces (org-string-width prefix 'pixel)) symbol-width)) + (suffix-pixels (- (- spaces (org-string-width prefix 'pixel)) (* symbol-width suffix-spaces))) + (suffix (concat (make-string suffix-spaces ?\s) + ;; Align to non-fixed width. + (if (zerop suffix-pixels) "" + (propertize " " + 'display + `(space . (:width (,suffix-pixels)))))))) (concat org-table-separator-space prefix field @@ -4342,7 +4364,8 @@ (defun org-table-align () (rows (remq 'hline table)) (widths nil) (alignments nil) - (columns-number 1)) + (columns-number 1) + (symbol-width (org-string-width "-" 'pixels))) (if (null rows) ;; Table contains only horizontal rules. Compute the ;; number of columns anyway, and choose an arbitrary width @@ -4352,17 +4375,17 @@ (defun org-table-align () (while (search-forward "+" end t) (cl-incf columns-number))) (setq widths (make-list columns-number 1)) - (setq alignments (make-list columns-number "l"))) + (setq alignments (make-list (* columns-number symbol-width) "l"))) ;; Compute alignment and width for each column. (setq columns-number (apply #'max (mapcar #'length rows))) (dotimes (i columns-number) - (let ((max-width 1) + (let ((max-width symbol-width) (fixed-align? nil) (numbers 0) (non-empty 0)) (dolist (row rows) (let ((cell (or (nth i row) ""))) - (setq max-width (max max-width (org-string-width cell))) + (setq max-width (max max-width (org-string-width cell 'pixels))) (cond (fixed-align? nil) ((equal cell "") nil) ((string-match "\\`<\\([lrc]\\)[0-9]*>\\'" cell) @@ -4386,9 +4409,18 @@ (defun org-table-align () ;; Build new table rows. Only replace rows that actually ;; changed. (let ((rule (and (memq 'hline table) - (mapconcat (lambda (w) (make-string (+ 2 w) ?-)) - widths - "+"))) + (mapconcat + (lambda (w) + (let* ((hline-dahes (+ 2 (/ w symbol-width))) + (hline-pixels (- w (* symbol-width (/ w symbol-width))))) + (concat (make-string hline-dahes ?-) + ;; Align to non-fixed width. + (if (zerop hline-pixels) "" + (propertize " " + 'display + `(:width (,hline-pixels))))))) + widths + "+"))) (indent (progn (looking-at "[ \t]*|") (match-string 0)))) (dolist (row table) (let ((previous (buffer-substring (point) (line-end-position))) @@ -4408,9 +4440,18 @@ (defun org-table-align () "|"))) "|"))) (if (equal new previous) - (forward-line) - (insert new "\n") - (delete-region (point) (line-beginning-position 2)))))) + (if (equal-including-properties new previous) + (forward-line) + (let ((pos 0) next) + (while (< pos (length new)) + (setq next (or (next-single-property-change pos 'display new) + (length new))) + (when (get-text-property pos 'display new) + (put-text-property (+ pos (point)) (+ next (point)) 'display (get-text-property pos 'display new))) + (setq pos next))) + (forward-line)) + (insert new "\n") + (delete-region (point) (line-beginning-position 2)))))) (set-marker end nil) (when org-table-overlay-coordinates (org-table-overlay-coordinates)) (setq org-table-may-need-update nil)))))) -- 2.26.3 --=-=-=--