Conversation
|
@thjbdvlt 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! |
|
@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 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 |
|
@thjbdvlt 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 |
|
@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: |
46b671d to
7d104fd
Compare
|
I upload some screenshots to illustrate the current state (still full of bugs and questions). A completion window, near the cursor: 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: A split screen (only horizontal splits are currently OK): 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 '
'''
|
|
@thjbdvlt 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 |
|
@akiyosi 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.
I think 2.1 is quite easy to do, and looks good enough. (Article Hedgehog on Wikipedia.) |
|
@thjbdvlt 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; 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. |
|
@akiyosi // 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. |
|
@thjbdvlt 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)
}
|
|
@akiyosi Thank you! I've added this change (with small difference, because some text wasn't displayed anymore, e.g. |
Fix lines full of `W` (or other very wide chars) to not be displayed. akiyosi#591 (comment)
|
@akiyosi // 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.go L.3115
// w.drawText(p, y, col, cols)
w.drawText(p, y, 0, w.cols)
w.drawTextDecoration(p, y, col, cols) |
|
@thjbdvlt 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. |
|
@akiyosi |
the issue mentioned above is likely caused by the following code. 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
}
}
} |
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) |
There was a problem hiding this comment.
With proportional fonts, bold styles tend to increase the character width even further, so I think that additional consideration for this case is necessary.
There was a problem hiding this comment.
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 |
|
@akiyosi Also, I've seen that a function I've added to compute left-column width ( /* 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
} |
|
@thjbdvlt 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. |
+ 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)
068e5c9 to
4ca7f05
Compare
6769e0c to
6f6fbbe
Compare
|
I've cleaned the commit history. I'll rebase to current master within a few days. |
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.
+ minor changes
6f6fbbe to
5d42ac6
Compare
|
@thjbdvlt |
|
@akiyosi I understand that it takes time, there are a lot of changes! And I need to rebase to master again anyway. |
f196ee6 to
c1a71b0
Compare







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
Things to be Fixed
autocmd BufEnter *.md :GonvimGridFontAutomaticHeight "Liberation Sans".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 (italicW). 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 italicW. Width seems not to vary so much. Thus, the size of the window could be based on the length of a letter narrower thanW, 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.Configuration Requirements
It's currently required to set some configuration variable in order to use proportional fonts:
DisableLigaturesmust be set tofalse, because if set totrueit will trigger the cell-based rendering. There's probably no reason to change this.LetterSpacemust be set to0for 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 newproportionalfield, added to theFontstructure. There are three more fields added to this structure, all of them aboutQFontMetrics:italicFontMetrics,boldFontMetrics,italicBoldFontMetrics. That's because in proportional fonts, each character for each font variant has a specific width. Thus, the existing fielditalicWidthor a new fielditalicWidthScalecouldn't be used to compute the width of italic/bold characters.The structure
Windowalso 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 toQFontMetrics.HorizontalAdvance(char, ...).Footnotes
There is a lot of plugins and tools to use (Neo)Vim with Pandoc, or Bib(La)TeX, or Zotero. ↩
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/ ↩
Howard Becker, Writing for Social Scientists. ↩