Refactor the tab bar into its own class

Also ensure that Screen APIs are only used in the child thread in the
tab bar
This commit is contained in:
Kovid Goyal 2017-08-29 15:53:14 +05:30
parent acdeaaeda5
commit a56e1296fd
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 146 additions and 97 deletions

View File

@ -112,10 +112,10 @@ class Selection: # {{{
# }}}
def calculate_gl_geometry(window_geometry):
dx, dy = 2 * cell_size.width / viewport_size.width, 2 * cell_size.height / viewport_size.height
xmargin = window_geometry.left / viewport_size.width
ymargin = window_geometry.top / viewport_size.height
def calculate_gl_geometry(window_geometry, viewport_width, viewport_height, cell_width, cell_height):
dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height
xmargin = window_geometry.left / viewport_width
ymargin = window_geometry.top / viewport_height
xstart = -1 + 2 * xmargin
ystart = 1 - 2 * ymargin
return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy)
@ -173,7 +173,7 @@ class CharGrid:
self.vao_id = None
def update_position(self, window_geometry):
self.screen_geometry = calculate_gl_geometry(window_geometry)
self.screen_geometry = calculate_gl_geometry(window_geometry, viewport_size.width, viewport_size.height, cell_size.width, cell_size.height)
def resize(self, window_geometry):
self.update_position(window_geometry)

View File

@ -2,10 +2,10 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from collections import deque
from collections import deque, namedtuple
from ctypes import addressof
from functools import partial
from threading import Lock
from queue import Queue, Empty
from .borders import Borders
from .char_grid import calculate_gl_geometry, render_cells
@ -23,6 +23,9 @@ from .utils import color_as_int
from .window import Window
TabbarData = namedtuple('TabbarData', 'title is_active is_last')
def SpecialWindow(cmd, stdin=None, override_title=None):
return (cmd, stdin, override_title)
@ -203,65 +206,139 @@ class Tab:
return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self)))
class TabManager:
class TabBar:
def __init__(self, opts, args, startup_session):
self.opts, self.args = opts, args
def __init__(self, data, opts):
self.num_tabs = 1
self.cell_width = 1
self.queue = Queue()
self.vao_id = None
self.tabbar_lock = Lock()
self.tabs = [Tab(opts, args, self.title_changed, t) for t in startup_session.tabs]
self.color_table = build_ansi_color_table(self.opts)
self.cell_ranges = []
self.active_tab_idx = startup_session.active_tab_idx
self.tabbar_dirty = True
self.default_fg = color_as_int(opts.inactive_tab_foreground)
self.default_bg = color_as_int(opts.inactive_tab_background)
self.tab_bar_blank_rects = ()
self.render_buf = None
self.dirty = True
self.screen = s = Screen(None, 1, 10)
s.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
s.color_profile.set_configured_colors(
color_as_int(opts.inactive_tab_foreground),
color_as_int(opts.inactive_tab_background)
)
s.color_profile.dirty = True
self.blank_rects = ()
self.current_data = data
def as_rgb(x):
return (x << 8) | 2
self.active_bg = as_rgb(color_as_int(opts.active_tab_background))
self.active_fg = as_rgb(color_as_int(opts.active_tab_foreground))
self.can_render = False
def layout(self, viewport_width, viewport_height, cell_width, cell_height):
' Must be called in the child thread '
self.cell_width = cell_width
s = self.screen
ncells = viewport_width // cell_width
s.resize(1, ncells)
s.reset_mode(DECAWM)
self.render_buf = (GLuint * (s.lines * s.columns * DATA_CELL_SIZE))()
margin = (viewport_width - ncells * cell_width) // 2
self.window_geometry = g = WindowGeometry(
margin, viewport_height - cell_size.height, viewport_width - margin, viewport_height, s.columns, s.lines)
if margin > 0:
self.tab_bar_blank_rects = (Rect(0, g.top, g.left, g.bottom), Rect(g.right - 1, g.top, viewport_width, g.bottom))
else:
self.tab_bar_blank_rects = ()
self.screen_geometry = calculate_gl_geometry(g, viewport_width, viewport_height, cell_width, cell_height)
self.update()
def update(self):
' Must be called in the child thread '
if self.render_buf is None:
return
s = self.screen
s.cursor.x = 0
s.erase_in_line(2, False)
while True:
try:
self.current_data = self.queue.get_nowait()
except Empty:
break
max_title_length = (self.screen_geometry.xnum // len(self.current_data)) - 1
cr = []
for t in self.current_data:
s.cursor.bg = self.active_bg if t.is_active else 0
s.cursor.fg = self.active_fg if t.is_active else 0
s.cursor.bold = s.cursor.italic = t.is_active
before = s.cursor.x
s.draw(t.title)
extra = s.cursor.x - before - max_title_length
if extra > 0:
s.cursor.x -= extra + 1
s.draw('')
cr.append((before, s.cursor.x))
s.cursor.bold = s.cursor.italic = False
s.cursor.fg = s.cursor.bg = 0
s.draw('')
if s.cursor.x > s.columns - max_title_length and not t.is_last:
s.draw('')
break
s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab
sprites = get_boss().sprites
s.update_cell_data(sprites.backend, addressof(self.render_buf), True)
self.cell_ranges = cr
self.dirty = True
glfw_post_empty_event()
def schedule_layout(self, data):
' Must be called in the GUI thread '
queue_action(self.layout, *data)
def schedule_update(self, data):
' Must be called in the GUI thread '
self.queue.put(data)
queue_action(self.update)
def render(self, cell_program, sprites):
' Must be called in the GUI thread '
if self.render_buf is not None:
sprites.render_dirty_cells()
if self.vao_id is None:
self.vao_id = cell_program.create_sprite_map()
if self.dirty:
cell_program.send_vertex_data(self.vao_id, self.render_buf)
self.dirty = False
render_cells(self.vao_id, self.screen_geometry, cell_program, sprites, self.screen.color_profile)
def tab_at(self, x):
' Must be called in the GUI thread '
x = (x - self.window_geometry.left) // self.cell_width
for i, (a, b) in enumerate(self.cell_ranges):
if a <= x <= b:
return i
class TabManager:
def __init__(self, opts, args, startup_session):
self.opts, self.args = opts, args
self.tabs = [Tab(opts, args, self.title_changed, t) for t in startup_session.tabs]
self.active_tab_idx = startup_session.active_tab_idx
self.tab_bar = TabBar(self.tab_bar_data, opts)
self.tab_bar.schedule_layout(self.tab_bar_layout_data)
def update_tab_bar(self):
if len(self.tabs) > 1:
self.layout_tab_bar()
self.tab_bar.schedule_update(self.tab_bar_data)
def resize(self, only_tabs=False):
if not only_tabs:
self.layout_tab_bar()
self.tab_bar.schedule_layout(self.tab_bar_layout_data)
for tab in self.tabs:
tab.relayout()
def layout_tab_bar(self):
self.can_render = False
ncells = viewport_size.width // cell_size.width
s = Screen(None, 1, ncells)
s.reset_mode(DECAWM)
s.color_profile.update_ansi_color_table(self.color_table)
s.color_profile.set_configured_colors(self.default_fg, self.default_bg)
s.color_profile.dirty = True
self.sprite_map_type = (GLuint * (s.lines * s.columns * DATA_CELL_SIZE))
with self.tabbar_lock:
self.sprite_map = self.sprite_map_type()
self.tab_bar_screen = s
self.tabbar_dirty = True
margin = (viewport_size.width - ncells * cell_size.width) // 2
self.window_geometry = g = WindowGeometry(
margin, viewport_size.height - cell_size.height, viewport_size.width - margin, viewport_size.height, s.columns, s.lines)
if margin > 0:
self.tab_bar_blank_rects = (Rect(0, g.top, g.left, g.bottom), Rect(g.right - 1, g.top, viewport_size.width, g.bottom))
else:
self.tab_bar_blank_rects = ()
self.screen_geometry = calculate_gl_geometry(g)
self.screen = s
self.can_render = True
def set_active_tab(self, idx):
self.active_tab_idx = idx
self.tabbar_dirty = True
self.active_tab.relayout_borders()
glfw_post_empty_event()
self.update_tab_bar()
def next_tab(self, delta=1):
if len(self.tabs) > 1:
@ -287,20 +364,18 @@ class TabManager:
nidx = (idx + len(self.tabs) + delta) % len(self.tabs)
self.tabs[idx], self.tabs[nidx] = self.tabs[nidx], self.tabs[idx]
self.active_tab_idx = nidx
glfw_post_empty_event()
self.update_tab_bar()
def title_changed(self, new_title):
with self.tabbar_lock:
self.tabbar_dirty = True
self.update_tab_bar()
def new_tab(self, special_window=None):
' Must be called in the GUI thread '
needs_resize = len(self.tabs) == 1
self.active_tab_idx = len(self.tabs)
self.tabs.append(Tab(self.opts, self.args, self.title_changed, special_window=special_window))
self.update_tab_bar()
if needs_resize:
if not self.can_render:
queue_action(self.layout_tab_bar)
queue_action(get_boss().tabbar_visibility_changed)
def remove(self, tab):
@ -308,62 +383,36 @@ class TabManager:
needs_resize = len(self.tabs) == 2
self.tabs.remove(tab)
self.active_tab_idx = max(0, min(self.active_tab_idx, len(self.tabs) - 1))
self.tabbar_dirty = True
self.update_tab_bar()
tab.destroy()
if needs_resize:
queue_action(get_boss().tabbar_visibility_changed)
def update_tab_bar_data(self, sprites, cell_program):
s = self.tab_bar_screen
s.cursor.x = 0
s.erase_in_line(2, False)
at = self.active_tab
max_title_length = (self.screen_geometry.xnum // len(self.tabs)) - 1
self.cell_ranges = []
@property
def tab_bar_layout_data(self):
' Must be called in the GUI thread '
return viewport_size.width, viewport_size.height, cell_size.width, cell_size.height
@property
def tab_bar_data(self):
at = self.active_tab
ans = []
for t in self.tabs:
title = (t.name or t.title or appname) + ' '
s.cursor.bg = self.active_bg if t is at else 0
s.cursor.fg = self.active_fg if t is at else 0
s.cursor.bold = s.cursor.italic = t is at
before = s.cursor.x
s.draw(title)
extra = s.cursor.x - before - max_title_length
if extra > 0:
s.cursor.x -= extra + 1
s.draw('')
self.cell_ranges.append((before, s.cursor.x))
s.cursor.bold = s.cursor.italic = False
s.cursor.fg = s.cursor.bg = 0
s.draw('')
if s.cursor.x > s.columns - max_title_length and t is not self.tabs[-1]:
s.draw('')
break
s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab
s.update_cell_data(
sprites.backend, addressof(self.sprite_map), True)
sprites.render_dirty_cells()
if self.vao_id is None:
self.vao_id = cell_program.create_sprite_map()
cell_program.send_vertex_data(self.vao_id, self.sprite_map)
ans.append(TabbarData(title, t is at, t is self.tabs[-1]))
return ans
def activate_tab_at(self, x):
x = (x - self.window_geometry.left) // cell_size.width
for i, (a, b) in enumerate(self.cell_ranges):
if a <= x <= b:
queue_action(self.set_active_tab, i)
return
i = self.tab_bar.tab_at(x)
if i is not None:
self.set_active_tab(i)
@property
def blank_rects(self):
if len(self.tabs) < 2:
return ()
return self.tab_bar_blank_rects
return self.tab_bar.blank_rects if len(self.tabs) < 2 else ()
def render(self, cell_program, sprites):
if not self.can_render or len(self.tabs) < 2:
' Must be called in the GUI thread '
if len(self.tabs) < 2:
return
with self.tabbar_lock:
if self.tabbar_dirty:
self.update_tab_bar_data(sprites, cell_program)
render_cells(self.vao_id, self.screen_geometry, cell_program, sprites, self.tab_bar_screen.color_profile)
self.tab_bar.render(cell_program, sprites)