Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[discuss] Popup Window API design. #4063

Closed
skywind3000 opened this issue Mar 2, 2019 · 138 comments
Closed

[discuss] Popup Window API design. #4063

skywind3000 opened this issue Mar 2, 2019 · 138 comments

Comments

@skywind3000
Copy link

skywind3000 commented Mar 2, 2019

This is not an issue, but I have no idea how to attach images in vim_dev group. So forgive me create this post in the issues and it can be closed immediately.

The human readable page is here:
https://github.com/skywind3000/vim-proposal/blob/master/popup_window.md


Popup Window API Proposal

Problem

Vim has already got some builtin popup-widgets for certain usage, like completion menu or balloon. But users and plugin authors are still asking for more:

In Vim conf 2018, Bram claimed that he has plan for this already:

Popup Windows:

Use for a notification:

  • Asynchronously show window with text “build done”.
  • Remove after a few seconds.

Use for picking an item:

  • Show window where each line is an item
  • Let user pick an item
  • A bit like confirm() but much nicer

Summary, there are two types of popup window: interactive and non-interactive.

For non-interactive popup windows, people want to use them to:

  • Display the documentation for a function:

emacs-2

  • Display linting errors or warnings:

kakoune-1

  • Preview a portion of file (can be used to preview grep results in quickfix):

emacs-1

  • Display funny help for newbies (like paperclip assistent in office 2000):

kakoune-2

For interactive popup windows, people want to use them to:

  • pick an item:

ex-pick

  • display a nice popup menu:

ex-menu

  • cmd line completion:

ex-cmdline

  • use fuzzy finder in a popup window:

ex-fuzzy

There are too many popup-related widgets for certain usage, designing one by one is nearly impossible.

Implementing a neovim's floating window will take too much time (it is working in progress for almost 2-years and still not merge to master).

Can we implement the popup window in a simple and adaptive way ? Is this possible to unify all their needs and simplify API design ?

The following parts of this article will introduce an overlay mechanism similar to Emacs's text overlay which is the backend of various popup windows in Emacs.

API Scope

Popup windows will draw into an overlay layer, A popup will remain after creation until an erase function is called. Everything in the overlay will not interfere vim's states, people can continue editing or using vim commands no matter there is a popup window or not.

So, the APIs are only designed for drawing a popup window and has nothing to do with user input. Dialogs like yes/no box and confirm box require user input, can be simulated with following steps:

function! Dialog_YesNo(...)
    while not_quit
       draw/update the popup window
       get input from getchar()
    endwhile
    erase the popup window
endfunc

Popup window APIs is not responsible for any interactive functionalities. Instead of implementing a complex widget/event system (which is too complex), it is sane to let user to handle the input by getchar().

There can be a popup.vim script contains some predefined popup windows/dialogs and will be shipped with vim itself. User can use the primitive APIs and getchar() to implement other complex dialogs like a popup fuzzy finder or a command line history completion box.

Overlay Buffer

The overlay buffer is a character matrix with the same size of the screen:

|\     |\
| \    | \
|  \   |  \
|   \  |   \
|   |  |   |
| 1 |  | 2 |   <---- Observer
|   |  |   |
|   /  |   /
|  /   |  /
| /    | /
|/     |/

^      ^
|      |
|   Overlay Buffer (M rows and N columns)
|
Ground Vim UI (M rows and N columns)
 

Similar to Video Buffer (0xb8000) in x86's text mode, the overlay buffer is a 2D array of characters and attributes. Change the content of the 2D array will change the text in the screen.

The overlay buffer is invisible by default and can be enabled by:

set guioptions+=o

Every existent text rendering code in both Vim & GVim needs to be updated to support this overlay buffer, once it finished, we can use it to build powerful popup windows.

There are also some basic APIs for overlay buffer:

  • add a text string with position and attribute.
  • erase a rectangle of text.
  • command redrawoverlay to update the overlay (it uses double buffer to prevent flicker).

With these primitive APIs, user can draw what ever they like on the overlay buffer.

Overlay Panes

Overlay panes is an abstraction of the popup windows, it consists of:

  • position and size
  • z order (for overlapping calculation)
  • background color
  • border styles
  • lines of text and text-properties

There can be multiple panes at the same time, the panes can be manipulated by:

pane_create(int row, int col, int width, int height, ...);
pane_destroy(int pane_id);
pane_update(int pane_id, ...);
pane_move(int pane_id, int new_row, int new_col, int new_width, int new_height);
pane_show(int pane_id, bool show_hide);

(PS: they are provided as both C-apis and vim functions).

The life cycle of a pane is between pane_create and pane_destroy.

The (row, col) is using screen coordinate system, and there can be some functions to convert window based coordinate system to screen coordinate system. If you want to display a popup balloon right above your cursor, you can use them to calculate the position.

Finally, there is a function to render the pane list into the overlay buffer:

pane_flush();

If you create some panes, the overlay buffer will not change until pane_flush().

Popup Windows

All the common popup windows are implemented in popup.vim script, they will use panes to display a popup window and getchar() to provide interactive functionalities.

There are some predefined popup windows:

  • Popup_Message(): display a "build complete" message and hide after a few seconds.
  • Popup_LinterHint(): display a message right above cursor and hide if cursor moves outside current <cword>.
  • Popup_Menu(): display a menu (use getchar() to receive user input) and quit after user select an item.
  • Popup_YesNo(): display a yes/no box and return the result after user made a selection.
  • ...
  • and so on.

User or plugin authors can use the high level APIs provided by popup.vim or design their own popup window by utilizing lower level pane APIs or overlay APIs.

Summary

It is complex to design an event system or NeoVim's floating window, and nearly impossible to implement every type of popup window for certain usage.

To unify and simplify the interface, this proposal suggests to provide an overlay mechanism with some primitive APIs to:

  • render a popup window (pane)
  • erase a popup window
  • update a popup window

And let user handle input themself by getchar(). At last, makes it possible to enable users to create various popup windows with different styles and functionalities.

--

2019/3/2 edit: floating window got merged to master.

@prabirshrestha
Copy link

prabirshrestha commented Mar 2, 2019

Few more things I would like to add.

  • It would also be good to see if we can come up with a different richer ui for GUI. For example embedding bold, italic, images and markdown texts, hr would be useful. Might be allow the user to set html and use the OS webview?
  • As for flush would be good to have an api that allows us to optimize re-renders. I'm working on creating a fast async quickpick with vimscript only that can support fuzzy finder and to make it look responsive I need to draw a spinner which only takes one char but because I'm calling redraw everything is getting redrawn. (If anyone knows how too force redraw statusline only let me know, that would also solve this issue).

https://github.com/prabirshrestha/quickpick.vim

NPM Picker

@oblitum
Copy link

oblitum commented Mar 2, 2019

@skywind3000: you may correct this statement:

it is working in progress for almost 2-years and still not merge to master

See r/vim/comments/discuss_vim_popup_window_api_design#comment.

@skywind3000
Copy link
Author

skywind3000 commented Mar 3, 2019

Possible implementation for GVim and Vim:

For Windows, create a layered child window on the top of textarea. Layered Window can use a color key to control transparent area. No need to update most of GVim code.

For terminal, ncurses has overlapping windows and panels. I don't know if they can be used because I have never used ncurses.

@epheien
Copy link

epheien commented Mar 3, 2019

Currently, nvim support floating window!
Vim need some features against it.

@iamcco
Copy link

iamcco commented Mar 3, 2019

  • It would also be good to see if we can come up with a different richer ui for GUI. For example embedding bold, italic, images and markdown texts, hr would be useful. Might be allow the user to set html and use the OS webview?

webview is crazy great

@skywind3000
Copy link
Author

skywind3000 commented Mar 3, 2019

@iamcco @prabirshrestha Webview is too big for vim, GVim needs to include at least 20MB dynamic libraries to support webview. You can check the binary size of CEF or Electron.

The webview can be implemented in a separated process, and attach to GVim's main window.

@prabirshrestha
Copy link

By webview I mean the OS default browser that is for Mac it would be safari and windows IE. Not sure about Linux. But if it isn’t portable then not worth it. Or if one could make a markdown renderer that would also be great.

@marcotrosi
Copy link

I think it is very important that the implementation is super simple (KISS) and NOT super feature rich. We need only the basic overlay with just the proposed controls and new highlight groups and then the users are free to use it for various stuff. There will be many new ideas, you'll see.

@epheien
Copy link

epheien commented Mar 3, 2019

Prefer text ui, not webview.
neovim's floating window is awesome feature!

@bstaletic
Copy link

For the plugin writers' sanity sake, we should really try to make our implementation's API as close to neovim's as possible. Though, that's a little hard at the moment, considering that neovim's documentation isn't available just yet.

@bfredl
Copy link
Contributor

bfredl commented Mar 3, 2019

@bstaletic I'm working on it here neovim/neovim#9669, though I need adjust the script to show the indented text in nvim_open_win() correctly, so currently that part can be read here.

Edit: initial documentation is on master, see :help api-floatwin

@skywind3000
Copy link
Author

skywind3000 commented Mar 4, 2019

@bstaletic
Copy link

That RFC has been updated in my vim fork: https://github.com/bstaletic/vim/tree/current_argument_update

@chemzqm
Copy link

chemzqm commented Mar 15, 2019

Highlight support is great, like this:
Screen Shot 2019-03-15 at 6 02 41 PM

One of reasons that I don't like balloon is that doesn't allow highlight.

@myitcv
Copy link

myitcv commented Apr 17, 2019

As part of https://github.com/myitcv/govim I'd like to see development in this area. govim is backed by gopls, an LSP server. So we have lots of rich information some users might like to be surfaced.

One of the key use cases is signature help. That is, when the user is in the middle of typing an expression that represents a function/method call, signature help reminds them of the function/method's signature, which parameter they are currently providing etc.

Some users will prefer this sort of detail in the status line.

Others, particularly those perhaps more used to things like VSCode, will appreciate the option of a balloon-like presentation of the signature help immediately above/below the relevant position.

Highlighting support would be a bonus, not least because it can be used to show the current parameter. (Highlighting support in balloons would also be useful too)

@brammool - what are your thoughts on this space in general?

@oblitum
Copy link

oblitum commented Apr 23, 2019

@myitcv reminder that you have all that available in neovim master/nightly, with coc.nvim making use of it for quite a while already. I find the govim's approach regarding adopting Vim8-only late remote API instead of NeoVim's one (and producing/using a wrapper over it for Vim8) kinda... strange. Is it worth it? I see even @Shougo preferring the latest approach with his plugins, even despite he being the original author of Vim's remote API implementation, if I recall correctly.

I think the remote API feature is one hard-to-borrow idea if you simply dismiss implementing it in a compatible way, and since NeoVim took the lead, I've seen many authors leveraging its remote API before Vim came with its own, unrelated one, that's of no use for the already existing remote plugins in NeoVim-land. Reason why I hardly see any Vim8-only remote plugin around.

@myitcv
Copy link

myitcv commented Apr 23, 2019

@oblitum - I'd be happy to continue the conversation about govim, Neovim vs Vim8, different APIs etc in an issue over at https://github.com/myitcv/govim. I don't think this thread is the best place for that discussion.

@brammool
Copy link
Contributor

I have added a detailed design in patch 8.1.1329. Feel free to comment.

@puremourning
Copy link
Contributor

Regarding the API, i'm a strong believer in "write the usage code first", i.e. you can't really know if the API works without some practical code which uses it. Bram, if you have a toy or POC implementation of the proposed API, i am more than willing to rewrite our my RFC demo to use it and let you know how it goes ? But on the face of it, your proposed API does seem to be a superset of what i proposed in that RFC.

In my original proposal i said this(;

hint_pum_set( {arg} ) = set the contents of the hint menu

For some reason i recall that i wanted to set the text, then display it separately. Maybe this was just implementation convenience, but perhaps it would make sense to allow popup_create which does all of what popup_show does, but doesn't actually show it ? This might be YAGNI, though.

@bstaletic
Copy link

@puremourning

For some reason i recall that i wanted to set the text, then display it separately.

I guess that was your idea at some point, but you also scrapped it and didn't end up using it in the actual code for the RFC mentioned, because you also had:

hint_pum_show( {arg} ) = set the contents and show the hint menu

Your branch didn't use hint_pum_set() at all by the time I took over the branch to rebase it onto, at the time, latest changes to the upstream vim.

@puremourning
Copy link
Contributor

OK then, carry on... :)

@tpimh
Copy link

tpimh commented May 15, 2019

Your paperclip design is awesome!

@brammool
Copy link
Contributor

I was wondering about mouse clicks. We can probably do this:

A mouse click arrives as . The coordinates are in
v:mouse_popup_col and v:mouse_popup_row. The top-left screen cell of the
popup is col 1, row 1 (not counting the border).

@brammool
Copy link
Contributor

See a discussion about using a buffer instead of a List of lines: https://groups.google.com/forum/#!topic/vim_dev/6RXkRnhpMNM

@epheien
Copy link

epheien commented May 20, 2019

I think, it's better to merge floating window feature from neovim.
Floating window is more generic than popup.

@junegunn
Copy link

@brammool Thank you for the quick response.

And that preview of travis.yml is put in the terminal window by that fzf program?

Yes, the whole section of the screen is drawn by fzf. It's a generic text filter with --preview functionality.

image

@skywind3000
Copy link
Author

skywind3000 commented Dec 16, 2019

What about a new ! command ? for example !!, or a new version of system() like system_in_popup().

which will execute command in a dedicated popup window and returns after command exit.

Not flexible I know, but enough for tools like fzf.

@brammool
Copy link
Contributor

brammool commented Dec 16, 2019 via email

@blayz3r
Copy link

blayz3r commented Dec 21, 2019

What about a new ! command ? for example !!, which will execute command in a dedicated popup window and returns after command exit. Not flexible I know, but enough for tools like fzf.
That starts looking like something you should do with a new terminal, not inside Vim: :!xterm -c "your command"& I was only thinking of the use case of a dialog to select a file, where the dialog can have keyboard focus until it's closed. Popup windows are intended for that, but so far excluding using a terminal window.

-- This is an airconditioned room, do not open Windows. /// Bram Moolenaar -- Bram@Moolenaar.net -- http://www.Moolenaar.net \\ /// sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ \\ \\ an exciting new programming language -- http://www.Zimbu.org /// \\ help me help AIDS victims -- http://ICCF-Holland.org ///

This would be great to have!

@skywind3000
Copy link
Author

If we have this topmost terminal popup window, maybe we can get keywordprg running in it.
much convenient.

@her
Copy link

her commented Dec 31, 2019

Hi @brammool thanks for Vim!

Just to +1 @junegunn comment above, I need some way to use a :terminal inside your new popup window feature. Here's an example of how I'm using Neovim 'floating' window to call fzf and display output:

Screen Shot 2019-12-30 at 11 06 22 PM

I absolutely love the API you've designed with popup. This would be a very useful addition for us I think.

Happy new year!

@skywind3000
Copy link
Author

@brammool , I am trying to simulate this, by calling term_sendkeys from a popup filter:

function! PopupBuffer(bid)
	let opts = {'mapping':0, 'close':'button'}
	let opts.filter = 'PopupFilter'
	let opts.title = ' POPUP '
	let opts.border = [1,1,1,1,1,1,1,1,1]
	let winid = popup_create(a:bid, opts)
	let opts = {"minwidth":20, "minheight":5, "maxwidth":20, "maxheight":5}
	call popup_move(winid, opts)
	call popup_show(winid)
endfunc

function! PopupFilter(winid, key)
	let bid = winbufnr(a:winid)
	if a:key == "\<c-x>"
		call popup_close(a:winid, 0)
		return 1
	endif
	echo "key: ".a:key
	call term_sendkeys(bid, a:key)
	return 1
endfunc

term
let bid = bufnr('')
wincmd p
call PopupBuffer(bid)

But it displays:

E278: Cannont put a terminal buffer in a popup window.

Can we just remove this restriction ? and I can send keys to a hidden terminal from my popup filter.

@brammool
Copy link
Contributor

brammool commented Feb 2, 2020

It is now possible to run a terminal in a popup window. First create the terminal buffer hidden, then create a popup for that buffer.
It is quite brittle, making a popup window the current window is unexpected and can cause many problems. I have found some things that can go wrong, there probably are several more.

@skywind3000
Copy link
Author

skywind3000 commented Feb 3, 2020

@brammool , I tried it but the popup can not be closed when terminal job finished:

let w = 80
let h = 24
let opts = {'hidden': 1, 'term_rows':h, 'term_cols':w}
let opts.term_kill = 'term'
let opts.norestore = 1
let bid = term_start(['bash'], opts)

let opts = {'maxwidth':w, 'maxheight':h, 'minwidth':w, 'minheight':h}
let opts.wrap = 0
let opts.mapping = 0
let opts.title = 'popup-terminal'
let opts.close = 'button'
let opts.border = [1,1,1,1,1,1,1,1,1]
let opts.drag = 1
let opts.resize = 0
let winid = popup_create(bid, opts)

save this to test.vim and so test.vim in vim 8.2.0200, than type exit in the popup terminal.

the terminal popup should be closed, but it remains there and in the terminal-normal mode.

I tried to add a exit_cb in term_start's opts argument and close popup in the exit_cb but it prompts me when I am invoking popup_close in the exit_cb:

E994: Not allowed in a popup window.

Also, I can use <c-\><c-n> to enter NORMAL mode in the terminal popup, and it behaves very strange.

@skywind3000
Copy link
Author

@brammool use close in the exit_cb function can close the terminal.

@itchyny
Copy link
Contributor

itchyny commented Feb 3, 2020

When I create a terminal in a popup window, &buftype is set to terminal (consider autocmd WinEnter * echom 'WinEnter: ' . &buftype). Should it be popup-terminal or something)? How can we detect popup windows (including a terminal in popup) since there're many limitations in the popup window.

@lacygoill
Copy link

FWIW, I inspect the output of popup_getoptions(win_getid()).

I consider the current window as a popup window if and only if it's not empty.
The downside is that – in addition to evaluating to an empty dictionary – it raises E993 in a non-popup window. So, I use silent!. So far, it seems to work as expected.

But I like your suggestion of setting &buftype to popup-terminal; it would make the code a little less verbose and a little more readable.

@itchyny
Copy link
Contributor

itchyny commented Feb 3, 2020

diff --git a/src/popupwin.c b/src/popupwin.c
index 714f457fe..32adb7935 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -1826,6 +1826,11 @@ popup_create(typval_T *argvars, typval_T *rettv, create_type_T type)
 	win_init_popup_win(wp, buf);
 	set_local_options_default(wp, FALSE);
 	buffer_ensure_loaded(buf);
+#ifdef FEAT_TERMINAL
+	if (buf->b_term != NULL)
+	    set_string_option_direct_in_buf(buf, (char_u *)"buftype", -1,
+		    (char_u *)"popup-terminal", OPT_FREE|OPT_LOCAL, 0);
+#endif
     }
     else
     {

@lacygoill
Copy link

I've noticed that TerminalOpen is triggered when a terminal buffer is created even without a window. I know it's not a bug since it's documented:

This event is triggered even if the buffer is created
without a window, with the ++hidden option.

But I had some issues because of it; some of my terminal-specific configurations were applied to regular buffers, because I didn't think it was necessary to inspect &buftype. I fixed my issues by wrapping all commands executed from TerminalOpen inside an if &bt is# 'terminal'.

@skywind3000
Copy link
Author

skywind3000 commented Feb 3, 2020

Don't use &buftype to detect popup window, a buffer may be displayed in a normal window and in a popup window at the same time. Assuming that you are editing a.c, and use popup window to preview symbol definitions in a.c. How to set the buftype in this situation ?

What you need to detect is window type not buffer type.

I have implemented a popup tag previewer in my plugin: vim-quickui:

I am editing BasicBitmap.cpp and preview definition in the BasicBitmap.h, and BasicBitmap.h is already loaded as a normal buffer in another tabpage. If you change the buftype of BasicBitmap.h it is really confusing.

@brammool
Copy link
Contributor

brammool commented Feb 3, 2020 via email

@itchyny
Copy link
Contributor

itchyny commented Feb 3, 2020

Don't use &buftype to detect popup window, a buffer may be displayed in a normal window and in a popup window at the same time.

Okay, got it. So what's the correct way to detect a popup window after all? I think I just want to expose WIN_IS_POPUP(wp) to Vim script. Maybe &wintype or is_popup_win({nr})? I think empty(popup_getoptions(win_getid())) is too long and allocates unnecessary dictionary.

@chrisbra
Copy link
Member

chrisbra commented Feb 3, 2020

But I had some issues because of it; some of my terminal-specific configurations were applied to regular buffers, because I didn't think it was necessary to inspect &buftype. I fixed my issues by wrapping all commands executed from TerminalOpen inside an if &bt is# 'terminal'.

Can't you simply use a TerminalWinOpen autocommand instead?

@skywind3000
Copy link
Author

skywind3000 commented Feb 3, 2020

@brammool , yes, using a close or quit in terminal's exit_cb can close the popup window:

let w = 80
let h = 24
let opts = {'hidden': 1, 'term_rows':h, 'term_cols':w}
let opts.term_kill = 'term'
let opts.norestore = 1
let opts.exit_cb = 'OnTermExit'
let bid = term_start(['bash'], opts)

function! OnTermExit(job, message)
    close
    " TODO: add some code to confirm that current window is a popup.
    " TODO: prevent close other window by accident.
endfunction

let opts = {'maxwidth':w, 'maxheight':h, 'minwidth':w, 'minheight':h}
let opts.wrap = 0
let opts.mapping = 0
let opts.title = 'popup-terminal'
let opts.close = 'button'
let opts.border = [1,1,1,1,1,1,1,1,1]
let opts.drag = 1
let opts.resize = 0
let winid = popup_create(bid, opts)

It works as expected now.

@lacygoill
Copy link

lacygoill commented Feb 3, 2020

Can't you simply use a TerminalWinOpen autocommand instead?

@chrisbra For some reason, I didn't notice the patch 8.1.2219 (nor the help...); you're right, I should listen to this event instead of TerminalOpen. Thank you very much, it will make my autocmds less verbose.

@skywind3000
Copy link
Author

This also works:

let opts.term_finish = 'close'

But I cannot get the exit code. using a exit_cb can get the exit code.

@lacygoill
Copy link

Okay, got it. So what's the correct way to detect a popup window after all? I think I just want to expose WIN_IS_POPUP(wp) to Vim script. Maybe &wintype or is_popup_win({nr})? I think empty(popup_getoptions(win_getid())) is too long and allocates unnecessary dictionary.

FWIW, I would go with win_ispopup() and/or add the entry popup in getwininfo().

@brammool
Copy link
Contributor

brammool commented Feb 3, 2020 via email

@skywind3000
Copy link
Author

skywind3000 commented Feb 19, 2020

@brammool, the popup_hide() and popup_close are not allowed in the popup terminal, can we allow one of them ??

The popup terminal can be more useful if it can be temporary hidden or shown. (or the relative popup window can be closed and recreated).

An efficient workflow will be possible if so:

  1. When I need to use the shell, I can press <c-F12> to open a popup terminal, unlike split terminal, it will not change my window layout and will not disturb me.

  2. The shell job is not finished, now I want to edit, I don't have to exit the terminal, just press <c-F12> to hide it.

  3. Then, I finish my editing I can use <c-f12> again to bring the popup terminal back and continue my previous works in the shell.

It can make me really productive if the terminal popup can be toggled on/off swiftly by <c-f12> . I even don't need the tmux pane any more if I can use this.

But sadly, I found both popup_hide() and popup_close() are disabled in an unfinished terminal popup. Can we enable one of them ??

@brammool
Copy link
Contributor

This issue was closed, please create a new issue for new items to discuss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests