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): def calculate_gl_geometry(window_geometry, viewport_width, viewport_height, cell_width, cell_height):
dx, dy = 2 * cell_size.width / viewport_size.width, 2 * cell_size.height / viewport_size.height dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height
xmargin = window_geometry.left / viewport_size.width xmargin = window_geometry.left / viewport_width
ymargin = window_geometry.top / viewport_size.height ymargin = window_geometry.top / viewport_height
xstart = -1 + 2 * xmargin xstart = -1 + 2 * xmargin
ystart = 1 - 2 * ymargin ystart = 1 - 2 * ymargin
return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy) return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy)
@ -173,7 +173,7 @@ class CharGrid:
self.vao_id = None self.vao_id = None
def update_position(self, window_geometry): 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): def resize(self, window_geometry):
self.update_position(window_geometry) self.update_position(window_geometry)

View File

@ -2,10 +2,10 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> # 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 ctypes import addressof
from functools import partial from functools import partial
from threading import Lock from queue import Queue, Empty
from .borders import Borders from .borders import Borders
from .char_grid import calculate_gl_geometry, render_cells from .char_grid import calculate_gl_geometry, render_cells
@ -23,6 +23,9 @@ from .utils import color_as_int
from .window import Window from .window import Window
TabbarData = namedtuple('TabbarData', 'title is_active is_last')
def SpecialWindow(cmd, stdin=None, override_title=None): def SpecialWindow(cmd, stdin=None, override_title=None):
return (cmd, stdin, override_title) return (cmd, stdin, override_title)
@ -203,65 +206,139 @@ class Tab:
return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self))) return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self)))
class TabManager: class TabBar:
def __init__(self, opts, args, startup_session): def __init__(self, data, opts):
self.opts, self.args = opts, args self.num_tabs = 1
self.cell_width = 1
self.queue = Queue()
self.vao_id = None self.vao_id = None
self.tabbar_lock = Lock() self.render_buf = None
self.tabs = [Tab(opts, args, self.title_changed, t) for t in startup_session.tabs] self.dirty = True
self.color_table = build_ansi_color_table(self.opts) self.screen = s = Screen(None, 1, 10)
self.cell_ranges = [] s.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
self.active_tab_idx = startup_session.active_tab_idx s.color_profile.set_configured_colors(
self.tabbar_dirty = True color_as_int(opts.inactive_tab_foreground),
self.default_fg = color_as_int(opts.inactive_tab_foreground) color_as_int(opts.inactive_tab_background)
self.default_bg = color_as_int(opts.inactive_tab_background) )
self.tab_bar_blank_rects = () s.color_profile.dirty = True
self.blank_rects = ()
self.current_data = data
def as_rgb(x): def as_rgb(x):
return (x << 8) | 2 return (x << 8) | 2
self.active_bg = as_rgb(color_as_int(opts.active_tab_background)) 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.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: if len(self.tabs) > 1:
self.layout_tab_bar() self.tab_bar.schedule_update(self.tab_bar_data)
def resize(self, only_tabs=False): def resize(self, only_tabs=False):
if not only_tabs: if not only_tabs:
self.layout_tab_bar() self.tab_bar.schedule_layout(self.tab_bar_layout_data)
for tab in self.tabs: for tab in self.tabs:
tab.relayout() 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): def set_active_tab(self, idx):
self.active_tab_idx = idx self.active_tab_idx = idx
self.tabbar_dirty = True
self.active_tab.relayout_borders() self.active_tab.relayout_borders()
glfw_post_empty_event() self.update_tab_bar()
def next_tab(self, delta=1): def next_tab(self, delta=1):
if len(self.tabs) > 1: if len(self.tabs) > 1:
@ -287,20 +364,18 @@ class TabManager:
nidx = (idx + len(self.tabs) + delta) % len(self.tabs) nidx = (idx + len(self.tabs) + delta) % len(self.tabs)
self.tabs[idx], self.tabs[nidx] = self.tabs[nidx], self.tabs[idx] self.tabs[idx], self.tabs[nidx] = self.tabs[nidx], self.tabs[idx]
self.active_tab_idx = nidx self.active_tab_idx = nidx
glfw_post_empty_event() self.update_tab_bar()
def title_changed(self, new_title): def title_changed(self, new_title):
with self.tabbar_lock: self.update_tab_bar()
self.tabbar_dirty = True
def new_tab(self, special_window=None): def new_tab(self, special_window=None):
' Must be called in the GUI thread ' ' Must be called in the GUI thread '
needs_resize = len(self.tabs) == 1 needs_resize = len(self.tabs) == 1
self.active_tab_idx = len(self.tabs) self.active_tab_idx = len(self.tabs)
self.tabs.append(Tab(self.opts, self.args, self.title_changed, special_window=special_window)) self.tabs.append(Tab(self.opts, self.args, self.title_changed, special_window=special_window))
self.update_tab_bar()
if needs_resize: if needs_resize:
if not self.can_render:
queue_action(self.layout_tab_bar)
queue_action(get_boss().tabbar_visibility_changed) queue_action(get_boss().tabbar_visibility_changed)
def remove(self, tab): def remove(self, tab):
@ -308,62 +383,36 @@ class TabManager:
needs_resize = len(self.tabs) == 2 needs_resize = len(self.tabs) == 2
self.tabs.remove(tab) self.tabs.remove(tab)
self.active_tab_idx = max(0, min(self.active_tab_idx, len(self.tabs) - 1)) 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() tab.destroy()
if needs_resize: if needs_resize:
queue_action(get_boss().tabbar_visibility_changed) queue_action(get_boss().tabbar_visibility_changed)
def update_tab_bar_data(self, sprites, cell_program): @property
s = self.tab_bar_screen def tab_bar_layout_data(self):
s.cursor.x = 0 ' Must be called in the GUI thread '
s.erase_in_line(2, False) return viewport_size.width, viewport_size.height, cell_size.width, cell_size.height
at = self.active_tab
max_title_length = (self.screen_geometry.xnum // len(self.tabs)) - 1
self.cell_ranges = []
@property
def tab_bar_data(self):
at = self.active_tab
ans = []
for t in self.tabs: for t in self.tabs:
title = (t.name or t.title or appname) + ' ' title = (t.name or t.title or appname) + ' '
s.cursor.bg = self.active_bg if t is at else 0 ans.append(TabbarData(title, t is at, t is self.tabs[-1]))
s.cursor.fg = self.active_fg if t is at else 0 return ans
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)
def activate_tab_at(self, x): def activate_tab_at(self, x):
x = (x - self.window_geometry.left) // cell_size.width i = self.tab_bar.tab_at(x)
for i, (a, b) in enumerate(self.cell_ranges): if i is not None:
if a <= x <= b: self.set_active_tab(i)
queue_action(self.set_active_tab, i)
return
@property @property
def blank_rects(self): def blank_rects(self):
if len(self.tabs) < 2: return self.tab_bar.blank_rects if len(self.tabs) < 2 else ()
return ()
return self.tab_bar_blank_rects
def render(self, cell_program, sprites): 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 return
with self.tabbar_lock: self.tab_bar.render(cell_program, sprites)
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)