Support cons cell for 'line-spacing'

* etc/NEWS: Announce the change.
* src/dispextern.h (struct glyph_row): Add
'extra_line_spacing_above' member.
(struct it): Add 'extra_line_spacing_above' member.
* src/frame.h (struct frame): Add 'extra_line_spacing_above'
member.  Update comment for 'extra_line_spacing.'
* src/buffer.c (syms_of_buffer): Update the docstring of
'line-spacing' to describe the cons cell usage.
* src/buffer.h (struct buffer): Update comment for
'extra_line_spacing'.
* src/frame.c (gui_set_line_spacing): Handle cons cell value for
'line-spacing'.  Calculate and set 'extra_line_spacing_above'
for both integer and float pairs.
* src/xdisp.c (init_iterator): Initialize 'extra_line_spacing_above'
from buffer or frame 'line-spacing', handling cons cells for both
integer and float values.
(gui_produce_glyphs): Use 'extra_line_spacing_above' to distribute
spacing between ascent and descent.  Update 'max_extra_line_spacing'
calculation.
(resize_mini_window): Take line spacing into account when resizing the
mini window.  Pass height of a single line to 'grow_mini_window' and
'shrink_mini_window'.
* src/window.c (grow_mini_window, shrink_mini_window): Add unit
argument which defines height of a single line.
* src/window.h (grow_mini_window, shrink_mini_window): Adjust function
prototypes accordingly with unit argument.
* lisp/subr.el (total-line-spacing): New function to calculate total
spacing from a number or cons cell.
(posn-col-row): Use total-line-spacing.
* lisp/simple.el (default-line-height): Use 'total-line-spacing'.
* lisp/textmodes/picture.el (picture-mouse-set-point): Use
'total-line-spacing'.
* lisp/window.el (window-default-line-height): Use
'total-line-spacing'.
(window--resize-mini-window): Take 'line-spacing' into account.
* test/lisp/subr-tests.el (total-line-spacing): New test.
* test/src/buffer-tests.el (test-line-spacing): New test.
* doc/emacs/display.texi (Display Custom): Document that
'line-spacing' can be a cons cell.
(Line Height): Document the new cons cell format for 'line-spacing'
to allow vertical centering.

Co-authored-by: Przemysław Alexander Kamiński <alexander@kaminski.se>
Co-authored-by: Daniel Mendler <mail@daniel-mendler.de>
This commit is contained in:
Daniel Mendler
2026-01-07 17:39:16 +01:00
committed by Eli Zaretskii
parent 2145519734
commit e8f26d554b
17 changed files with 224 additions and 47 deletions

View File

@@ -2351,6 +2351,16 @@ of lines which are a multiple of certain numbers. Customize
@code{display-line-numbers-minor-tick} respectively to set those
numbers.
@vindex line-spacing
The variable @code{line-spacing} controls the vertical spacing between
lines. It can be set to an integer (specifying pixels) or a float
(specifying spacing relative to the default frame font height). You can
also set this variable to a cons cell of integers or floats, such as
@code{(@var{top} . @var{bottom})}. When set to a cons cell, the spacing
is distributed above and below the line, allowing for text to be
vertically centered within the line height. See also @ref{Line Height,,,
elisp, The Emacs Lisp Reference Manual}.
@vindex visible-bell
If the variable @code{visible-bell} is non-@code{nil}, Emacs attempts
to make the whole screen blink when it would normally make an audible bell

View File

@@ -2582,10 +2582,13 @@ the spacing relative to the frame's default line height.
@vindex line-spacing
You can specify the line spacing for all lines in a buffer via the
buffer-local @code{line-spacing} variable. An integer specifies
the number of pixels put below lines. A floating-point number
specifies the spacing relative to the default frame line height. This
overrides line spacings specified for the frame.
buffer-local @code{line-spacing} variable. An integer specifies the
number of pixels put below lines. A floating-point number specifies the
spacing relative to the default frame line height. A cons cell of
integers or floating-point numbers specifies the spacing put above and
below the line, allowing for vertically centering text. This overrides
line spacings specified for the frame.
@kindex line-spacing @r{(text property)}
Finally, a newline can have a @code{line-spacing} text or overlay

View File

@@ -82,6 +82,12 @@ other directory on your system. You can also invoke the
* Changes in Emacs 31.1
** 'line-spacing' now supports specifying spacing above the line.
Previously, only spacing below the line could be specified. The variable
can now be set to a cons cell to specify spacing both above and below
the line, which allows for vertically centering text.
+++
** 'prettify-symbols-mode' attempts to ignore undisplayable characters.
Previously, such characters would be rendered as, e.g., white boxes.

View File

@@ -7875,10 +7875,10 @@ This function uses the definition of the default face for the currently
selected frame."
(let ((dfh (default-font-height))
(lsp (if (display-graphic-p)
(or line-spacing
(default-value 'line-spacing)
(frame-parameter nil 'line-spacing)
0)
(total-line-spacing (or line-spacing
(default-value 'line-spacing)
(frame-parameter nil 'line-spacing)
0))
0)))
(if (floatp lsp)
(setq lsp (truncate (* (frame-char-height) lsp))))

View File

@@ -2027,8 +2027,9 @@ and `event-end' functions."
(let* ((spacing (when (display-graphic-p frame)
(or (with-current-buffer
(window-buffer (frame-selected-window frame))
line-spacing)
(frame-parameter frame 'line-spacing)))))
(total-line-spacing))
(total-line-spacing
(frame-parameter frame 'line-spacing))))))
(cond ((floatp spacing)
(setq spacing (truncate (* spacing
(frame-char-height frame)))))
@@ -7936,4 +7937,12 @@ and return the value found in PLACE instead."
,(funcall setter val)
,val)))))
(defun total-line-spacing (&optional line-spacing-param)
"Return numeric value of line-spacing, summing it if it's a cons.
When LINE-SPACING-PARAM is provided, calculate from it instead."
(let ((v (or line-spacing-param line-spacing)))
(pcase v
((pred numberp) v)
(`(,above . ,below) (+ above below)))))
;;; subr.el ends here

View File

@@ -235,8 +235,8 @@ Use \"\\[command-apropos] picture-movement\" to see commands which control motio
(char-ht (frame-char-height frame))
(spacing (when (display-graphic-p frame)
(or (with-current-buffer (window-buffer window)
line-spacing)
(frame-parameter frame 'line-spacing)))))
(total-line-spacing))
(total-line-spacing (frame-parameter frame 'line-spacing))))))
(cond ((floatp spacing)
(setq spacing (truncate (* spacing char-ht))))
((null spacing)

View File

@@ -2850,9 +2850,15 @@ as small) as possible, but don't signal an error."
(let* ((frame (window-frame window))
(root (frame-root-window frame))
(height (window-pixel-height window))
(min-height (+ (frame-char-height frame)
(- (window-pixel-height window)
(window-body-height window t))))
;; Take line-spacing into account if the line-spacing is
;; configured as a cons cell with above > 0 to prevent
;; mini-window jiggling.
(ls (or (buffer-local-value 'line-spacing (window-buffer window))
(frame-parameter frame 'line-spacing)))
(min-height (+ (if (and (consp ls) (> (car ls) 0))
(window-default-line-height window)
(frame-char-height frame))
(- height (window-body-height window t))))
(max-delta (- (window-pixel-height root)
(window-min-size root nil nil t))))
;; Don't make mini window too small.
@@ -9906,8 +9912,8 @@ face on WINDOW's frame."
(buffer (window-buffer window))
(space-height
(or (and (display-graphic-p frame)
(or (buffer-local-value 'line-spacing buffer)
(frame-parameter frame 'line-spacing)))
(total-line-spacing (or (buffer-local-value 'line-spacing buffer)
(frame-parameter frame 'line-spacing))))
0)))
(+ font-height
(if (floatp space-height)

View File

@@ -5875,12 +5875,15 @@ cursor's appearance is instead controlled by the variable
`cursor-in-non-selected-windows'. */);
DEFVAR_PER_BUFFER ("line-spacing",
&BVAR (current_buffer, extra_line_spacing), Qnumberp,
&BVAR (current_buffer, extra_line_spacing), Qnil,
doc: /* Additional space to put between lines when displaying a buffer.
The space is measured in pixels, and put below lines on graphic displays,
see `display-graphic-p'.
If value is a floating point number, it specifies the spacing relative
to the default frame line height. A value of nil means add no extra space. */);
to the default frame line height.
If value is a cons cell containing a pair of floats or integers,
it is interpreted as space above and below the line, respectively.
A value of nil means add no extra space. */);
DEFVAR_PER_BUFFER ("cursor-in-non-selected-windows",
&BVAR (current_buffer, cursor_in_non_selected_windows), Qnil,

View File

@@ -575,7 +575,10 @@ struct buffer
Lisp_Object cursor_type_;
/* An integer > 0 means put that number of pixels below text lines
in the display of this buffer. */
in the display of this buffer.
A float ~ 1.0 means add extra number of pixels below text lines
relative to the line height.
A cons means put car spacing above and cdr spacing below the line. */
Lisp_Object extra_line_spacing_;
#ifdef HAVE_TREE_SITTER

View File

@@ -960,6 +960,9 @@ struct glyph_row
in last row when checking if row is fully visible. */
int extra_line_spacing;
/* Part of extra_line_spacing that should go above the line. */
int extra_line_spacing_above;
/* First position in this row. This is the text position, including
overlay position information etc, where the display of this row
started, and can thus be less than the position of the first
@@ -2772,6 +2775,10 @@ struct it
window systems only.) */
int extra_line_spacing;
/* Default amount of additional space in pixels above lines (for
window systems only). */
int extra_line_spacing_above;
/* Max extra line spacing added in this row. */
int max_extra_line_spacing;

View File

@@ -5454,18 +5454,60 @@ void
gui_set_line_spacing (struct frame *f, Lisp_Object new_value, Lisp_Object old_value)
{
if (NILP (new_value))
f->extra_line_spacing = 0;
{
f->extra_line_spacing = 0;
f->extra_line_spacing_above = 0;
}
else if (RANGED_FIXNUMP (0, new_value, INT_MAX))
f->extra_line_spacing = XFIXNAT (new_value);
{
f->extra_line_spacing = XFIXNAT (new_value);
f->extra_line_spacing_above = 0;
}
else if (FLOATP (new_value))
{
int new_spacing = XFLOAT_DATA (new_value) * FRAME_LINE_HEIGHT (f) + 0.5;
int new_spacing = XFLOAT_DATA (new_value) * FRAME_LINE_HEIGHT (f);
if (new_spacing >= 0)
if (new_spacing >= 0) {
f->extra_line_spacing = new_spacing;
f->extra_line_spacing_above = 0;
}
else
signal_error ("Invalid line-spacing", new_value);
}
else if (CONSP (new_value))
{
Lisp_Object above = XCAR (new_value);
Lisp_Object below = XCDR (new_value);
/* Integer pair case. */
if (RANGED_FIXNUMP (0, above, INT_MAX)
&& RANGED_FIXNUMP (0, below, INT_MAX))
{
f->extra_line_spacing = XFIXNAT (above) + XFIXNAT (below);
f->extra_line_spacing_above = XFIXNAT (above);
}
/* Float pair case. */
else if (FLOATP (XCAR (new_value))
&& FLOATP (XCDR (new_value)))
{
int new_spacing = (XFLOAT_DATA (above) + XFLOAT_DATA (below)) * FRAME_LINE_HEIGHT (f);
int spacing_above = XFLOAT_DATA (above) * FRAME_LINE_HEIGHT (f);
if(new_spacing >= 0 && spacing_above >= 0)
{
f->extra_line_spacing = new_spacing;
f->extra_line_spacing_above = spacing_above;
}
else
signal_error ("Invalid line-spacing", new_value);
}
/* Unmatched pair case. */
else
{
signal_error ("Invalid line-spacing", new_value);
}
}
else
signal_error ("Invalid line-spacing", new_value);
if (FRAME_VISIBLE_P (f))

View File

@@ -718,9 +718,16 @@ struct frame
frame parameter. 0 means don't do gamma correction. */
double gamma;
/* Additional space to put between text lines on this frame. */
/* Additional space to put below text lines on this frame.
Also takes part in line height calculation. */
int extra_line_spacing;
/* Amount of space (included in extra_line_spacing) that goes ABOVE
line line.
IMPORTANT: Don't use this for line height calculations.
(5 . 20) means that extra_line_spacing is 25 with 5 above. */
int extra_line_spacing_above;
/* All display backends seem to need these two pixel values. */
unsigned long background_pixel;
unsigned long foreground_pixel;

View File

@@ -5894,11 +5894,11 @@ resize_mini_window_apply (struct window *w, int delta)
* line of text.
*/
void
grow_mini_window (struct window *w, int delta)
grow_mini_window (struct window *w, int delta, int unit)
{
struct frame *f = XFRAME (w->frame);
int old_height = window_body_height (w, WINDOW_BODY_IN_PIXELS);
int min_height = FRAME_LINE_HEIGHT (f);
int min_height = unit;
eassert (MINI_WINDOW_P (w));
@@ -5926,7 +5926,7 @@ grow_mini_window (struct window *w, int delta)
resize_mini_window_apply (w, -XFIXNUM (grow));
}
FRAME_WINDOWS_FROZEN (f)
= window_body_height (w, WINDOW_BODY_IN_PIXELS) > FRAME_LINE_HEIGHT (f);
= window_body_height (w, WINDOW_BODY_IN_PIXELS) > unit;
}
/**
@@ -5936,11 +5936,10 @@ grow_mini_window (struct window *w, int delta)
* line of text.
*/
void
shrink_mini_window (struct window *w)
shrink_mini_window (struct window *w, int unit)
{
struct frame *f = XFRAME (w->frame);
int delta = (window_body_height (w, WINDOW_BODY_IN_PIXELS)
- FRAME_LINE_HEIGHT (f));
int delta = (window_body_height (w, WINDOW_BODY_IN_PIXELS) - unit);
eassert (MINI_WINDOW_P (w));
@@ -5959,10 +5958,10 @@ shrink_mini_window (struct window *w)
else if (delta < 0)
/* delta can be less than zero after adding horizontal scroll
bar. */
grow_mini_window (w, -delta);
grow_mini_window (w, -delta, unit);
FRAME_WINDOWS_FROZEN (f)
= window_body_height (w, WINDOW_BODY_IN_PIXELS) > FRAME_LINE_HEIGHT (f);
= window_body_height (w, WINDOW_BODY_IN_PIXELS) > unit;
}
DEFUN ("resize-mini-window-internal", Fresize_mini_window_internal,

View File

@@ -1126,8 +1126,8 @@ extern Lisp_Object window_from_coordinates (struct frame *, int, int,
extern void resize_frame_windows (struct frame *, int, bool);
extern void restore_window_configuration (Lisp_Object);
extern void delete_all_child_windows (Lisp_Object);
extern void grow_mini_window (struct window *, int);
extern void shrink_mini_window (struct window *);
extern void grow_mini_window (struct window *, int, int);
extern void shrink_mini_window (struct window *, int);
extern int window_relative_x_coord (struct window *, enum window_part, int);
void run_window_change_functions (void);

View File

@@ -3316,13 +3316,50 @@ init_iterator (struct it *it, struct window *w,
if (base_face_id == DEFAULT_FACE_ID
&& FRAME_WINDOW_P (it->f))
{
Lisp_Object line_space_above;
Lisp_Object line_space_below;
if (FIXNATP (BVAR (current_buffer, extra_line_spacing)))
it->extra_line_spacing = XFIXNAT (BVAR (current_buffer, extra_line_spacing));
{
it->extra_line_spacing = XFIXNAT (BVAR (current_buffer, extra_line_spacing));
it->extra_line_spacing_above = 0;
}
else if (FLOATP (BVAR (current_buffer, extra_line_spacing)))
it->extra_line_spacing = (XFLOAT_DATA (BVAR (current_buffer, extra_line_spacing))
* FRAME_LINE_HEIGHT (it->f));
{
it->extra_line_spacing = (XFLOAT_DATA (BVAR (current_buffer, extra_line_spacing))
* FRAME_LINE_HEIGHT (it->f));
it->extra_line_spacing_above = 0;
}
else if (CONSP (BVAR (current_buffer, extra_line_spacing)))
{
line_space_above = XCAR (BVAR (current_buffer, extra_line_spacing));
line_space_below = XCDR (BVAR (current_buffer, extra_line_spacing));
/* Integer pair case. */
if (FIXNATP (line_space_above) && FIXNATP (line_space_below))
{
int line_space_total = XFIXNAT (line_space_below) + XFIXNAT (line_space_above);
it->extra_line_spacing = line_space_total;
it->extra_line_spacing_above = XFIXNAT (line_space_above);
}
/* Float pair case. */
else if (FLOATP (line_space_above) && FLOATP (line_space_below))
{
double line_space_total = XFLOAT_DATA (line_space_above) + XFLOAT_DATA (line_space_below);
it->extra_line_spacing = (line_space_total * FRAME_LINE_HEIGHT (it->f));
it->extra_line_spacing_above = (XFLOAT_DATA (line_space_above) * FRAME_LINE_HEIGHT (it->f));
}
/* Invalid cons. */
else
{
it->extra_line_spacing = 0;
it->extra_line_spacing_above = 0;
}
}
else if (it->f->extra_line_spacing > 0)
it->extra_line_spacing = it->f->extra_line_spacing;
{
it->extra_line_spacing = it->f->extra_line_spacing;
it->extra_line_spacing_above = it->f->extra_line_spacing_above;
}
}
/* If realized faces have been removed, e.g. because of face
@@ -13157,7 +13194,7 @@ resize_mini_window (struct window *w, bool exact_p)
else
{
struct it it;
int unit = FRAME_LINE_HEIGHT (f);
int unit;
int height, max_height;
struct text_pos start;
struct buffer *old_current_buffer = NULL;
@@ -13171,6 +13208,10 @@ resize_mini_window (struct window *w, bool exact_p)
init_iterator (&it, w, BEGV, BEGV_BYTE, NULL, DEFAULT_FACE_ID);
/* Unit includes line spacing if line spacing is added above */
unit = FRAME_LINE_HEIGHT (f) +
(it.extra_line_spacing_above ? it.extra_line_spacing : 0);
/* Compute the max. number of lines specified by the user. */
if (FLOATP (Vmax_mini_window_height))
max_height = XFLOAT_DATA (Vmax_mini_window_height) * windows_height;
@@ -13203,7 +13244,10 @@ resize_mini_window (struct window *w, bool exact_p)
}
else
height = it.current_y + it.max_ascent + it.max_descent;
height -= min (it.extra_line_spacing, it.max_extra_line_spacing);
/* Remove final line spacing in the mini-window */
if (!it.extra_line_spacing_above)
height -= min (it.extra_line_spacing, it.max_extra_line_spacing);
/* Compute a suitable window start. */
if (height > max_height)
@@ -13241,13 +13285,13 @@ resize_mini_window (struct window *w, bool exact_p)
/* Let it grow only, until we display an empty message, in which
case the window shrinks again. */
if (height > old_height)
grow_mini_window (w, height - old_height);
grow_mini_window (w, height - old_height, unit);
else if (height < old_height && (exact_p || BEGV == ZV))
shrink_mini_window (w);
shrink_mini_window (w, unit);
}
else if (height != old_height)
/* Always resize to exact size needed. */
grow_mini_window (w, height - old_height);
grow_mini_window (w, height - old_height, unit);
if (old_current_buffer)
set_buffer_internal (old_current_buffer);
@@ -24068,6 +24112,7 @@ append_space_for_newline (struct it *it, bool default_face_p)
{
Lisp_Object height, total_height;
int extra_line_spacing = it->extra_line_spacing;
int extra_line_spacing_above = it->extra_line_spacing_above;
int boff = font->baseline_offset;
if (font->vertical_centering)
@@ -24109,7 +24154,7 @@ append_space_for_newline (struct it *it, bool default_face_p)
if (!NILP (total_height))
spacing = calc_line_height_property (it, total_height, font,
boff, false);
boff, false);
else
{
spacing = get_it_property (it, Qline_spacing);
@@ -24121,11 +24166,13 @@ append_space_for_newline (struct it *it, bool default_face_p)
extra_line_spacing = XFIXNUM (spacing);
if (!NILP (total_height))
extra_line_spacing -= (it->phys_ascent + it->phys_descent);
}
}
if (extra_line_spacing > 0)
{
it->descent += extra_line_spacing;
it->descent += (extra_line_spacing - extra_line_spacing_above);
it->ascent += extra_line_spacing_above;
if (extra_line_spacing > it->max_extra_line_spacing)
it->max_extra_line_spacing = extra_line_spacing;
}
@@ -33138,6 +33185,7 @@ void
gui_produce_glyphs (struct it *it)
{
int extra_line_spacing = it->extra_line_spacing;
int extra_line_spacing_above = it->extra_line_spacing_above;
it->glyph_not_available_p = false;
@@ -33891,7 +33939,8 @@ gui_produce_glyphs (struct it *it)
if (extra_line_spacing > 0)
{
it->descent += extra_line_spacing;
it->descent += extra_line_spacing - extra_line_spacing_above;
it->ascent += extra_line_spacing_above;
if (extra_line_spacing > it->max_extra_line_spacing)
it->max_extra_line_spacing = extra_line_spacing;
}

View File

@@ -1694,5 +1694,20 @@ final or penultimate step during initialization."))
(should (equal (funcall (subr--identity #'any) #'minusp ls) '(-1 -2 -3)))
(should (equal (funcall (subr--identity #'any) #'stringp ls) nil))))
(ert-deftest total-line-spacing ()
(progn
(let ((line-spacing 10))
(should (equal (total-line-spacing) line-spacing) ))
(let ((line-spacing 0.8))
(should (equal (total-line-spacing) 0.8)))
(let ((line-spacing '(10 . 5)))
(should (equal (total-line-spacing) 15)))
(let ((line-spacing '(0.3 . 0.4)))
(should (equal (total-line-spacing) 0.7)))
(should (equal (total-line-spacing 10) 10))
(should (equal (total-line-spacing 0.3) 0.3))
(should (equal (total-line-spacing '(1 . 3)) 4))
(should (equal (total-line-spacing '(0.1 . 0.1 )) 0.2))))
(provide 'subr-tests)
;;; subr-tests.el ends here

View File

@@ -8650,4 +8650,22 @@ Finally, kill the buffer and its temporary file."
(should (= (point-min) 1))
(should (= (point-max) 5001))))
(ert-deftest test-line-spacing ()
"Test `line-spacing' impact on text size"
(skip-unless (display-graphic-p))
(let*
((size-with-text (lambda (ls)
(with-temp-buffer
(setq-local line-spacing ls)
(insert "X\nX")
(cdr (buffer-text-pixel-size))))))
(cl-loop for x from 0 to 50
for y from 0 to 50
do
(ert-info ((format "((linespacing '(%d . %d)) == (linespacing %d)" x y (+ x y))
:prefix "Linespace check: ")
(should (=
(funcall size-with-text (+ x y))
(funcall size-with-text (cons x y))))))))
;;; buffer-tests.el ends here