My laptop has a 4K screen therefore a very high DPI, much higher than my external monitors. So when I switch from using external monitors to the builtin laptop screen I would like to quickly scale the text and, if possible, all the GUI widgets in Emacs to suite the new DPI. I'm aware of set-frame-font but I haven't found a corresponding function that adjusts the scaling aswell. And text-scale-adjust only works on a buffer locally.
4 Answers
You can set the value of
default-frame-alist
, including itsfont
parameter, conditionally, depending on the current monitor/display. (You can use functiondisplay-monitor-attributes-list
to give you the monitor/display information.) This lets you use different default font sizes for different monitors or other different contexts.You can also change font size dynamically, for all windows and buffers shown in any frame, using command
zoom-in
,zoom-out
, orzoom-in/out
, from libraryzoom-frm.el
.These same commands can either zoom a buffer (in all windows, on whatever frame) or a frame (all of its windows, whatever their buffer). See Emacs Wiki page Set Fonts.
I'm using something similar to @gavenkoa (by pure coincidence also with a helper named my-dpi
!) to make new frames get the right size, but calling on zoom-frm
since I use that package anyway. It also works on multiheaded setups, by using frame-monitor-attributes
instead of display-monitor-attributes-list
:
(defun my-dpi (&optional frame)
"Get the DPI of FRAME (or current if nil)."
(cl-flet ((pyth (lambda (w h)
(sqrt (+ (* w w)
(* h h)))))
(mm2in (lambda (mm)
(/ mm 25.4))))
(let* ((atts (frame-monitor-attributes frame))
(pix-w (cl-fourth (assoc 'geometry atts)))
(pix-h (cl-fifth (assoc 'geometry atts)))
(pix-d (pyth pix-w pix-h))
(mm-w (cl-second (assoc 'mm-size atts)))
(mm-h (cl-third (assoc 'mm-size atts)))
(mm-d (pyth mm-w mm-h)))
(/ pix-d (mm2in mm-d)))))
(defvar my-zoom-frm-wanted-dpi 70
"The DPI I want to achieve when using `my-zoom-frm-by-dpi'.")
(defun my-zoom-frm-by-dpi (&optional frame)
"Zoom FRAME so the DPI is closer to `my-zoom-frm-wanted-dpi'."
(interactive)
(let ((frame (or frame (selected-frame))))
(when (frame-parameter frame 'zoomed)
(zoom-frm-unzoom frame))
(let ((frame-zoom-font-difference (1- (round (/ (my-dpi frame)
my-zoom-frm-wanted-dpi)))))
(when (called-interactively-p 'interactive)
(message "Zooming by %S" frame-zoom-font-difference))
(zoom-frm-in frame))))
;; Apply the scaling I want to each newly created frame:
(add-hook 'after-make-frame-functions #'my-zoom-frm-by-dpi)
-
If there is a way to trigger this automatically when frame is first displayed at different display and DPI has changed?– krokodilCommented Nov 12, 2019 at 20:44
-
(add-hook 'after-make-frame-functions #'my-zoom-frm-by-dpi)
will make it run on creating the frame. I don't know if there's a hook for moving an existing frame across displays, if that's what you meant.– unhammerCommented Nov 13, 2019 at 8:57 -
3
-
2I found that on Windows, the my-dpi function is a little more accurate if you use the 'workarea' attribute instead of the 'geometry' attribute. This is because the 'mm-size' attribute is the size of the area not obscured by taskbars and so on.– glmorousCommented Nov 20, 2019 at 17:59
-
I made my config portable across different laptops/desktops with:
(defun my-dpi ()
(let* ((attrs (car (display-monitor-attributes-list)))
(size (assoc 'mm-size attrs))
(sizex (cadr size))
(res (cdr (assoc 'geometry attrs)))
(resx (- (caddr res) (car res)))
dpi)
(catch 'exit
;; in terminal
(unless sizex
(throw 'exit 10))
;; on big screen
(when (> sizex 1000)
(throw 'exit 10))
;; DPI
(* (/ (float resx) sizex) 25.4))))
(defun my-preferred-font-size ()
(let ( (dpi (my-dpi)) )
(cond
((< dpi 110) 10)
((< dpi 130) 11)
((< dpi 160) 12)
(t 12))))
(defvar my-preferred-font-size (my-preferred-font-size))
(defvar my-regular-font
(cond
((eq window-system 'x) (format "DejaVu Sans Mono-%d:weight=normal" my-preferred-font-size))
((eq window-system 'w32) (format "Courier New-%d:antialias=none" my-preferred-font-size))))
(defvar my-symbol-font
(cond
((eq window-system 'x) (format "DejaVu Sans Mono-%d:weight=normal" my-preferred-font-size))
((eq window-system 'w32) (format "DejaVu Sans Mono-%d:antialias=none" my-preferred-font-size))))
(cond
((eq window-system 'x)
(if (and (fboundp 'find-font) (find-font (font-spec :name my-regular-font)))
(set-frame-font my-regular-font)
(set-frame-font "7x14")))
((eq window-system 'w32)
(set-frame-font my-regular-font)
(set-fontset-font nil 'cyrillic my-regular-font)
(set-fontset-font nil 'greek my-regular-font)
(set-fontset-font nil 'phonetic my-regular-font)
(set-fontset-font nil 'symbol my-symbol-font)))
Though I haven't tried to make it works in multiheaded setup. Don't know if it's ever possible.
-
1This actually works better for me when fixing the scaling factor in my-dpi from 25.4 to 17.2 (since emacs using xrandr for monitor dimensions, not xdpyinfo)– Greg BCommented May 5, 2020 at 12:22
-
Can you please describe in more details what your code does. Or minimally use in-code comments to describe it. It is hard to understand for newbies.– buhtzCommented Oct 29, 2021 at 7:47
Emacs interprets font size integers as a pixel size and floats as a point size. So if you use a desktop environment that supports display scaling, you can simply set the Emacs font size as float and it will scale properly across displays.
(set-frame-font (font-spec :family "Fira Code" :size 12.0 :weight 'normal :width 'normal))