Skip to content

[WIP] Proportional fonts support#591

Draft
thjbdvlt wants to merge 6 commits intoakiyosi:masterfrom
thjbdvlt:proportional-font
Draft

[WIP] Proportional fonts support#591
thjbdvlt wants to merge 6 commits intoakiyosi:masterfrom
thjbdvlt:proportional-font

Conversation

@thjbdvlt
Copy link
Contributor

@thjbdvlt thjbdvlt commented Jun 17, 2025

This work-in-progress attempt to add support for proportional (i.e. non-monospace) fonts, as discussed in #127.

Motivation

As a terminal application, Vim/Neovim have no support for non-monospace fonts, while many Graphical editors have: Emacs, VS Code, and many others. This is limiting the use of Neovim for non-programming (prose) writing practices, despite its value for such stuff: academic texts1, novels, poetry 2 --whatever writing task that requires not so much to write text but to "rewrite and edit3" it--, or everyday tasks such as mail, personal notes...
For prose, proportional fonts have strong benefits (easier to read, especially for long texts), but even for code, some people argue that proportional fonts are worth to use (see the Go Fonts).

The design of Goneovim seems to make possible the use of proportional fonts for Neovim. It would be great, since there is no GUI that support that feature (Neovide, Neovim-Qt, Fvim, Uivonim...).

Current Progress

  • Text, background and decoration rendering.
  • Cursor position, text, background and shape.
  • Align the Line Number column end, to align the beginning of the buffer lines.
  • Left align buffer content (independantly of gutter/signcolumn content).
  • Possibility to use both proportional fonts for some grids and monospace for others.

victor-burgin

Things to be Fixed

  • Floating windows borders.
  • Floating window position (completion / LSP).
  • CPU usage.
  • Autocommands such as autocmd BufEnter *.md :GonvimGridFontAutomaticHeight "Liberation Sans".
  • Completion window position at the right place (near cursor).
  • Some punctuation having incorrect width depending on the context:

brackets
parentheses
ampersand

Current Limitation: Visual Wrap

The linebreak (visual wrap) is done, as now, by Neovim (using optiong linebreak) and not by Goneovim. Thus the linebreak happens long before the horizontal end (right) of the window (see screenshot), because it's based on the widest character (italic W). There are many options here. Goneovim could manage by itself the wrapping, but I don't know how much work it would require, and how it could impact performance.
On the other hand, as the screenshot shows, when writing prose, line usually tends to have no extreme widths: neither as narrow as lines fulls of i, nor as wide as lines full of uppercased italic W. Width seems not to vary so much. Thus, the size of the window could be based on the length of a letter narrower than W, e.g. u. (But I don't know what to do / how to handle special cases, when line is too long.) At the same time, line break long before the horizontal limit of the window may not be an issue at all for some users, as short lines (~10 words) as usually considered easier to read than long lines.

visual-wrap

Configuration Requirements

It's currently required to set some configuration variable in order to use proportional fonts:

  • DisableLigatures must be set to false, because if set to true it will trigger the cell-based rendering. There's probably no reason to change this.
  • LetterSpace must be set to 0 for the same reason.

Changes and Implementation

The changes must not impact performances of Goneovim for non-monospace fonts. Thus, every change is wrapped inside a conditional if !ont.proportional. (There may be a small impact because of these tests, but it may not be significant.) These changes use the new proportional field, added to the Font structure. There are three more fields added to this structure, all of them about QFontMetrics: italicFontMetrics, boldFontMetrics, italicBoldFontMetrics. That's because in proportional fonts, each character for each font variant has a specific width. Thus, the existing field italicWidth or a new field italicWidthScale couldn't be used to compute the width of italic/bold characters.
The structure Window also have a new field, also unused when monospace fonts are used: xPixelsIndexes. It's used to store the position, in X-axis, of each character for the lines currently drawn. It's useful to avoid hundreds of calls to QFontMetrics.HorizontalAdvance(char, ...).

Footnotes

  1. There is a lot of plugins and tools to use (Neo)Vim with Pandoc, or Bib(La)TeX, or Zotero.

  2. There are many blog posts stating that (Neo)Vim is a good prose editor. See, e.g.: https://jonathanh.co.uk/blog/writing-prose-in-vim/ or https://discover.hubpages.com/technology/how-and-why-i-use-vim-for-prose-writing or https://jamierubin.net/2019/03/21/writing-with-vim/

  3. Howard Becker, Writing for Social Scientists.

@akiyosi
Copy link
Owner

akiyosi commented Jun 20, 2025

@thjbdvlt
Thank you for the PR! I’ll be reviewing the details more thoroughly soon, but I wanted to share some initial thoughts.

Since Neovim’s UI is fundamentally grid-based, simply setting a proportional font as guifont would break the layout of all UI elements. While I don’t intend to restrict users from setting proportional fonts in guifont, I wonder if we could consider an approach where proportional fonts are configured separately using a dedicated command, and applied only to specific grids.

For example, we might imagine a command like GonvimProportionalFontView "some-proportional-font-name", which would allow certain content grids to be rendered using a proportional font, while keeping the base UI rendered with a monospaced font. This concept would be similar to the current GonvimGridFont command, but tailored for proportional font usage.

Additionally, when rendering with proportional fonts, we would likely need to avoid drawing float window borders using the font itself. Instead, we could consider rendering alternative borders using the GUI layer to maintain visual consistency.

Looking forward to discussing this further!

@thjbdvlt
Copy link
Contributor Author

thjbdvlt commented Jun 20, 2025

@akiyosi Thank you for your thoughts!

Having a special command to set a proportional font for a specific window seems to be the best option. I would typically have an autocmd for markdown files that would set a proportional font for the current window. (So I could keep coding with Inconsolata.)

I think that the modifications proposed in this draft are actually very compatible with this option, as I mostly focused on making the current rendering functions compatible with proportional fonts. It typically kinda works with GonvimGridFont (I just had to add two lines in the function I wrote to compute cell positions; in this commit), but it adds some bugs (glitches and invisible text). Another function could be written, but GonvimGridFont could be used as well, maybe without any change, since the proportional field in Font struct is queried directly to the font (using Qt). I guess that nobody want to use proportional fonts rendered in cells like monospace ones anyway, because it looks terrible. Another issue with GonvimGridFont is about resizing, but it seems that it's an issue even with monospace fonts?

I had not yet tried anything to fix floating windows, because with these changes these looks terrible, because of the right border, as you noticed. Completion menu is another issue: it's always incorrectly positioned.

One question I have is: instead of modifying existing functions, like I've done here adding conditions if font.proportional {...}, should I make new one, dedicated to proportional fonts? I didn't think it was good, because the changes to those functions are overall small, so it would mostly looks like duplication. But maybe you prefer to separate both rendering logics.

@akiyosi
Copy link
Owner

akiyosi commented Jun 21, 2025

@thjbdvlt
You're right that using GonvimGridFont for proportional fonts is one possible approach. However, the GonvimGridFont command is more about assigning a specific font to a given grid, and I feel that mixing this with the proportional font use case might not be ideal. (I do recognize that there are still issues with GonvimGridFont, including resizing.)

When a proportional font is specified, maybe the best approach is to apply it to all grids except the global grid (gridid=1) and the message grid (usually gridid=3), which would continue to use the default monospace font. That way, code editing and core UI elements remain stable and predictable, while other content (like markdown previews) can benefit from proportional rendering. That said, as you pointed out, figuring out how to properly position the completion menu remains a major challenge.

Regarding floating windows, goneovim already has logic to draw borders. I think we could build on that mechanism to support proportional fonts, rather than relying on the right-side cell rendering.

As for your last question: if most of the logic is shared, then adding if font.proportional { ... } branches within existing functions seems totally fine to me. I don't think it's worth duplicating entire functions just for that case.

@thjbdvlt
Copy link
Contributor Author

thjbdvlt commented Jun 22, 2025

@akiyosi I'll go that way, excluding global and message grids, and using the mechanisms for external windows to draw floating windows.

I've implemented a workaround for completion (or LSP, or other cursor-based floating window), which is not perfect (thus, I hope, temporary), because there currently exists no reliable way to check if a floating window must be position relatively to the cursor or not: relative = "cursor" is rarely used by plugins, see this issue I've opened on neovim.

@thjbdvlt thjbdvlt force-pushed the proportional-font branch 2 times, most recently from 46b671d to 7d104fd Compare June 23, 2025 21:54
@thjbdvlt
Copy link
Contributor Author

I upload some screenshots to illustrate the current state (still full of bugs and questions).

A completion window, near the cursor:

completion

A grid with proportional fonts, without any consequence on a fzf-lua floating window, because the floating window is unaffected by the proportional change on the working grid:

fzf

A split screen (only horizontal splits are currently OK):

split

Configuration used:

[Editor]
FontSize = 24
FontFamily = "Inconsolata"
### Fonts that fits together (Mono/Variable)
# Inconsolata + Liberation Sans    (24)
# Iosevka     + Linux Libertine O  (22)
ProportionalFontAlignGutter = true # This is new
DisableLigatures = false
IndentGuide = false
CachedDrawing = true
CacheSize = 400
# An autocommand would be better, but here is with a keymap
Ginitvim = '''nnoremap zp :GonvimGridFontAutomaticHeight 'Liberation Sans'<cr>
let &stc='%s%C%l    '
'''

@akiyosi
Copy link
Owner

akiyosi commented Jun 24, 2025

@thjbdvlt
Thanks! I'm currently testing your branch.

It looks like some of the floating window positioning logic is still based on cell width, which doesn't work well with proportional fonts. I think this needs to be adjusted to use pixel-based positioning instead. For example, part of the Telescope window appears overlapped on my screen due to this.

This seems to be related to logic in functions like setFloatWindowPosition() in window.go.
image

@thjbdvlt
Copy link
Contributor Author

thjbdvlt commented Jun 24, 2025

@akiyosi
May I ask you which fonts you are using on the screenshot? I think it could be an issue when the proportional font is wider than the global monospace font. (But I don't understand yet.) I can reproduce with Iosevka + Liberation Sans, while it's fine with Iosevka + Linux Libertine O, or with Inconsolata + Liberation Sans.

One issue here is that there are many floating windows, and that the position of the right one depends on the width of the left-one, so we need to use both the cellwidth of the floating window and the pixel position from the anchor window.

Also, I have thought about visual-wrap, which is (I think) one of the hardest issue, and I would like to hear your opinion about that.

I see two solutions.

  1. The first, that doesn't seem reasonable to me, would be to manage the visual-wrap entirely in Goneovim, and to add :GonvimVisualUp/Down to replace builtins gk/gj. (I don't think that there is a way to tell to Neovim that this line is wrapped at 50h character, this one at 74th, this one at 84th..., so that Neovim manages by its own gk/gj; but I may be wrong.)
  2. Another solution would be to let Neovim in charge of visual wrap. When writing prose, lines (as wrapped by Neovim) tends to have an average width, so it actually produces overall good results (see screenshot below). But there could be extreme cases, like a like full in W. In these cases, that could be easily identified, we can artificially make the whole line narrower (using the QFont), or smaller, or we can truncate it with a special sign. (I think narrowing would be preferable; it would be similar to print practices with spaces for justification).
    2.1. We can also increase the window columns to reduce the unused space on the right.
    2.2. It would even be possible (using the same logic as narrowing) to optionally justify the text (using spaces).

I think 2.1 is quite easy to do, and looks good enough.

hedgehog

(Article Hedgehog on Wikipedia.)

@akiyosi
Copy link
Owner

akiyosi commented Jun 26, 2025

@thjbdvlt
Regarding visual-wrap, I agree that it's best to leave it to Neovim. There are certainly challenges in extreme cases, but rather than trying to address those now, I think our priority should be completing the core rendering functionality for proportional fonts first.

As for the floating window positioning issue, I’ve also been looking into the source code but haven’t been able to identify the root cause yet. Here’s what I’ve observed:

If I prepare a buffer where the first line contains consecutive “i” characters and the second line contains consecutive “W” characters;

iiiiiiiii
WWWWWWWW

the layout of the Telescope floating windows differs depending on whether the cursor is on the “i” line or the “W” line. I’ve debugged this and tracked the anchor window — it is always window 1. Given that, it seems unlikely that the result of setFloatWindowPosition() alone would cause this difference. I suspect some other process is involved, but I haven’t been able to investigate it deeply yet.

@thjbdvlt
Copy link
Contributor Author

@akiyosi
It probably comes from the workaround I've found for windows that must be placed near the cursor (completion/lsp, mostly).
This is this the snippet you can comment to fix the Telescope floating window (window.go, L.4108):

// There is currently no reliable way to identify cursor-based floating
// window. All completion plugins set `relative` to `"editor"` in the
// window config (and not to `"cursor"`).
// Related issue: https://github.com/neovim/neovim/issues/34595
// For now, it seems that best workaround is to make the position of
// every editor/cursor-relative floating window relative to the actual
// content of the grid. This will place completion window correctly.
// As a consequence, centered floating window won't be centered anymore,
// but it's a minor issue since text itself doesn't occupy (as now) the
// whole window (because of text wrapping).
winwithcontent, ok := w.s.getWindow(w.s.ws.cursor.gridid)
if ok && winwithcontent.getFont().proportional {
	config, err := w.s.ws.nvim.WindowConfig(w.id)
	if err == nil && anchorwin != nil && config != nil &&
		(config.Relative == "cursor" || config.Relative == "editor") {
                //                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^
                //                                    THIS IS THE ISSUE
		contentRow := anchorwin.s.ws.cursor.row
		x = int(winwithcontent.getSinglePixelX(contentRow, col))
		y = winwithcontent.getFont().lineHeight * row
       }
}

I don't yet see a way to fix both issues.
It's probably best not to support completion windows if they don't use relative = "cursor" (none uses it), or at least to find another workaround.

@akiyosi
Copy link
Owner

akiyosi commented Jun 26, 2025

@thjbdvlt
You're right — after commenting out that section of code, the issue with the Telescope floating window is resolved.

Another issue I noticed is that on lines with many "W" characters, the width of the text cache image is insufficient. The following change is needed to fix this.

@@ -3012,9 +3011,14 @@ func (w *Window) newTextCache(text string, hlkey HlKey, isNormalWidth bool) *gui
                )
        }
 
-       width := float64(len(text))*font.cellwidth + 1
-       if hlkey.italic {
-               width = float64(len(text))*font.italicWidth + 1
+       var width float64
+       if !fontfallbacked.proportional {
+               width = float64(len(text))*font.cellwidth + 1
+               if hlkey.italic {
+                       width = float64(len(text))*font.italicWidth + 1
+               }
+       } else {
+               width = fontfallbacked.fontMetrics.HorizontalAdvance(text, -1)
        }

@thjbdvlt
Copy link
Contributor Author

@akiyosi Thank you! I've added this change (with small difference, because some text wasn't displayed anymore, e.g. Amiga just before the cursor or at the end of a line).

thjbdvlt added a commit to thjbdvlt/goneovim that referenced this pull request Jun 26, 2025
Fix lines full of `W` (or other very wide chars) to not be displayed.
akiyosi#591 (comment)
@thjbdvlt
Copy link
Contributor Author

thjbdvlt commented Jun 27, 2025

@akiyosi
I have a question regarding the function Window.paint().
I've finally manage to fix a bug causing background/decoration to stop before the end of the line when using proportional font.
decoration-bg
The fix requires to change the definitions of cols in Window.paint(), which is currently defined as:

// window.go L.323

cols := int(math.Ceil(float64(rect.Width()) / font.cellwidth))

And I need to add:

if font.proportional && w.cols > cols {
    cols = w.cols
}

I've seen that in the function window.drawForeground(), called within window.paint(), the cols argument is not used anymore to draw text. Instead, w.cols is used. so I was wondering why w.cols isn't used in the first place? Should I avoid using it for proportional font?

// window.go L.3115

// w.drawText(p, y, col, cols)
w.drawText(p, y, 0, w.cols)
w.drawTextDecoration(p, y, col, cols)

@akiyosi
Copy link
Owner

akiyosi commented Jun 27, 2025

@thjbdvlt
Thanks for the question — here’s some clarification.

In general, window.paint() receives redraw requests for a specific region of the screen, and internally converts that region into a grid-based area represented by col, row, cols, and rows. Goneovim uses these values to update only the relevant portion of the screen, so using them is the standard approach.

However, the actual text rendering in drawText() is very performance-sensitive, and for that reason, it doesn’t follow the straightforward approach of rendering only the content within the given col and cols range. Instead, it processes the entire line by grouping characters with the same highlight and rendering each group as a unit.

While this makes the implementation more complex, it has the significant benefit of improving cache efficiency. By rendering the entire line, the likelihood of cache hits for pre-rendered text images increases, especially when highlight groups remain unchanged. Because of this, col and cols are currently unused in drawText().

That said, in proportional font mode, if using w.cols during background rendering is necessary to ensure proper visual behavior (such as filling the entire background), I think it’s completely reasonable to do so.

@thjbdvlt
Copy link
Contributor Author

@akiyosi
Thank's for the explanation!
(I found another solution, a bit more complex but that avoids drawing the whole line.)

@akiyosi
Copy link
Owner

akiyosi commented Jul 4, 2025

@thjbdvlt

Some punctuation having incorrect width depending on the context:
image

the issue mentioned above is likely caused by the following code.
https://github.com/akiyosi/goneovim/blob/master/editor/window.go#L2443-L2474

This logic treats characters with the same highlight, separated only by single spaces, as a single text chunk for drawing. However, this behavior seems to be incompatible with proportional fonts.

Below, I propose a revised and optimized version of the code with that logic removed.

@@ -2622,19 +2622,22 @@ func (w *Window) drawText(p *gui.QPainter, y int, col int, cols int) {
                for hlkey, colorSlice := range chars {
                        var buffer bytes.Buffer
                        slice := colorSlice
-
                        isIndentationWhiteSpace := true
                        pos := col
+
+                       horScrollPixels := 0
+                       verScrollPixels := 0
+                       if w.s.ws.mouseScroll != "" {
+                               horScrollPixels = w.scrollPixels[0]
+                       }
+                       if w.lastScrollphase != core.Qt__NoScrollPhase {
+                               verScrollPixels = w.scrollPixels2
+                       }
+                       if editor.config.Editor.LineToScroll == 1 {
+                               verScrollPixels += w.scrollPixels[1]
+                       }
+
                        for x := col; x <= col+cols; x++ {
-                               if w.s.ws.mouseScroll != "" {
-                                       horScrollPixels = w.scrollPixels[0]
-                               }
-                               if w.lastScrollphase != core.Qt__NoScrollPhase {
-                                       verScrollPixels = w.scrollPixels2
-                               }
-                               if editor.config.Editor.LineToScroll == 1 {
-                                       verScrollPixels += w.scrollPixels[1]
-                               }
                                if line[x].highlight.isSignColumn() {
                                        horScrollPixels = 0
                                }
@@ -2647,62 +2650,46 @@ func (w *Window) drawText(p *gui.QPainter, y int, col int, cols int) {
                                        verScrollPixels = 0
                                }
 
-                               isDrawWord := false
+                               if len(slice) == 0 {
+                                       break
+                               }
                                index := slice[0]
 
-                               if len(slice) != 0 {
-
-                                       // e.g. when the contents of the line is;
-                                       //    [ 'a', 'b', ' ', 'c', ' ', ' ', 'd', 'e', 'f' ]
-                                       //
-                                       // then, the slice is [ 1,2,4,7,8,9 ]
-                                       // the following process is
-                                       //  * If a word is separated by a single space, it is treated as a single word.
-                                       //  * If there are more than two continuous spaces, each word separated by a space
-                                       //    is treated as an independent word.
-                                       //
-                                       //  therefore, the above example will treet that;
-                                       //  "ab c" and "def"
-
-                                       if x != index {
-                                               if isIndentationWhiteSpace {
-                                                       continue
-                                               } else {
-                                                       if len(slice) > 1 {
-                                                               if x+1 == index {
-                                                                       if buffer.Len() > 0 {
-                                                                               pos++
-                                                                               buffer.WriteString(" ")
-                                                                       }
-                                                               } else {
-                                                                       isDrawWord = true
-                                                               }
-                                                       } else {
-                                                               isDrawWord = true
-                                                       }
-                                               }
+                               if x != index {
+                                       if isIndentationWhiteSpace {
+                                               continue
                                        }
-
-                                       if x == index {
-                                               pos++
-
-                                               char := line[x].char
-                                               if line[x].covered && w.grid == 1 {
-                                                       char = " "
-                                               }
-                                               buffer.WriteString(char)
-                                               slice = slice[1:]
-                                               isIndentationWhiteSpace = false
-
+                                       // draw collected word
+                                       if buffer.Len() != 0 {
+                                               w.drawTextInPos(
+                                                       p,
+                                                       int(w.getPixelX(wsfont, y, x-pos))+horScrollPixels,
+                                                       wsfontLineHeight+verScrollPixels,
+                                                       buffer.String(),
+                                                       hlkey,
+                                                       true,
+                                                       false,
+                                               )
+                                               buffer.Reset()
+                                               pos = 0
                                        }
+                                       continue
                                }
 
-                               if isDrawWord || len(slice) == 0 {
-                                       if len(slice) == 0 {
-                                               x++
-                                       }
+                               // x == index
+                               pos++
+                               char := line[x].char
+                               if line[x].covered && w.grid == 1 {
+                                       char = " "
+                               }
+                               buffer.WriteString(char)
+                               slice = slice[1:]
+                               isIndentationWhiteSpace = false
 
+                               if len(slice) == 0 {
+                                       // draw last word
                                        if buffer.Len() != 0 {
+                                               x++ // prepare for next column
                                                w.drawTextInPos(
                                                        p,
                                                        int(w.getPixelX(wsfont, y, x-pos))+horScrollPixels,
@@ -2712,15 +2699,10 @@ func (w *Window) drawText(p *gui.QPainter, y int, col int, cols int) {
                                                        true,
                                                        false,
                                                )
-
                                                buffer.Reset()
-                                               isDrawWord = false
                                                pos = 0
                                        }
-
-                                       if len(slice) == 0 {
-                                               break
-                                       }
+                                       break
                                }
                        }
                }

@thjbdvlt
Copy link
Contributor Author

thjbdvlt commented Jul 4, 2025

@akiyosi
That's fantastic! I've spend few hours on it but couldn't solve.
Now plugins like leap.nvim works wonderfully with proportional fonts.
I push your changes to this WIP.
(I haven't much time until the end of month, but I don't abandon this!)

thjbdvlt added a commit to thjbdvlt/goneovim that referenced this pull request Jul 4, 2025
Apply a change by @akiyosi:
> This logic treats characters with the same highlight, separated only by
> single spaces, as a single text chunk for drawing. However, this
> behavior seems to be incompatible with proportional fonts.
> Below, I propose a revised and optimized version of the code with that
> logic removed.
See akiyosi#591 (comment)
editor/window.go Outdated
width := float64(len(text))*font.cellwidth + 1
if hlkey.italic {
if fontfallbacked.proportional {
fmWidth := fontfallbacked.fontMetrics.HorizontalAdvance(text, -1)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With proportional fonts, bold styles tend to increase the character width even further, so I think that additional consideration for this case is necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. It can be done easily, I think, e.g. using this small function I've added:

func getFontMetrics(font *Font,  highlight *Highlight) *gui.QFontMetricsF {
	if !highlight.italic {
		if !highlight.bold {
			return font.fontMetrics
		}
		return font.boldFontMetrics
	}
	if !highlight.bold {
		return font.italicFontMetrics
	}
	return font.italicBoldFontMetrics
}

The following change can thus be enough:

diff --git a/editor/window.go b/editor/window.go
index 0cbaa3a..71a6342 100644
--- a/editor/window.go
+++ b/editor/window.go
@@ -3015,7 +3015,8 @@ func (w *Window) newTextCache(text string, hlkey HlKey, isNormalWidth bool) *gui
 
 	width := float64(len(text))*font.cellwidth + 1
 	if fontfallbacked.proportional {
-		fmWidth := fontfallbacked.fontMetrics.HorizontalAdvance(text, -1)
+		fm := getFontMetrics(fontfallbacked, &Highlight{bold: hlkey.bold, italic: hlkey.italic})
+		fmWidth := fm.HorizontalAdvance(text, -1)
 		if fmWidth > width {
 			width = fmWidth
 		}

(It seems to work, I'll do some tests.)

thjbdvlt added a commit to thjbdvlt/goneovim that referenced this pull request Jul 10, 2025
@akiyosi
Copy link
Owner

akiyosi commented Jul 13, 2025

@thjbdvlt
I'm planning to apply equivalent changes to master as mentioned here: #591 (comment).
Once this PR is ready to be merged, I assume the commit history will be cleaned up and adjusted with appropriate granularity.
So even if there are some temporary inconsistencies with master, I think it's fine to resolve them afterward.

@thjbdvlt
Copy link
Contributor Author

@akiyosi
I'll rebase to master when the changes will be applied.
I think that, at least for the current state of the PR, 3-4 commits should be enough, maybe even less?
(Most commits are actually fixes of aspects of the initial modifications I haven't thought about.)

Also, I've seen that a function I've added to compute left-column width (getTextOff) works well but has significant CPU usage. Do you think I should remove it, or it's OK to let it (I've added it as a non-default option)?

/* Get the buffer offset, i.e. SignColumn and Numbers. */
func (w *Window) getTextOff(delayMs time.Duration) (int, bool) {
	if !editor.config.Editor.ProportionalFontAlignGutter {
		return 0, true
	}
	if w.isFloatWin || w.isMsgGrid || w.isPopupmenu {
		return 0, false
	}
	var isValid bool
	validCh := make(chan bool)
	errCh := make(chan error)
	go func() {
		result, err := w.s.ws.nvim.IsWindowValid(w.id)
		if err != nil {
			errCh <- err
			validCh <- false
			return
		} else {
			validCh <- result
			errCh <- nil
			return
		}
	}()
	select {
	case <- errCh:
		return 0, false
	case isValid = <-validCh:
	case <-time.After(delayMs * time.Millisecond):
		// If the delay is over, returns the last stored version.
		// This avoids flickering.
		return w.endGutterIdx, false
	}
	if !isValid {
		return 0, false
	}
	var output any
	err := w.s.ws.nvim.Call("getwininfo", &output, w.id)
	if err != nil {
		return 0, false
	}
	outputArr, ok := output.([]any)
	if !ok || len(outputArr) < 1 {
		return 0, false
	}
	outputMap, ok := outputArr[0].(map[string]any)
	if !ok {
		return 0, false
	}
	textoff, ok := outputMap["textoff"]
	if !ok {
		return 0, false
	}
	textoffInt, ok := textoff.(int64)
	if !ok {
		return 0, false
	}
	w.endGutterIdx = int(textoffInt)
	return w.endGutterIdx, true
}

@akiyosi
Copy link
Owner

akiyosi commented Jul 25, 2025

@thjbdvlt
Thanks! I also think 3–4 commits are enough.

My understanding is that getTextOff() is there to stabilize the gutter width. I’m in favor of keeping that functionality, but we do need to factor in the performance cost—and I don’t have a good solution for that yet either. The existing isSignColumn() check is incomplete, but moving the decision there (or improving it) could be one possible direction.

thjbdvlt added a commit to thjbdvlt/goneovim that referenced this pull request Aug 16, 2025
+ rebase to master
+ add function getFontMetrics
+ use a single map in `refreshLinesPixels`, using struct as keys
+ apply akiyosi#591 (comment)
+ apply akiyosi#591 (comment)
+ `xPixelIndexes` is now of type `float64`
+ fix index out of range
+ fix cursor width in insert mode when completion windows pops up
+ fix background/decoration
+ fix visual mode wrap (background)
+ fix gridFontAutomaticHeight for autocmd
+ fix floating window position
+ fix character width, see:
  akiyosi#591 (comment)
@thjbdvlt thjbdvlt force-pushed the proportional-font branch 4 times, most recently from 068e5c9 to 4ca7f05 Compare August 16, 2025 12:50
@thjbdvlt thjbdvlt force-pushed the proportional-font branch 6 times, most recently from 6769e0c to 6f6fbbe Compare August 16, 2025 13:47
@thjbdvlt
Copy link
Contributor Author

thjbdvlt commented Aug 16, 2025

@akiyosi

I've cleaned the commit history. I'll rebase to current master within a few days.
I'll also try to improve isExistingColumn so we can use it for proportional fonts (if I remember well, it doesn't handle yet the spaces in the beginning of wrapped lines, but I need to check).

Implement basic support for proportional font:
- foreground, background, decoration
- proportional cursor width
For the purpose of completion floating windows, every floating window
that has, in its config, `relative = "cursor"` or `relative = "editor"`
will be positioned relatively to the actual text on the buffer.
It should be a temporary solution until (I hope) neovim implement a way to
identify floating windows that are positioned near the cursor.
(See: neovim/neovim#34595)
A consequence of this commit is that some floating window that must be
centered won't be centered anymore, since their X coordinate will
depend on the text that the buffer contained. But it may not be a real
issue, since the text itself, because of visual wrapping, doesn't occupy
the whole window.
(Note: as other commits, only proportional fonts are affected.)
Problem:
The Sign Column, containing the Line Numbers and other informations,
has not always the same width, with Proportional Fonts, leading to
misalignement in the text, especially disturbing when using relative
numbers, since the passage from 9 to 10, replacing a space by a digit,
moves the line to the right.

Solution:
Compute the length of the gutter, using `getwininfo` and extract the
field `textoff`. Characters in the gutter all have a fixed-width.
As it can leads to more computation, a global option has been added,
`Editor.ProportionalFontAlignGutter`, that can deactivate this feature.
Add a new function, similar to GonvimGridFont, but that automatically
calculate the height of the new font, to avoid resizing issues.
@akiyosi
Copy link
Owner

akiyosi commented Sep 11, 2025

@thjbdvlt
Sorry for the delay in replying. I’ve been quite busy with my work, and given the large scope of this change, it has been difficult to make progress on the review. That said, this feature is very appealing, and I will make sure to respond in the near future.

@thjbdvlt
Copy link
Contributor Author

@akiyosi I understand that it takes time, there are a lot of changes! And I need to rebase to master again anyway.

@akiyosi akiyosi force-pushed the master branch 2 times, most recently from f196ee6 to c1a71b0 Compare February 1, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants