From b1e39dac023223c9547a2126f8251da28ffd51b0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Nov 2016 13:38:13 +0530 Subject: [PATCH] Start work on rendering window borders --- kitty/borders.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++ kitty/config.py | 3 +- kitty/gl.h | 65 ++++++++++++++++++++++---- kitty/kitty.conf | 9 ++++ kitty/layout.py | 16 +++++-- kitty/shaders.py | 36 ++++++++++----- kitty/tabs.py | 12 +++-- 7 files changed, 228 insertions(+), 29 deletions(-) create mode 100644 kitty/borders.py diff --git a/kitty/borders.py b/kitty/borders.py new file mode 100644 index 000000000..e86b0eb36 --- /dev/null +++ b/kitty/borders.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +import ctypes + +from .constants import viewport_size +from .fast_data_types import glUniform3fv, GL_TRIANGLE_STRIP, glDrawArrays +from .layout import available_height +from .utils import get_dpi +from .shaders import ShaderProgram + + +def as_color(c): + return c[0] / 255, c[1] / 255, c[2] / 255 + + +def to_opengl(x, y): + return -1 + 2 * x / viewport_size.width, 1 - 2 * y / viewport_size.height + + +def as_rect(left, top, right, bottom, color=0): + for (x, y) in ((left, bottom), (right, bottom), (right, top), (left, top)): + x, y = to_opengl(x, y) + yield x + yield y + yield color + + +class BordersProgram(ShaderProgram): + + def __init__(self): + ShaderProgram.__init__(self, '''\ +uniform vec3 colors[3]; +in vec3 vertex; +out vec3 color; + +void main() { + gl_Position = vec4(vertex[0], vertex[1], 0, 1); + color = colors[uint(vertex[2])]; +} +''', '''\ +in vec3 color; +out vec4 final_color; + +void main() { + final_color = vec4(color, 1); +} + ''') + self.add_vertex_array('rects') + + def send_data(self, data): + self.send_vertex_data('rects', data) + + def set_colors(self, color_buf): + glUniform3fv(self.uniform_location('colors'), 3, ctypes.addressof(color_buf)) + + +class Borders: + + def __init__(self, opts): + self.is_dirty = False + self.can_render = False + dpix, dpiy = get_dpi()['logical'] + dpi = (dpix + dpiy) / 2 + self.border_width = round(opts.window_border_width * dpi / 72) + self.color_buf = (ctypes.c_float * 9)( + *as_color(opts.background), + *as_color(opts.active_border_color), + *as_color(opts.inactive_border_color) + ) + + def __call__(self, windows, active_window, draw_window_borders=True): + self.can_render = True + self.is_dirty = True + vw, vh = viewport_size.width, available_height() + if windows: + left_edge = min(w.geometry.left for w in windows) + right_edge = max(w.geometry.right for w in windows) + top_edge = min(w.geometry.top for w in windows) + bottom_edge = max(w.geometry.bottom for w in windows) + else: + left_edge = top_edge = 0 + right_edge = vw + bottom_edge = vh + rects = [] + if left_edge > 0: + rects.extend(as_rect(0, 0, left_edge, vh)) + if top_edge > 0: + rects.extend(as_rect(0, 0, vw, top_edge)) + if right_edge < vw: + rects.extend(as_rect(right_edge, 0, vw, vh)) + if bottom_edge < vh: + rects.extend(as_rect(0, bottom_edge, vw, vh)) + if draw_window_borders and self.border_width > 0: + bw = self.border_width + for w in windows: + g = w.geometry + color = 1 if w is active_window else 2 + rects.extend(as_rect(g.left - bw, g.top - bw, g.left, g.bottom + bw, color)) + rects.extend(as_rect(g.left - bw, g.top - bw, g.right + bw, g.top, color)) + rects.extend(as_rect(g.right, g.top - bw, g.right + bw, g.bottom + bw, color)) + rects.extend(as_rect(g.left - bw, g.bottom, g.right + bw, g.bottom + bw, color)) + self.data = (ctypes.c_float * len(rects))() + for i, x in enumerate(rects): + self.data[i] = x + + def render(self, program): + if not self.can_render: + return + with program: + if self.is_dirty: + program.send_data(self.data) + program.set_colors(self.color_buf) + self.is_dirty = False + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) diff --git a/kitty/config.py b/kitty/config.py index 8a22f60a8..6901024f6 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -44,9 +44,10 @@ type_map = { 'cursor_blink': to_bool, 'cursor_opacity': to_opacity, 'repaint_delay': int, + 'window_border_width': float, } -for name in 'foreground background cursor'.split(): +for name in 'foreground background cursor active_border_color inactive_border_color'.split(): type_map[name] = lambda x: to_color(x, validate=True) for i in range(16): type_map['color%d' % i] = lambda x: to_color(x, validate=True) diff --git a/kitty/gl.h b/kitty/gl.h index 2c2913824..22efe0662 100644 --- a/kitty/gl.h +++ b/kitty/gl.h @@ -101,6 +101,17 @@ Uniform4f(PyObject UNUSED *self, PyObject *args) { CHECK_ERROR; Py_RETURN_NONE; } + +static PyObject* +Uniform3fv(PyObject UNUSED *self, PyObject *args) { + int location; + unsigned int count; + PyObject *l; + if (!PyArg_ParseTuple(args, "iIO!", &location, &count, &PyLong_Type, &l)) return NULL; + glUniform3fv(location, count, PyLong_AsVoidPtr(l)); + CHECK_ERROR; + Py_RETURN_NONE; +} // }}} @@ -152,6 +163,17 @@ DrawArrays(PyObject UNUSED *self, PyObject *args) { Py_RETURN_NONE; } +static PyObject* +MultiDrawArrays(PyObject UNUSED *self, PyObject *args) { + int mode; + unsigned int draw_count; + PyObject *a, *b; + if (!PyArg_ParseTuple(args, "iO!O!I", &mode, &PyLong_Type, &a, &PyLong_Type, &b, &draw_count)) return NULL; + glMultiDrawArrays(mode, PyLong_AsVoidPtr(a), PyLong_AsVoidPtr(b), draw_count); + CHECK_ERROR; + Py_RETURN_NONE; +} + static PyObject* DrawArraysInstanced(PyObject UNUSED *self, PyObject *args) { int mode, first; @@ -431,14 +453,14 @@ TexSubImage3D(PyObject UNUSED *self, PyObject *args) { } static PyObject* -BufferData(PyObject UNUSED *self, PyObject *args) { - int target, usage; - unsigned long size; +NamedBufferData(PyObject UNUSED *self, PyObject *args) { + int usage; + unsigned long size, target; PyObject *address; - if (!PyArg_ParseTuple(args, "ikO!i", &target, &size, &PyLong_Type, &address, &usage)) return NULL; + if (!PyArg_ParseTuple(args, "kkO!i", &target, &size, &PyLong_Type, &address, &usage)) return NULL; void *data = PyLong_AsVoidPtr(address); if (data == NULL) { PyErr_SetString(PyExc_TypeError, "Not a valid data pointer"); return NULL; } - glBufferData(target, size, data, usage); + glNamedBufferData(target, size, data, usage); CHECK_ERROR; Py_RETURN_NONE; } @@ -520,13 +542,34 @@ Disable(PyObject UNUSED *self, PyObject *val) { Py_RETURN_NONE; } +static PyObject* +EnableVertexAttribArray(PyObject UNUSED *self, PyObject *val) { + long x = PyLong_AsLong(val); + glEnableVertexAttribArray(x); + CHECK_ERROR; + Py_RETURN_NONE; +} + +static PyObject* +VertexAttribPointer(PyObject UNUSED *self, PyObject *args) { + unsigned int index, stride; + int type=GL_FLOAT, normalized, size; + void *offset; + PyObject *l; + if (!PyArg_ParseTuple(args, "IiipIO!", &index, &size, &type, &normalized, &stride, &PyLong_Type, &l)) return NULL; + offset = PyLong_AsVoidPtr(l); + glVertexAttribPointer(index, size, type, normalized, stride, offset); + CHECK_ERROR; + Py_RETURN_NONE; +} + int add_module_gl_constants(PyObject *module) { #define GLC(x) if (PyModule_AddIntConstant(module, #x, x) != 0) { PyErr_NoMemory(); return 0; } GLC(GL_VERSION); GLC(GL_VENDOR); GLC(GL_SHADING_LANGUAGE_VERSION); GLC(GL_RENDERER); - GLC(GL_TRIANGLE_FAN); + GLC(GL_TRIANGLE_FAN); GLC(GL_TRIANGLE_STRIP); GLC(GL_COLOR_BUFFER_BIT); GLC(GL_VERTEX_SHADER); GLC(GL_FRAGMENT_SHADER); @@ -543,9 +586,9 @@ int add_module_gl_constants(PyObject *module) { GLC(GL_TEXTURE_WRAP_S); GLC(GL_TEXTURE_WRAP_T); GLC(GL_UNPACK_ALIGNMENT); GLC(GL_R8); GLC(GL_RED); GLC(GL_UNSIGNED_BYTE); GLC(GL_RGB32UI); - GLC(GL_TEXTURE_BUFFER); GLC(GL_STATIC_DRAW); + GLC(GL_TEXTURE_BUFFER); GLC(GL_STATIC_DRAW); GLC(GL_STREAM_DRAW); GLC(GL_SRC_ALPHA); GLC(GL_ONE_MINUS_SRC_ALPHA); - GLC(GL_BLEND); + GLC(GL_BLEND); GLC(GL_FLOAT); GLC(GL_ARRAY_BUFFER); return 1; } @@ -562,6 +605,7 @@ int add_module_gl_constants(PyObject *module) { METH(Uniform1i, METH_VARARGS) \ METH(Uniform2f, METH_VARARGS) \ METH(Uniform4f, METH_VARARGS) \ + METH(Uniform3fv, METH_VARARGS) \ METH(GetUniformLocation, METH_VARARGS) \ METH(GetAttribLocation, METH_VARARGS) \ METH(ShaderSource, METH_VARARGS) \ @@ -582,11 +626,14 @@ int add_module_gl_constants(PyObject *module) { METH(DeleteShader, METH_O) \ METH(Enable, METH_O) \ METH(Disable, METH_O) \ + METH(EnableVertexAttribArray, METH_O) \ + METH(VertexAttribPointer, METH_VARARGS) \ METH(GetProgramInfoLog, METH_O) \ METH(GetShaderInfoLog, METH_O) \ METH(ActiveTexture, METH_O) \ METH(DrawArraysInstanced, METH_VARARGS) \ METH(DrawArrays, METH_VARARGS) \ + METH(MultiDrawArrays, METH_VARARGS) \ METH(CreateProgram, METH_NOARGS) \ METH(AttachShader, METH_VARARGS) \ METH(BindTexture, METH_VARARGS) \ @@ -597,6 +644,6 @@ int add_module_gl_constants(PyObject *module) { METH(TexStorage3D, METH_VARARGS) \ METH(CopyImageSubData, METH_VARARGS) \ METH(TexSubImage3D, METH_VARARGS) \ - METH(BufferData, METH_VARARGS) \ + METH(NamedBufferData, METH_VARARGS) \ METH(BlendFunc, METH_VARARGS) \ diff --git a/kitty/kitty.conf b/kitty/kitty.conf index bfb68b649..e7eb99826 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -31,6 +31,15 @@ scrollback_lines 2000 # that sufficient for most uses. repaint_delay 20 +# The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution. +window_border_width 2 + +# The color for the border of the active window +active_border_color #00ff00 + +# The color for the border of inactive windows +inactive_border_color #cccccc + # black color0 #000000 color8 #4d4d4d diff --git a/kitty/layout.py b/kitty/layout.py index 7e87e79a1..0ae8cfab6 100644 --- a/kitty/layout.py +++ b/kitty/layout.py @@ -2,7 +2,11 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -from .constants import WindowGeometry, viewport_size, cell_size +from .constants import WindowGeometry, viewport_size, cell_size, tab_manager + + +def available_height(): + return viewport_size.height - tab_manager().current_tab_bar_height def layout_dimension(length, cell_length, number_of_windows=1, border_length=0): @@ -25,6 +29,11 @@ def layout_dimension(length, cell_length, number_of_windows=1, border_length=0): class Layout: name = None + needs_window_borders = True + + def __init__(self, opts, border_width): + self.opts = opts + self.border_width = border_width def add_window(self, windows, window): raise NotImplementedError() @@ -36,9 +45,10 @@ class Layout: raise NotImplementedError() -class Stack: +class Stack(Layout): name = 'stack' + needs_window_borders = False def add_window(self, windows, window): windows.appendleft(window) @@ -50,7 +60,7 @@ class Stack: def __call__(self, windows): xstart, xnum = next(layout_dimension(viewport_size.width, cell_size.width)) - ystart, ynum = next(layout_dimension(viewport_size.height, cell_size.height)) + ystart, ynum = next(layout_dimension(available_height(), cell_size.height)) wg = WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_size.width * xnum, bottom=ystart + cell_size.height * ynum) for i, w in enumerate(windows): w.is_visible_in_layout = i == 0 diff --git a/kitty/shaders.py b/kitty/shaders.py index b5a352a74..ae7861386 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -14,14 +14,16 @@ from .fast_data_types import ( glCreateShader, glShaderSource, glCompileShader, glGetShaderiv, GL_COMPILE_STATUS, glGetShaderInfoLog, glGetUniformLocation, glGetAttribLocation, glUseProgram, glBindVertexArray, GL_TEXTURE0, - GL_TEXTURE1, glGetIntegerv, GL_MAX_ARRAY_TEXTURE_LAYERS, glBufferData, + GL_TEXTURE1, glGetIntegerv, GL_MAX_ARRAY_TEXTURE_LAYERS, glNamedBufferData, GL_MAX_TEXTURE_SIZE, glDeleteTexture, GL_TEXTURE_2D_ARRAY, glGenTextures, glBindTexture, glTexParameteri, GL_CLAMP_TO_EDGE, glDeleteBuffer, GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER, GL_TEXTURE_WRAP_S, GL_NEAREST, GL_TEXTURE_WRAP_T, glGenBuffers, GL_R8, GL_RED, - GL_UNPACK_ALIGNMENT, GL_UNSIGNED_BYTE, GL_STATIC_DRAW, GL_TEXTURE_BUFFER, - GL_RGB32UI, glBindBuffer, glPixelStorei, glTexBuffer, glActiveTexture, - glTexStorage3D, glCopyImageSubData, glTexSubImage3D, ITALIC, BOLD, SpriteMap + GL_UNPACK_ALIGNMENT, GL_UNSIGNED_BYTE, GL_STATIC_DRAW, GL_STREAM_DRAW, + GL_TEXTURE_BUFFER, GL_RGB32UI, GL_FLOAT, GL_ARRAY_BUFFER, glBindBuffer, + glPixelStorei, glTexBuffer, glActiveTexture, glTexStorage3D, + glCopyImageSubData, glTexSubImage3D, ITALIC, BOLD, SpriteMap, + glEnableVertexAttribArray, glVertexAttribPointer ) GL_VERSION = (3, 3) @@ -143,11 +145,8 @@ class Sprites: self.buffer_texture_id = glGenTextures(1) self.buffer_texture_unit = GL_TEXTURE1 - def set_sprite_map(self, data): - tgt = GL_TEXTURE_BUFFER - glBindBuffer(tgt, self.buffer_id) - glBufferData(tgt, sizeof(data), addressof(data), GL_STATIC_DRAW) - glBindBuffer(tgt, 0) + def set_sprite_map(self, data, usage=GL_STREAM_DRAW): + glNamedBufferData(self.buffer_id, sizeof(data), addressof(data), usage) def __enter__(self): self.ensure_state() @@ -175,7 +174,6 @@ class ShaderProgram: """ self.program_id = glCreateProgram() - self.is_active = False vs_id = self.add_shader(vertex, GL_VERTEX_SHADER) glAttachShader(self.program_id, vs_id) @@ -192,6 +190,22 @@ class ShaderProgram: glDeleteShader(vs_id) glDeleteShader(frag_id) self.vao_id = glGenVertexArrays(1) + self.buffers = {} + + def add_vertex_array(self, name, size=3, dtype=GL_FLOAT, normalized=False, stride=0, offset=0): + glBindVertexArray(self.vao_id) + if name not in self.buffers: + self.buffers[name] = glGenBuffers(1) + glBindBuffer(GL_ARRAY_BUFFER, self.buffers[name]) + aid = self.attribute_location(name) + glEnableVertexAttribArray(aid) + glVertexAttribPointer(aid, size, dtype, normalized, stride, offset) + glBindBuffer(GL_ARRAY_BUFFER, 0) + glBindVertexArray(0) + + def send_vertex_data(self, name, data, usage=GL_STATIC_DRAW): + bufid = self.buffers[name] + glNamedBufferData(bufid, sizeof(data), addressof(data), usage) def __hash__(self) -> int: return self.program_id @@ -230,9 +244,7 @@ class ShaderProgram: def __enter__(self): glUseProgram(self.program_id) glBindVertexArray(self.vao_id) - self.is_active = True def __exit__(self, *args): glUseProgram(0) glBindVertexArray(0) - self.is_active = False diff --git a/kitty/tabs.py b/kitty/tabs.py index df72d6f27..3450c5472 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -20,6 +20,7 @@ from .child import Child from .constants import viewport_size, shell_path, appname, set_tab_manager, tab_manager, wakeup, cell_size from .fast_data_types import glViewport, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, glClearColor, glClear, GL_COLOR_BUFFER_BIT from .fonts import set_font_family +from .borders import Borders, BordersProgram from .char_grid import cursor_shader, cell_shader from .keys import interpret_text_event, interpret_key_event, get_shortcut from .layout import Stack @@ -36,7 +37,8 @@ class Tab: def __init__(self, opts, args): self.opts, self.args = opts, args self.windows = deque() - self.current_layout = Stack() + self.borders = Borders(opts) + self.current_layout = Stack(opts, self.borders.border_width) @property def is_visible(self): @@ -58,6 +60,7 @@ class Tab: def relayout(self): if self.windows: self.current_layout(self.windows) + self.borders(self.windows, self.active_window, self.current_layout.needs_window_borders) def launch_child(self, use_shell=False): if use_shell: @@ -92,8 +95,7 @@ class Tab: del self.windows def render(self): - # TODO: Render window borders and clear the extra pixels - pass + self.borders.render(tab_manager().borders_program) class TabManager(Thread): @@ -103,6 +105,7 @@ class TabManager(Thread): def __init__(self, glfw_window, opts, args): Thread.__init__(self, name='ChildMonitor') self.glfw_window_title = None + self.current_tab_bar_height = 0 self.action_queue = Queue() self.pending_resize = None self.resize_gl_viewport = False @@ -128,12 +131,13 @@ class TabManager(Thread): self.sprites = Sprites() self.cell_program = ShaderProgram(*cell_shader) self.cursor_program = ShaderProgram(*cursor_shader) + self.borders_program = BordersProgram() glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) self.sprites.do_layout(cell_size.width, cell_size.height) + self.queue_action(self.active_tab.new_window, False) bg = opts.background glClearColor(bg.red / 255, bg.green / 255, bg.blue / 255, 1) glClear(GL_COLOR_BUFFER_BIT) - self.queue_action(self.active_tab.new_window, False) def signal_received(self): try: