From cb1828b48626eca6df992b0abf321626108d9918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Linse?= Date: Fri, 11 May 2018 08:41:47 +0200 Subject: [PATCH 1/3] make the gui use grid_line representation --- neovim_gui/gtk_ui.py | 151 +++++++++++++++++++------------------------ neovim_gui/screen.py | 5 +- 2 files changed, 69 insertions(+), 87 deletions(-) diff --git a/neovim_gui/gtk_ui.py b/neovim_gui/gtk_ui.py index 65a1553..5e71019 100644 --- a/neovim_gui/gtk_ui.py +++ b/neovim_gui/gtk_ui.py @@ -91,14 +91,14 @@ def __init__(self, font): self._resize_timer_id = None self._pressed = None self._invalid = None - self._pending = [0, 0, 0] self._reset_cache() + self._attr_defs = {} def start(self, bridge): """Start the UI event loop.""" debug_ext_env = os.environ.get("NVIM_PYTHON_UI_DEBUG_EXT", "") extra_exts = {x:True for x in debug_ext_env.split(",") if x} - bridge.attach(80, 24, rgb=True, **extra_exts) + bridge.attach(80, 24, rgb=True, ext_linegrid=True, **extra_exts) drawing_area = Gtk.DrawingArea() drawing_area.connect('draw', self._gtk_draw) window = Gtk.Window() @@ -138,7 +138,6 @@ def schedule_screen_update(self, apply_updates): """Schedule screen updates to run in the UI event loop.""" def wrapper(): apply_updates() - self._flush() self._start_blinking() self._screen_invalid() GObject.idle_add(wrapper) @@ -146,7 +145,8 @@ def wrapper(): def _screen_invalid(self): self._drawing_area.queue_draw() - def _nvim_resize(self, columns, rows): + def _nvim_grid_resize(self, grid, columns, rows): + assert grid == 1 da = self._drawing_area # create FontDescription object for the selected font/size font_str = '{0} {1}'.format(self._font_name, self._font_size) @@ -173,17 +173,12 @@ def _nvim_resize(self, columns, rows): self._screen = Screen(columns, rows) self._window.resize(pixel_width, pixel_height) - def _nvim_clear(self): + def _nvim_grid_clear(self, grid): self._clear_region(self._screen.top, self._screen.bot + 1, self._screen.left, self._screen.right + 1) self._screen.clear() - def _nvim_eol_clear(self): - row, col = self._screen.row, self._screen.col - self._clear_region(row, row + 1, col, self._screen.right + 1) - self._screen.eol_clear() - - def _nvim_cursor_goto(self, row, col): + def _nvim_grid_cursor_goto(self, grid, row, col): self._screen.cursor_goto(row, col) def _nvim_busy_start(self): @@ -201,18 +196,12 @@ def _nvim_mouse_off(self): def _nvim_mode_change(self, mode): self._insert_cursor = mode == 'insert' - def _nvim_set_scroll_region(self, top, bot, left, right): - self._screen.set_scroll_region(top, bot, left, right) - - def _nvim_scroll(self, count): - self._flush() - top, bot = self._screen.top, self._screen.bot + 1 - left, right = self._screen.left, self._screen.right + 1 + def _nvim_grid_scroll(self, grid, top, bot, left, right, rows, cols): # The diagrams below illustrate what will happen, depending on the # scroll direction. "=" is used to represent the SR(scroll region) # boundaries and "-" the moved rectangles. note that dst and src share # a common region - if count > 0: + if rows > 0: # move an rectangle in the SR up, this can happen while scrolling # down # +-------------------------+ @@ -224,8 +213,8 @@ def _nvim_scroll(self, count): # |-------------------------| dst_bot | # | src (cleared) | | # +=========================+ src_bot - src_top, src_bot = top + count, bot - dst_top, dst_bot = top, bot - count + src_top, src_bot = top + rows, bot + dst_top, dst_bot = top, bot - rows clr_top, clr_bot = dst_bot, src_bot else: # move a rectangle in the SR down, this can happen while scrolling @@ -239,8 +228,8 @@ def _nvim_scroll(self, count): # |=========================| dst_bot | # | (clipped below SR) | v # +-------------------------+ - src_top, src_bot = top, bot + count - dst_top, dst_bot = top - count, bot + src_top, src_bot = top, bot + rows + dst_top, dst_bot = top - rows, bot clr_top, clr_bot = src_top, dst_top self._cairo_surface.flush() self._cairo_context.save() @@ -255,21 +244,50 @@ def _nvim_scroll(self, count): self._cairo_context.restore() # Clear the emptied region self._clear_region(clr_top, clr_bot, left, right) - self._screen.scroll(count) + self._screen.scroll(rows) - def _nvim_highlight_set(self, attrs): - self._attrs = self._get_pango_attrs(attrs) + def _nvim_hl_attr_define(self, hlid, attr, cterm_attr, info): + self._attr_defs[hlid] = attr + + def _nvim_grid_line(self, grid, row, col_start, cells): + assert grid == 1 - def _nvim_put(self, text): - if self._screen.row != self._pending[0]: - # flush pending text if jumped to a different row - self._flush() - # work around some redraw glitches that can happen - self._redraw_glitch_fix() # Update internal screen - self._screen.put(self._get_pango_text(text), self._attrs) - self._pending[1] = min(self._screen.col - 1, self._pending[1]) - self._pending[2] = max(self._screen.col, self._pending[2]) + col = col_start + attr = None # will be set in first cell + for cell in cells: + text = cell[0] + if len(cell) > 1: + hl_id = cell[1] + attr = self._get_pango_attrs(hl_id) + repeat = cell[2] if len(cell) > 2 else 1 + for i in range(repeat): + self._screen.put(row, col, self._get_pango_text(text), attr) + col += 1 + col_end = col + + # work around some redraw glitches that can happen + col_start, col_end = self._redraw_glitch_fix(row, col_start, col_end) + + self._cairo_context.save() + ccol = col_start + buf = [] + bold = False + for _, col, text, attrs in self._screen.iter(row, row, col_start, + col_end - 1): + newbold = attrs and 'bold' in attrs[0] + if newbold != bold or not text: + if buf: + self._pango_draw(row, ccol, buf) + bold = newbold + buf = [(text, attrs,)] + ccol = col + else: + buf.append((text, attrs,)) + if buf: + self._pango_draw(row, ccol, buf) + self._cairo_context.restore() + def _nvim_bell(self): self._window.get_window().beep() @@ -277,11 +295,8 @@ def _nvim_bell(self): def _nvim_visual_bell(self): pass - def _nvim_update_fg(self, fg): + def _nvim_default_colors_set(self, fg, bg, sp, cterm_fg, cterm_bg): self._foreground = fg - self._reset_cache() - - def _nvim_update_bg(self, bg): self._background = bg self._reset_cache() @@ -430,7 +445,6 @@ def blink(*args): blink() def _clear_region(self, top, bot, left, right): - self._flush() self._cairo_context.save() self._mask_region(top, bot, left, right) r, g, b = _split_color(self._background) @@ -456,37 +470,11 @@ def _get_coords(self, row, col): y = row * self._cell_pixel_height return x, y - def _flush(self): - row, startcol, endcol = self._pending - self._pending[0] = self._screen.row - self._pending[1] = self._screen.col - self._pending[2] = self._screen.col - if startcol == endcol: - return - self._cairo_context.save() - ccol = startcol - buf = [] - bold = False - for _, col, text, attrs in self._screen.iter(row, row, startcol, - endcol - 1): - newbold = attrs and 'bold' in attrs[0] - if newbold != bold or not text: - if buf: - self._pango_draw(row, ccol, buf) - bold = newbold - buf = [(text, attrs,)] - ccol = col - else: - buf.append((text, attrs,)) - if buf: - self._pango_draw(row, ccol, buf) - self._cairo_context.restore() - def _pango_draw(self, row, col, data, cr=None, cursor=False): markup = [] for text, attrs in data: if not attrs: - attrs = self._get_pango_attrs(None) + attrs = self._get_pango_attrs(0) attrs = attrs[1] if cursor else attrs[0] markup.append('{1}'.format(attrs, text)) markup = ''.join(markup) @@ -511,10 +499,10 @@ def _get_pango_text(self, text): self._pango_text_cache[text] = rv return rv - def _get_pango_attrs(self, attrs): - key = tuple(sorted((k, v,) for k, v in (attrs or {}).items())) - rv = self._pango_attrs_cache.get(key, None) + def _get_pango_attrs(self, hl_id): + rv = self._pango_attrs_cache.get(hl_id, None) if rv is None: + attrs = self._attr_defs.get(hl_id, {}) fg = self._foreground if self._foreground != -1 else 0 bg = self._background if self._background != -1 else 0xffffff n = { @@ -548,36 +536,31 @@ def _get_pango_attrs(self, attrs): n = ' '.join(['{0}="{1}"'.format(k, v) for k, v in n.items()]) c = ' '.join(['{0}="{1}"'.format(k, v) for k, v in c.items()]) rv = (n, c,) - self._pango_attrs_cache[key] = rv + self._pango_attrs_cache[hl_id] = rv return rv def _reset_cache(self): self._pango_text_cache = {} self._pango_attrs_cache = {} - def _redraw_glitch_fix(self): - row, col = self._screen.row, self._screen.col - text, attrs = self._screen.get_cursor() + def _redraw_glitch_fix(self, row, col_start, col_end): # when updating cells in italic or bold words, the result can become # messy(characters can be clipped or leave remains when removed). To # prevent that, always update non empty sequences of cells and the # surrounding space. # find the start of the sequence - lcol = col - 1 - while lcol >= 0: - text, _ = self._screen.get_cell(row, lcol) - lcol -= 1 + while col_start-1 >= 0: + text, _ = self._screen.get_cell(row, col_start-1) if text == ' ': break - self._pending[1] = min(lcol + 1, self._pending[1]) + col_start -= 1 # find the end of the sequence - rcol = col + 1 - while rcol < self._screen.columns: - text, _ = self._screen.get_cell(row, rcol) - rcol += 1 + while col_end < self._screen.columns: + text, _ = self._screen.get_cell(row, col_end) if text == ' ': break - self._pending[2] = max(rcol, self._pending[2]) + col_end += 1 + return col_start, col_end def _split_color(n): diff --git a/neovim_gui/screen.py b/neovim_gui/screen.py index 6ebf99b..01a6ce4 100644 --- a/neovim_gui/screen.py +++ b/neovim_gui/screen.py @@ -88,11 +88,10 @@ def scroll(self, count): for row in range(stop, stop + count, step): self._clear_region(row, row, left, right) - def put(self, text, attrs): + def put(self, row, col, text, attrs): """Put character on virtual cursor position.""" - cell = self._cells[self.row][self.col] + cell = self._cells[row][col] cell.set(text, attrs) - self.cursor_goto(self.row, self.col + 1) def get_cell(self, row, col): """Get text, attrs at row, col.""" From 67260789acf103f875aa711489bd2911ccbc668b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Linse?= Date: Sun, 5 Nov 2017 17:55:23 +0100 Subject: [PATCH 2/3] implement multigrid abstraction --- neovim_gui/gtk_ui.py | 252 ++++++++++++++++++++++++---------------- neovim_gui/screen.py | 5 + neovim_gui/ui_bridge.py | 8 +- 3 files changed, 160 insertions(+), 105 deletions(-) diff --git a/neovim_gui/gtk_ui.py b/neovim_gui/gtk_ui.py index 5e71019..d1dcbba 100644 --- a/neovim_gui/gtk_ui.py +++ b/neovim_gui/gtk_ui.py @@ -2,6 +2,9 @@ from __future__ import print_function, division import math import os +import sys + +from functools import partial import cairo @@ -69,6 +72,8 @@ def Rectangle(x, y, w, h): r.x, r.y, r.width, r.height = x, y, w, h return r +class Grid(object): + pass class GtkUI(object): @@ -81,26 +86,26 @@ def __init__(self, font): self._background = -1 self._font_name = font[0] self._font_size = font[1] - self._screen = None self._attrs = None self._busy = False - self._mouse_enabled = False + self._mouse_enabled = True self._insert_cursor = False self._blink = False self._blink_timer_id = None - self._resize_timer_id = None self._pressed = None self._invalid = None self._reset_cache() self._attr_defs = {} - - def start(self, bridge): - """Start the UI event loop.""" - debug_ext_env = os.environ.get("NVIM_PYTHON_UI_DEBUG_EXT", "") - extra_exts = {x:True for x in debug_ext_env.split(",") if x} - bridge.attach(80, 24, rgb=True, ext_linegrid=True, **extra_exts) + self._curgrid = 0 + self.grids = {} + self.g = None + + def create_drawing_area(self, handle): + g = Grid() + g.handle = handle + g._resize_timer_id = None drawing_area = Gtk.DrawingArea() - drawing_area.connect('draw', self._gtk_draw) + drawing_area.connect('draw', partial(self._gtk_draw, g)) window = Gtk.Window() window.add(drawing_area) window.set_events(window.get_events() | @@ -108,25 +113,43 @@ def start(self, bridge): Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.SCROLL_MASK) - window.connect('configure-event', self._gtk_configure) + window.connect('configure-event', partial(self._gtk_configure, g)) window.connect('delete-event', self._gtk_quit) window.connect('key-press-event', self._gtk_key) window.connect('key-release-event', self._gtk_key_release) - window.connect('button-press-event', self._gtk_button_press) - window.connect('button-release-event', self._gtk_button_release) - window.connect('motion-notify-event', self._gtk_motion_notify) - window.connect('scroll-event', self._gtk_scroll) + window.connect('button-press-event', partial(self._gtk_button_press, g)) + window.connect('button-release-event', partial(self._gtk_button_release, g)) + window.connect('motion-notify-event', partial(self._gtk_motion_notify, g)) + window.connect('scroll-event', partial(self._gtk_scroll, g)) window.connect('focus-in-event', self._gtk_focus_in) window.connect('focus-out-event', self._gtk_focus_out) window.show_all() + g._pango_context = drawing_area.create_pango_context() + g._drawing_area = drawing_area + g._window = window + g._pending = [0, 0, 0] + g._screen = None + + self.grids[handle] = g + return g + + + def start(self, bridge): + """Start the UI event loop.""" + opts = {} + debug_ext_env = os.environ.get("NVIM_PYTHON_UI_DEBUG_EXT", "") + opts = {x:True for x in debug_ext_env.split(",") if x} + self.has_float = False + if 'ext_float' in bridge._nvim.metadata['ui_options']: + opts['ext_float'] = True + self.has_float = True + bridge.attach(80, 24, rgb=True, ext_multigrid=True, **opts) im_context = Gtk.IMMulticontext() - im_context.set_client_window(drawing_area.get_window()) im_context.set_use_preedit(False) # TODO: preedit at cursor position im_context.connect('commit', self._gtk_input) - self._pango_context = drawing_area.create_pango_context() - self._drawing_area = drawing_area - self._window = window self._im_context = im_context + self.g = self.create_drawing_area(1) + self._window = self.g._window self._bridge = bridge Gtk.main() @@ -139,15 +162,26 @@ def schedule_screen_update(self, apply_updates): def wrapper(): apply_updates() self._start_blinking() - self._screen_invalid() + self._im_context.set_client_window(self.g._drawing_area.get_window()) + for g in self.grids.values(): + g._drawing_area.queue_draw() GObject.idle_add(wrapper) - def _screen_invalid(self): - self._drawing_area.queue_draw() + def _nvim_grid_cursor_goto(self, grid, row, col): + g = self.grids[grid] + self.g = g + if g._screen is not None: + # TODO: this should really be asserted on the nvim side + row, col = min(row, g._screen.rows-1), min(col, g._screen.columns-1) + g._screen.cursor_goto(row,col) + self._window= self.g._window def _nvim_grid_resize(self, grid, columns, rows): - assert grid == 1 - da = self._drawing_area + print("da") + if grid not in self.grids: + self.create_drawing_area(grid) + g = self.grids[grid] + da = g._drawing_area # create FontDescription object for the selected font/size font_str = '{0} {1}'.format(self._font_name, self._font_size) self._font, pixels, normal_width, bold_width = _parse_font(font_str) @@ -160,26 +194,25 @@ def _nvim_grid_resize(self, grid, columns, rows): pixel_height = cell_pixel_height * rows gdkwin = da.get_window() content = cairo.CONTENT_COLOR - self._cairo_surface = gdkwin.create_similar_surface(content, + g._cairo_surface = gdkwin.create_similar_surface(content, pixel_width, pixel_height) - self._cairo_context = cairo.Context(self._cairo_surface) - self._pango_layout = PangoCairo.create_layout(self._cairo_context) - self._pango_layout.set_alignment(Pango.Alignment.LEFT) - self._pango_layout.set_font_description(self._font) - self._pixel_width, self._pixel_height = pixel_width, pixel_height + g._cairo_context = cairo.Context(g._cairo_surface) + g._pango_layout = PangoCairo.create_layout(g._cairo_context) + g._pango_layout.set_alignment(Pango.Alignment.LEFT) + g._pango_layout.set_font_description(self._font) + g._pixel_width, g._pixel_height = pixel_width, pixel_height self._cell_pixel_width = cell_pixel_width self._cell_pixel_height = cell_pixel_height - self._screen = Screen(columns, rows) - self._window.resize(pixel_width, pixel_height) + g._screen = Screen(columns, rows) + g._window.resize(pixel_width, pixel_height) def _nvim_grid_clear(self, grid): - self._clear_region(self._screen.top, self._screen.bot + 1, - self._screen.left, self._screen.right + 1) - self._screen.clear() + g = self.grids[grid] + self._clear_region(g, g._screen.top, g._screen.bot + 1, + g._screen.left, g._screen.right + 1) + g._screen.clear() - def _nvim_grid_cursor_goto(self, grid, row, col): - self._screen.cursor_goto(row, col) def _nvim_busy_start(self): self._busy = True @@ -197,6 +230,7 @@ def _nvim_mode_change(self, mode): self._insert_cursor = mode == 'insert' def _nvim_grid_scroll(self, grid, top, bot, left, right, rows, cols): + g = self.grids[grid] # The diagrams below illustrate what will happen, depending on the # scroll direction. "=" is used to represent the SR(scroll region) # boundaries and "-" the moved rectangles. note that dst and src share @@ -231,28 +265,32 @@ def _nvim_grid_scroll(self, grid, top, bot, left, right, rows, cols): src_top, src_bot = top, bot + rows dst_top, dst_bot = top - rows, bot clr_top, clr_bot = src_top, dst_top - self._cairo_surface.flush() - self._cairo_context.save() + g._cairo_surface.flush() + g._cairo_context.save() # The move is performed by setting the source surface to itself, but # with a coordinate transformation. _, y = self._get_coords(dst_top - src_top, 0) - self._cairo_context.set_source_surface(self._cairo_surface, 0, y) + g._cairo_context.set_source_surface(g._cairo_surface, 0, y) # Clip to ensure only dst is affected by the change - self._mask_region(dst_top, dst_bot, left, right) + self._mask_region(g, dst_top, dst_bot, left, right) # Do the move - self._cairo_context.paint() - self._cairo_context.restore() + g._cairo_context.paint() + g._cairo_context.restore() # Clear the emptied region - self._clear_region(clr_top, clr_bot, left, right) - self._screen.scroll(rows) + self._clear_region(g, clr_top, clr_bot, left, right) + g._screen.scroll(rows) def _nvim_hl_attr_define(self, hlid, attr, cterm_attr, info): self._attr_defs[hlid] = attr def _nvim_grid_line(self, grid, row, col_start, cells): - assert grid == 1 # Update internal screen + + g = self.grids[grid] + screen = self.grids[grid]._screen + # TODO: delet this + # Update internal screen col = col_start attr = None # will be set in first cell for cell in cells: @@ -262,31 +300,31 @@ def _nvim_grid_line(self, grid, row, col_start, cells): attr = self._get_pango_attrs(hl_id) repeat = cell[2] if len(cell) > 2 else 1 for i in range(repeat): - self._screen.put(row, col, self._get_pango_text(text), attr) + screen.put(row, col, self._get_pango_text(text), attr) col += 1 col_end = col # work around some redraw glitches that can happen - col_start, col_end = self._redraw_glitch_fix(row, col_start, col_end) + col_start, col_end = self._redraw_glitch_fix(g, row, col_start, col_end) - self._cairo_context.save() + g._cairo_context.save() ccol = col_start buf = [] bold = False - for _, col, text, attrs in self._screen.iter(row, row, col_start, + for _, col, text, attrs in screen.iter(row, row, col_start, col_end - 1): newbold = attrs and 'bold' in attrs[0] if newbold != bold or not text: if buf: - self._pango_draw(row, ccol, buf) + self._pango_draw(g, row, ccol, buf) bold = newbold buf = [(text, attrs,)] ccol = col else: buf.append((text, attrs,)) if buf: - self._pango_draw(row, ccol, buf) - self._cairo_context.restore() + self._pango_draw(g, row, ccol, buf) + g._cairo_context.restore() def _nvim_bell(self): @@ -309,50 +347,52 @@ def _nvim_set_title(self, title): def _nvim_set_icon(self, icon): self._window.set_icon_name(icon) - def _gtk_draw(self, wid, cr): - if not self._screen: + def _gtk_draw(self, g, wid, cr): + if not g._screen: return # from random import random # cr.rectangle(0, 0, self._pixel_width, self._pixel_height) # cr.set_source_rgb(random(), random(), random()) # cr.fill() - self._cairo_surface.flush() + g._cairo_surface.flush() cr.save() - cr.rectangle(0, 0, self._pixel_width, self._pixel_height) + + cr.rectangle(0, 0, g._pixel_width, g._pixel_height) cr.clip() - cr.set_source_surface(self._cairo_surface, 0, 0) + cr.set_source_surface(g._cairo_surface, 0, 0) cr.paint() cr.restore() - if not self._busy and self._blink: + if not self._busy and self._blink and g is self.g: # Cursor is drawn separately in the window. This approach is # simpler because it doesn't taint the internal cairo surface, # which is used for scrolling - row, col = self._screen.row, self._screen.col - text, attrs = self._screen.get_cursor() - self._pango_draw(row, col, [(text, attrs,)], cr=cr, cursor=True) + row, col = g._screen.row, g._screen.col + text, attrs = g._screen.get_cursor() + self._pango_draw(g, row, col, [(text, attrs,)], cr=cr, cursor=True) x, y = self._get_coords(row, col) currect = Rectangle(x, y, self._cell_pixel_width, self._cell_pixel_height) self._im_context.set_cursor_location(currect) - def _gtk_configure(self, widget, event): + def _gtk_configure(self, g, widget, event): def resize(*args): self._resize_timer_id = None - width, height = self._window.get_size() + width, height = g._window.get_size() columns = width // self._cell_pixel_width rows = height // self._cell_pixel_height - if self._screen.columns == columns and self._screen.rows == rows: + if g._screen.columns == columns and g._screen.rows == rows: return - self._bridge.resize(columns, rows) + ## TODO: this must tell the grid + self._bridge.resize(g.handle, columns, rows) - if not self._screen: + if not g._screen: return - if event.width == self._pixel_width and \ - event.height == self._pixel_height: + if event.width == g._pixel_width and \ + event.height == g._pixel_height: return - if self._resize_timer_id is not None: - GLib.source_remove(self._resize_timer_id) - self._resize_timer_id = GLib.timeout_add(250, resize) + if g._resize_timer_id is not None: + GLib.source_remove(g._resize_timer_id) + g._resize_timer_id = GLib.timeout_add(250, resize) def _gtk_quit(self, *args): self._bridge.exit() @@ -382,7 +422,7 @@ def _gtk_key(self, widget, event, *args): def _gtk_key_release(self, widget, event, *args): self._im_context.filter_keypress(event) - def _gtk_button_press(self, widget, event, *args): + def _gtk_button_press(self, g, widget, event, *args): if not self._mouse_enabled or event.type != Gdk.EventType.BUTTON_PRESS: return button = 'Left' @@ -393,24 +433,31 @@ def _gtk_button_press(self, widget, event, *args): col = int(math.floor(event.x / self._cell_pixel_width)) row = int(math.floor(event.y / self._cell_pixel_height)) input_str = _stringify_key(button + 'Mouse', event.state) - input_str += '<{0},{1}>'.format(col, row) + if self.has_float: + input_str += '<{},{},{}>'.format(g.handle, col, row) + else: + input_str += '<{},{}>'.format(col, row) + print(input_str,file=sys.stderr) self._bridge.input(input_str) self._pressed = button return True - def _gtk_button_release(self, widget, event, *args): + def _gtk_button_release(self, g, widget, event, *args): self._pressed = None - def _gtk_motion_notify(self, widget, event, *args): + def _gtk_motion_notify(self, g, widget, event, *args): if not self._mouse_enabled or not self._pressed: return col = int(math.floor(event.x / self._cell_pixel_width)) row = int(math.floor(event.y / self._cell_pixel_height)) input_str = _stringify_key(self._pressed + 'Drag', event.state) - input_str += '<{0},{1}>'.format(col, row) + if self.has_float: + input_str += '<{},{},{}>'.format(g.handle, col, row) + else: + input_str += '<{},{}>'.format(col, row) self._bridge.input(input_str) - def _gtk_scroll(self, widget, event, *args): + def _gtk_scroll(self, g, widget, event, *args): if not self._mouse_enabled: return col = int(math.floor(event.x / self._cell_pixel_width)) @@ -422,7 +469,7 @@ def _gtk_scroll(self, widget, event, *args): else: return input_str = _stringify_key(key, event.state) - input_str += '<{0},{1}>'.format(col, row) + input_str += '<{},{},{}>'.format(g.handle, col, row) self._bridge.input(input_str) def _gtk_focus_in(self, *a): @@ -437,25 +484,24 @@ def _gtk_input(self, widget, input_str, *args): def _start_blinking(self): def blink(*args): self._blink = not self._blink - self._screen_invalid() + self.g._drawing_area.queue_draw() self._blink_timer_id = GLib.timeout_add(500, blink) if self._blink_timer_id: GLib.source_remove(self._blink_timer_id) self._blink = False blink() - def _clear_region(self, top, bot, left, right): - self._cairo_context.save() - self._mask_region(top, bot, left, right) - r, g, b = _split_color(self._background) - r, g, b = r / 255.0, g / 255.0, b / 255.0 - self._cairo_context.set_source_rgb(r, g, b) - self._cairo_context.paint() - self._cairo_context.restore() - - def _mask_region(self, top, bot, left, right, cr=None): - if not cr: - cr = self._cairo_context + def _clear_region(self, g, top, bot, left, right): + g._cairo_context.save() + self._mask_region(g, top, bot, left, right) + red, green, blue = _split_color(self._background) + red, green, blue = red / 255.0, green / 255.0, blue / 255.0 + g._cairo_context.set_source_rgb(red, green, blue) + g._cairo_context.paint() + g._cairo_context.restore() + + def _mask_region(self, g, top, bot, left, right): + cr = g._cairo_context x1, y1, x2, y2 = self._get_rect(top, bot, left, right) cr.rectangle(x1, y1, x2 - x1, y2 - y1) cr.clip() @@ -470,7 +516,7 @@ def _get_coords(self, row, col): y = row * self._cell_pixel_height return x, y - def _pango_draw(self, row, col, data, cr=None, cursor=False): + def _pango_draw(self, g, row, col, data, cr=None, cursor=False): markup = [] for text, attrs in data: if not attrs: @@ -478,19 +524,19 @@ def _pango_draw(self, row, col, data, cr=None, cursor=False): attrs = attrs[1] if cursor else attrs[0] markup.append('{1}'.format(attrs, text)) markup = ''.join(markup) - self._pango_layout.set_markup(markup, -1) + g._pango_layout.set_markup(markup, -1) # Draw the text if not cr: - cr = self._cairo_context + cr = g._cairo_context x, y = self._get_coords(row, col) - if cursor and self._insert_cursor: + if cursor and self._insert_cursor and g is self.g: cr.rectangle(x, y, self._cell_pixel_width / 4, self._cell_pixel_height) cr.clip() cr.move_to(x, y) - PangoCairo.update_layout(cr, self._pango_layout) - PangoCairo.show_layout(cr, self._pango_layout) - _, r = self._pango_layout.get_pixel_extents() + PangoCairo.update_layout(cr, g._pango_layout) + PangoCairo.show_layout(cr, g._pango_layout) + _, r = g._pango_layout.get_pixel_extents() def _get_pango_text(self, text): rv = self._pango_text_cache.get(text, None) @@ -543,20 +589,20 @@ def _reset_cache(self): self._pango_text_cache = {} self._pango_attrs_cache = {} - def _redraw_glitch_fix(self, row, col_start, col_end): + def _redraw_glitch_fix(self, g, row, col_start, col_end): # when updating cells in italic or bold words, the result can become # messy(characters can be clipped or leave remains when removed). To # prevent that, always update non empty sequences of cells and the # surrounding space. # find the start of the sequence while col_start-1 >= 0: - text, _ = self._screen.get_cell(row, col_start-1) + text, _ = g._screen.get_cell(row, col_start-1) if text == ' ': break col_start -= 1 # find the end of the sequence - while col_end < self._screen.columns: - text, _ = self._screen.get_cell(row, col_end) + while col_end < g._screen.columns: + text, _ = g._screen.get_cell(row, col_end) if text == ' ': break col_end += 1 diff --git a/neovim_gui/screen.py b/neovim_gui/screen.py index 01a6ce4..d13f4ef 100644 --- a/neovim_gui/screen.py +++ b/neovim_gui/screen.py @@ -93,6 +93,11 @@ def put(self, row, col, text, attrs): cell = self._cells[row][col] cell.set(text, attrs) + def put_cell(self, row, col, text, attrs): + """Put character on position.""" + cell = self._cells[row][col] + cell.set(text, attrs) + def get_cell(self, row, col): """Get text, attrs at row, col.""" return self._cells[row][col].get() diff --git a/neovim_gui/ui_bridge.py b/neovim_gui/ui_bridge.py index 408e3a3..f4becf0 100644 --- a/neovim_gui/ui_bridge.py +++ b/neovim_gui/ui_bridge.py @@ -47,9 +47,12 @@ def input(self, input_str): """Send input to nvim.""" self._call(self._nvim.input, input_str) - def resize(self, columns, rows): + def resize(self, grid, columns, rows): """Send a resize request to nvim.""" - self._call(self._nvim.ui_try_resize, columns, rows) + if 'ext_float' in self._nvim.metadata['ui_options']: + self._call(self._nvim.api.ui_grid_try_resize, grid, columns, rows) + else: + self._call(self._nvim.api.ui_try_resize, columns, rows) def attach(self, columns, rows, **options): """Attach the UI to nvim.""" @@ -101,6 +104,7 @@ def apply_updates(): handler = getattr(self._ui, '_nvim_' + update[0]) nparam = len(signature(handler).parameters) + except AttributeError: if self.debug_events: print(repr(update), file=sys.stdout) From d460e89caf3999b03311fd0ec1e11e1cdd8a1d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Linse?= Date: Sun, 7 Jan 2018 14:46:44 +0100 Subject: [PATCH 3/3] implement proper floating windows --- neovim_gui/gtk_ui.py | 90 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/neovim_gui/gtk_ui.py b/neovim_gui/gtk_ui.py index d1dcbba..9350a93 100644 --- a/neovim_gui/gtk_ui.py +++ b/neovim_gui/gtk_ui.py @@ -5,6 +5,7 @@ import sys from functools import partial +from types import SimpleNamespace import cairo @@ -100,14 +101,29 @@ def __init__(self, font): self.grids = {} self.g = None - def create_drawing_area(self, handle): + def get_grid(self, handle): + if handle in self.grids: + return self.grids[handle] g = Grid() g.handle = handle - g._resize_timer_id = None + g._pending = [0, 0, 0] + g._screen = None drawing_area = Gtk.DrawingArea() drawing_area.connect('draw', partial(self._gtk_draw, g)) + g._pango_context = drawing_area.create_pango_context() + g._drawing_area = drawing_area + g._window = None + g.options = None + self.grids[handle] = g + return g + + def create_window(self, handle): + g = self.get_grid(handle) + g._resize_timer_id = None window = Gtk.Window() - window.add(drawing_area) + layout = Gtk.Fixed() + window.add(layout) + layout.put(g._drawing_area,0,0) window.set_events(window.get_events() | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | @@ -124,14 +140,9 @@ def create_drawing_area(self, handle): window.connect('focus-in-event', self._gtk_focus_in) window.connect('focus-out-event', self._gtk_focus_out) window.show_all() - g._pango_context = drawing_area.create_pango_context() - g._drawing_area = drawing_area g._window = window - g._pending = [0, 0, 0] - g._screen = None + g._layout = layout - self.grids[handle] = g - return g def start(self, bridge): @@ -148,8 +159,10 @@ def start(self, bridge): im_context.set_use_preedit(False) # TODO: preedit at cursor position im_context.connect('commit', self._gtk_input) self._im_context = im_context - self.g = self.create_drawing_area(1) + self.create_window(1) + self.g = self.get_grid(1) self._window = self.g._window + self._layout = self.g._layout self._bridge = bridge Gtk.main() @@ -168,19 +181,60 @@ def wrapper(): GObject.idle_add(wrapper) def _nvim_grid_cursor_goto(self, grid, row, col): - g = self.grids[grid] + g = self.get_grid(grid) self.g = g if g._screen is not None: # TODO: this should really be asserted on the nvim side row, col = min(row, g._screen.rows-1), min(col, g._screen.columns-1) g._screen.cursor_goto(row,col) self._window= self.g._window + self._screen = self.g._screen + + def _nvim_float_info(self, win, handle, width, height, options): + g = self.get_grid(handle) + g.nvim_win = win + g.options = SimpleNamespace(**options) + self.configure_float(g) + + def _nvim_float_close(self, win, handle): + g = self.get_grid(handle) + + if g._window is not None: + g._layout.remove(g._drawing_area) + g._window.destroy() + elif g._drawing_area.get_parent() == self._layout: + self._layout.remove(g._drawing_area) + + def configure_float(self, g): + if g.options.standalone: + if not g._window: + if g._drawing_area.get_parent() == self._layout: + self._layout.remove(g._drawing_area) + self.create_window(g.handle) + else: + if g._window is not None: + g._layout.remove(g._drawing_area) + g._window.destroy() + # this is ugly, but I'm too lazy to refactor nvim_resize + # to fit the flow of information + if g._drawing_area.get_parent() != self._layout: + self._layout.add(g._drawing_area) + g._drawing_area.show() + if g._screen is not None: + x = g.options.x*self._cell_pixel_width + y = g.options.y*self._cell_pixel_height + w,h = g.pixel_size + if len(g.options.anchor) >= 2: + if g.options.anchor[0] == 'S': + y -= h + if g.options.anchor[1] == 'E': + x -= w + self._layout.move(g._drawing_area,x,y) + def _nvim_grid_resize(self, grid, columns, rows): print("da") - if grid not in self.grids: - self.create_drawing_area(grid) - g = self.grids[grid] + g = self.get_grid(grid) da = g._drawing_area # create FontDescription object for the selected font/size font_str = '{0} {1}'.format(self._font_name, self._font_size) @@ -205,7 +259,13 @@ def _nvim_grid_resize(self, grid, columns, rows): self._cell_pixel_width = cell_pixel_width self._cell_pixel_height = cell_pixel_height g._screen = Screen(columns, rows) - g._window.resize(pixel_width, pixel_height) + g._drawing_area.set_size_request(pixel_width, pixel_height) + g.pixel_size = pixel_width, pixel_height + if g.options is not None: + self.configure_float(g) + + if g._window is not None: + g._window.resize(pixel_width, pixel_height) def _nvim_grid_clear(self, grid): g = self.grids[grid]