Implement painting of cells

This commit is contained in:
Kovid Goyal 2016-10-15 14:26:53 +05:30
parent 59f92f9db8
commit 0336873afc
5 changed files with 186 additions and 9 deletions

25
kitty/boss.py Normal file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from collections import deque
from PyQt5.QtCore import QObject
from .term import TerminalWidget
class Boss(QObject):
def __init__(self, opts, parent=None):
self.linebuf = deque(maxlen=max(1000, opts.scrollback_lines))
self.term = TerminalWidget(opts, self.linebuf, parent)
self.term.relayout_lines.connect(self.relayout_lines)
def apply_opts(self, opts):
if opts.scrollback_lines != self.linebuf.maxlen:
self.linebuf, old = deque(maxlen=max(1000, opts.scrollback_lines)), self.linebuf
self.linebuf.extend(old)
self.term.apply_opts(opts)
def relayout_lines(self):
pass

View File

@ -4,13 +4,33 @@
import re import re
from collections import namedtuple from collections import namedtuple
from typing import Tuple
from PyQt5.QtGui import QFont, QFontInfo from PyQt5.QtGui import QFont, QFontInfo, QColor
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$') key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
defaults = {} defaults = {}
def to_qcolor(x):
ans = QColor(x)
if not ans.isValid():
raise ValueError('{} is not a valid color'.format(x))
return ans
def to_font_size(x):
return max(6, float(x))
type_map = {
'scrollback_lines': int,
'font_size': to_font_size,
}
for name in 'foreground foreground_bold background cursor'.split():
type_map[name] = to_qcolor
for line in ''' for line in '''
term xterm-termite term xterm-termite
foreground #dddddd foreground #dddddd
@ -18,7 +38,8 @@ foreground_bold #ffffff
cursor #dddddd cursor #dddddd
background #000000 background #000000
font_family monospace font_family monospace
font_size system font_size 10.0
scrollback_lines 10000
# black # black
color0 #000000 color0 #000000
@ -58,12 +79,15 @@ color15 #ffffff
m = key_pat.match(line) m = key_pat.match(line)
if m is not None: if m is not None:
key, val = m.groups() key, val = m.groups()
tm = type_map.get(key)
if tm is not None:
val = tm(val)
defaults[key] = val defaults[key] = val
Options = namedtuple('Defaults', ','.join(defaults.keys())) Options = namedtuple('Defaults', ','.join(defaults.keys()))
defaults = Options(**defaults) defaults = Options(**defaults)
def load_config(path): def load_config(path: str) -> Options:
if not path: if not path:
return defaults return defaults
ans = defaults._asdict() ans = defaults._asdict()
@ -80,10 +104,24 @@ def load_config(path):
if m is not None: if m is not None:
key, val = m.groups() key, val = m.groups()
if key in ans: if key in ans:
tm = type_map.get(key)
if tm is not None:
val = tm(val)
ans[key] = val ans[key] = val
return Options(**ans) return Options(**ans)
def validate_font(opts): def validate_font(opts: Options):
if not QFontInfo(QFont(opts.font_family)).fixedPitch(): if not QFontInfo(QFont(opts.font_family)).fixedPitch():
raise ValueError('The font specified in the configuration "{}" is not a monospace font'.format(opts.font_family)) raise ValueError('The font specified in the configuration "{}" is not a monospace font'.format(opts.font_family))
def build_ansi_color_tables(opts: Options) -> Tuple[dict, dict]:
fg = {30 + i: getattr(opts, 'color{}'.format(i)) for i in range(8)}
fg[39] = opts.foreground
fg.update({90 + i: getattr(opts, 'color{}'.format(i + 8)) for i in range(8)})
fg[99] = opts.foreground_bold
bg = {40 + i: getattr(opts, 'color{}'.format(i)) for i in range(8)}
bg[49] = opts.background
bg.update({100 + i: getattr(opts, 'color{}'.format(i + 8)) for i in range(8)})
return fg, bg

View File

@ -2,7 +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 typing import Tuple, Dict, Union
from numpy import zeros, dtype from numpy import zeros, dtype
from PyQt5.QtGui import QColor
color_type = dtype([('type', 'u1'), ('r', 'u1'), ('g', 'u1'), ('b', 'u1')]) color_type = dtype([('type', 'u1'), ('r', 'u1'), ('g', 'u1'), ('b', 'u1')])
@ -22,3 +25,11 @@ class Line:
self.strikethrough = zeros(sz, bool) self.strikethrough = zeros(sz, bool)
self.decoration = zeros(sz, 'u1') self.decoration = zeros(sz, 'u1')
self.decoration_fg = zeros(sz, color_type) self.decoration_fg = zeros(sz, color_type)
def as_color(entry: Tuple[int, int, int, int], color_table: Dict[int, QColor]) -> Union[QColor, None]:
t, r, g, b = entry
if t == 1:
return color_table.get(r)
if t == 2:
return QColor(r, g, b)

View File

@ -14,7 +14,7 @@ from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
from .config import load_config, validate_font from .config import load_config, validate_font
from .constants import appname, str_version, config_dir from .constants import appname, str_version, config_dir
from .term import TerminalWidget from .boss import Boss
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
@ -24,8 +24,8 @@ class MainWindow(QMainWindow):
self.setWindowTitle(appname) self.setWindowTitle(appname)
sys.excepthook = self.on_unhandled_error sys.excepthook = self.on_unhandled_error
self.handle_unix_signals() self.handle_unix_signals()
self.terminal = TerminalWidget(opts, self) self.boss = Boss(opts, self)
self.setCentralWidget(self.terminal) self.setCentralWidget(self.boss.term)
def on_unhandled_error(self, etype, value, tb): def on_unhandled_error(self, etype, value, tb):
if etype == KeyboardInterrupt: if etype == KeyboardInterrupt:
@ -35,7 +35,6 @@ class MainWindow(QMainWindow):
msg = str(value) msg = str(value)
except Exception: except Exception:
msg = repr(value) msg = repr(value)
msg = '<p>' + msg + '<br>' + _('Click "Show details" for more information')
QMessageBox.critical(self, _('Unhandled exception'), msg) QMessageBox.critical(self, _('Unhandled exception'), msg)
def handle_unix_signals(self): def handle_unix_signals(self):

View File

@ -2,10 +2,114 @@
# 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 typing import Tuple, Iterator, Union
from PyQt5.QtCore import pyqtSignal, QTimer, Qt, QRect
from PyQt5.QtGui import QColor, QPainter, QFont, QFontMetrics, QRegion, QPen
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
from .config import build_ansi_color_tables
from .data_types import Line, as_color
def ascii_width(fm: QFontMetrics) -> int:
ans = 0
for i in range(32, 128):
ans = max(ans, fm.widthChar(chr(i)))
return ans
class TerminalWidget(QWidget): class TerminalWidget(QWidget):
def __init__(self, opts, parent=None): relayout_lines = pyqtSignal(object, object)
cells_per_line = 80
def __init__(self, opts, linebuf, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.linebuf = linebuf
self.setAutoFillBackground(True)
self.apply_opts(opts)
self.debounce_resize_timer = t = QTimer(self)
t.setSingleShot(True)
t.setInterval(50)
t.timeout.connect(self.do_layout)
def apply_opts(self, opts):
self.opts = opts
pal = self.palette()
pal.setColor(pal.Window, QColor(opts.background))
pal.setColor(pal.WindowText, QColor(opts.foreground))
self.setPalette(pal)
self.current_bg = pal.color(pal.Window)
self.current_fg = pal.color(pal.WindowText)
self.ansi_fg, self.ansi_bg = build_ansi_color_tables(opts)
f = QFont(opts.font_family)
f.setPointSizeF(opts.font_size)
self.setFont(f)
self.font_metrics = fm = QFontMetrics(self.font())
self.cell_height = fm.lineSpacing()
self.cell_width = ascii_width(fm)
self.do_layout()
def do_layout(self):
previous, self.cells_per_line = self.cells_per_line, self.width() // self.cell_width
if previous != self.cells_per_line:
self.relayout_lines.emit(previous, self.cells_per_line)
self.lines_per_screen = self.height() // self.cell_height
self.hmargin = (self.width() - self.cells_per_line * self.cell_width) // 2
self.vmargin = (self.height() % self.cell_height) // 2
self.line_positions = tuple(self.vmargin + i * self.cell_height for i in range(self.lines_per_screen))
self.cell_positions = tuple(self.hmargin + i * self.cell_width for i in range(self.cells_per_line))
self.layout_size = self.size()
self.update()
def resizeEvent(self, ev):
self.debounce_resize_timer.start()
def dirty_lines(self, region: QRegion) -> Iterator[Tuple[int, QRegion]]:
w = self.width() - 2 * self.hmargin
for i, y in enumerate(self.line_positions):
ir = region.intersected(QRect(self.hmargin, y, w, self.cell_height))
if not ir.isEmpty():
yield i, ir
def dirty_cells(self, y: int, line_region: QRegion) -> Iterator[int]:
for i, x in enumerate(self.cell_positions):
if line_region.intersects(QRect(x, y, self.cell_width, self.cell_height)):
yield i
def line(self, screen_line: int) -> Union[Line, None]:
try:
lpos = len(self.linebuf) - self.lines_per_screen
return self.linebuf[lpos]
except IndexError:
pass
def paintEvent(self, ev):
if self.size() != self.layout_size:
return
r = ev.region()
p = QPainter(self)
for lnum, line_region in self.dirty_lines(r):
line = self.line(lnum)
if line is not None:
ypos = self.line_positions[lnum]
for cnum in self.dirty_cells(ypos, line_region):
p.save()
self.paint_cell(p, line, cnum, ypos)
p.restore()
def paint_cell(self, painter: QPainter, line: Line, col: int, y: int) -> None:
char = line.char[col]
if not char:
return
x = self.cell_positions[col]
r = QRect(x, y, self.cell_width, self.cell_height)
t, r, g, b = line.fg[col]
fg = as_color(line.fg[col], self.ansi_fg)
if fg is not None:
painter.setPen(QPen(fg))
bg = as_color(line.bg[col], self.ansi_bg)
if bg is not None:
painter.fillRect(r, bg)
painter.drawText(r, Qt.AlignHCenter | Qt.AlignBaseline | Qt.TextSingleLine, char)