Move selection tracking into the Screen class
This commit is contained in:
parent
5caea75115
commit
408c719c5f
@ -63,35 +63,6 @@ def load_shader_programs():
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
class Selection: # {{{
|
|
||||||
|
|
||||||
__slots__ = tuple('in_progress start_x start_y start_scrolled_by end_x end_y end_scrolled_by'.split())
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.clear()
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.in_progress = False
|
|
||||||
self.start_x = self.start_y = self.end_x = self.end_y = 0
|
|
||||||
self.start_scrolled_by = self.end_scrolled_by = 0
|
|
||||||
|
|
||||||
def limits(self, scrolled_by, lines, columns):
|
|
||||||
|
|
||||||
def coord(x, y, ydelta):
|
|
||||||
y = y - ydelta + scrolled_by
|
|
||||||
if y < 0:
|
|
||||||
x, y = 0, 0
|
|
||||||
elif y >= lines:
|
|
||||||
x, y = columns - 1, lines - 1
|
|
||||||
return x, y
|
|
||||||
|
|
||||||
a = coord(self.start_x, self.start_y, self.start_scrolled_by)
|
|
||||||
b = coord(self.end_x, self.end_y, self.end_scrolled_by)
|
|
||||||
return (a, b) if a[1] < b[1] or (a[1] == b[1] and a[0] <= b[0]) else (b, a)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_gl_geometry(window_geometry, viewport_width, viewport_height, cell_width, cell_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
|
dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height
|
||||||
xmargin = window_geometry.left / viewport_width
|
xmargin = window_geometry.left / viewport_width
|
||||||
@ -124,7 +95,6 @@ class CharGrid:
|
|||||||
|
|
||||||
def __init__(self, screen, opts):
|
def __init__(self, screen, opts):
|
||||||
self.vao_id = None
|
self.vao_id = None
|
||||||
self.current_selection = Selection()
|
|
||||||
self.screen_reversed = False
|
self.screen_reversed = False
|
||||||
self.last_rendered_selection = None
|
self.last_rendered_selection = None
|
||||||
self.render_data = None
|
self.render_data = None
|
||||||
@ -152,7 +122,7 @@ class CharGrid:
|
|||||||
self.update_position(window_geometry)
|
self.update_position(window_geometry)
|
||||||
self.data_buffer_size = self.screen_geometry.ynum * self.screen_geometry.xnum * CELL['size']
|
self.data_buffer_size = self.screen_geometry.ynum * self.screen_geometry.xnum * CELL['size']
|
||||||
self.selection_buffer_size = self.screen_geometry.ynum * self.screen_geometry.xnum * sizeof(GLfloat)
|
self.selection_buffer_size = self.screen_geometry.ynum * self.screen_geometry.xnum * sizeof(GLfloat)
|
||||||
self.current_selection.clear()
|
self.screen.clear_selection()
|
||||||
|
|
||||||
def change_colors(self, changes):
|
def change_colors(self, changes):
|
||||||
dirtied = False
|
dirtied = False
|
||||||
@ -175,13 +145,9 @@ class CharGrid:
|
|||||||
def update_cell_data(self, cell_program):
|
def update_cell_data(self, cell_program):
|
||||||
if self.data_buffer_size is None:
|
if self.data_buffer_size is None:
|
||||||
return
|
return
|
||||||
clear_selection = self.screen.is_dirty
|
|
||||||
with cell_program.mapped_vertex_data(self.vao_id, self.data_buffer_size) as address:
|
with cell_program.mapped_vertex_data(self.vao_id, self.data_buffer_size) as address:
|
||||||
cursor_changed, self.screen_reversed = self.screen.update_cell_data(
|
cursor_changed, self.screen_reversed = self.screen.update_cell_data(
|
||||||
address, False)
|
address, False)
|
||||||
|
|
||||||
if clear_selection:
|
|
||||||
self.current_selection.clear()
|
|
||||||
self.render_data = self.screen_geometry
|
self.render_data = self.screen_geometry
|
||||||
if cursor_changed:
|
if cursor_changed:
|
||||||
c = self.screen.cursor
|
c = self.screen.cursor
|
||||||
@ -200,16 +166,11 @@ class CharGrid:
|
|||||||
y = 0 if my <= cell_size.height else self.screen.lines - 1
|
y = 0 if my <= cell_size.height else self.screen.lines - 1
|
||||||
ps = None
|
ps = None
|
||||||
if is_press:
|
if is_press:
|
||||||
self.current_selection.start_x = self.current_selection.end_x = x
|
self.screen.start_selection(x, y)
|
||||||
self.current_selection.start_y = self.current_selection.end_y = y
|
elif self.screen.is_selection_in_progress():
|
||||||
self.current_selection.start_scrolled_by = self.current_selection.end_scrolled_by = self.screen.scrolled_by
|
ended = is_press is False
|
||||||
self.current_selection.in_progress = True
|
self.screen.update_selection(x, y, ended)
|
||||||
elif self.current_selection.in_progress:
|
if ended:
|
||||||
self.current_selection.end_x = x
|
|
||||||
self.current_selection.end_y = y
|
|
||||||
self.current_selection.end_scrolled_by = self.screen.scrolled_by
|
|
||||||
if is_press is False:
|
|
||||||
self.current_selection.in_progress = False
|
|
||||||
ps = self.text_for_selection()
|
ps = self.text_for_selection()
|
||||||
if ps and ps.strip():
|
if ps and ps.strip():
|
||||||
set_primary_selection(ps)
|
set_primary_selection(ps)
|
||||||
@ -251,16 +212,14 @@ class CharGrid:
|
|||||||
if x is not None:
|
if x is not None:
|
||||||
line = self.screen.visual_line(y)
|
line = self.screen.visual_line(y)
|
||||||
if line is not None and count in (2, 3):
|
if line is not None and count in (2, 3):
|
||||||
s = self.current_selection
|
|
||||||
s.start_scrolled_by = s.end_scrolled_by = self.scrolled_by
|
|
||||||
s.start_y = s.end_y = y
|
|
||||||
s.in_progress = False
|
|
||||||
if count == 2:
|
if count == 2:
|
||||||
s.start_x, xlimit = self.screen.selection_range_for_word(x, y, self.opts.select_by_word_characters)
|
start_x, xlimit = self.screen.selection_range_for_word(x, y, self.opts.select_by_word_characters)
|
||||||
s.end_x = max(s.start_x, xlimit - 1)
|
end_x = max(start_x, xlimit - 1)
|
||||||
elif count == 3:
|
elif count == 3:
|
||||||
s.start_x, xlimit = self.screen.selection_range_for_line(y)
|
start_x, xlimit = self.screen.selection_range_for_line(y)
|
||||||
s.end_x = max(s.start_x, xlimit - 1)
|
end_x = max(start_x, xlimit - 1)
|
||||||
|
self.screen.start_selection(start_x, y)
|
||||||
|
self.screen.update_selection(end_x, y, True)
|
||||||
ps = self.text_for_selection()
|
ps = self.text_for_selection()
|
||||||
if ps:
|
if ps:
|
||||||
set_primary_selection(ps)
|
set_primary_selection(ps)
|
||||||
@ -271,10 +230,8 @@ class CharGrid:
|
|||||||
self.screen.linebuf.as_ansi(ans.append)
|
self.screen.linebuf.as_ansi(ans.append)
|
||||||
return ''.join(ans).encode('utf-8')
|
return ''.join(ans).encode('utf-8')
|
||||||
|
|
||||||
def text_for_selection(self, sel=None):
|
def text_for_selection(self):
|
||||||
s = sel or self.current_selection
|
return ''.join(self.screen.text_for_selection())
|
||||||
start, end = s.limits(self.screen.scrolled_by, self.screen.lines, self.screen.columns)
|
|
||||||
return ''.join(self.screen.text_for_selection(*start, *end))
|
|
||||||
|
|
||||||
def prepare_for_render(self, cell_program):
|
def prepare_for_render(self, cell_program):
|
||||||
if self.vao_id is None:
|
if self.vao_id is None:
|
||||||
@ -282,12 +239,11 @@ class CharGrid:
|
|||||||
if self.screen.scroll_changed or self.screen.is_dirty:
|
if self.screen.scroll_changed or self.screen.is_dirty:
|
||||||
self.update_cell_data(cell_program)
|
self.update_cell_data(cell_program)
|
||||||
sg = self.render_data
|
sg = self.render_data
|
||||||
start, end = self.current_selection.limits(self.screen.scrolled_by, self.screen.lines, self.screen.columns)
|
sel = self.screen.selection_limits()
|
||||||
sel = start, end, self.screen.scrolled_by
|
|
||||||
selection_changed = sel != self.last_rendered_selection
|
selection_changed = sel != self.last_rendered_selection
|
||||||
if selection_changed:
|
if selection_changed:
|
||||||
with cell_program.mapped_vertex_data(self.vao_id, self.selection_buffer_size, bufnum=1) as address:
|
with cell_program.mapped_vertex_data(self.vao_id, self.selection_buffer_size, bufnum=1) as address:
|
||||||
self.screen.apply_selection(address, self.selection_buffer_size, *start, *end)
|
self.screen.apply_selection(address, self.selection_buffer_size)
|
||||||
self.last_rendered_selection = sel
|
self.last_rendered_selection = sel
|
||||||
return sg
|
return sg
|
||||||
|
|
||||||
|
|||||||
@ -220,11 +220,17 @@ typedef struct {
|
|||||||
#define PARSER_BUF_SZ (8 * 1024)
|
#define PARSER_BUF_SZ (8 * 1024)
|
||||||
#define READ_BUF_SZ (1024*1024)
|
#define READ_BUF_SZ (1024*1024)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
unsigned int start_x, start_y, start_scrolled_by, end_x, end_y, end_scrolled_by;
|
||||||
|
bool in_progress;
|
||||||
|
} Selection;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
|
|
||||||
unsigned int columns, lines, margin_top, margin_bottom, charset, scrolled_by;
|
unsigned int columns, lines, margin_top, margin_bottom, charset, scrolled_by;
|
||||||
uint32_t utf8_state, utf8_codepoint, *g0_charset, *g1_charset, *g_charset;
|
uint32_t utf8_state, utf8_codepoint, *g0_charset, *g1_charset, *g_charset;
|
||||||
|
Selection selection;
|
||||||
bool use_latin1;
|
bool use_latin1;
|
||||||
Cursor *cursor;
|
Cursor *cursor;
|
||||||
SavepointBuffer main_savepoints, alt_savepoints;
|
SavepointBuffer main_savepoints, alt_savepoints;
|
||||||
|
|||||||
111
kitty/screen.c
111
kitty/screen.c
@ -21,6 +21,7 @@
|
|||||||
#include "wcwidth9.h"
|
#include "wcwidth9.h"
|
||||||
|
|
||||||
static const ScreenModes empty_modes = {0, .mDECAWM=true, .mDECTCEM=true, .mDECARM=true};
|
static const ScreenModes empty_modes = {0, .mDECAWM=true, .mDECTCEM=true, .mDECARM=true};
|
||||||
|
static Selection EMPTY_SELECTION = {0};
|
||||||
|
|
||||||
// Constructor/destructor {{{
|
// Constructor/destructor {{{
|
||||||
|
|
||||||
@ -1225,6 +1226,7 @@ update_cell_data(Screen *self, PyObject *args) {
|
|||||||
if (!PyArg_ParseTuple(args, "O!p", &PyLong_Type, &dp, &force_screen_refresh)) return NULL;
|
if (!PyArg_ParseTuple(args, "O!p", &PyLong_Type, &dp, &force_screen_refresh)) return NULL;
|
||||||
data = PyLong_AsVoidPtr(dp);
|
data = PyLong_AsVoidPtr(dp);
|
||||||
PyObject *cursor_changed = self->cursor_changed;
|
PyObject *cursor_changed = self->cursor_changed;
|
||||||
|
bool selection_must_be_cleared = self->is_dirty == Py_True ? true : false;
|
||||||
if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count);
|
if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count);
|
||||||
reset_dirty(self);
|
reset_dirty(self);
|
||||||
self->scroll_changed = Py_False;
|
self->scroll_changed = Py_False;
|
||||||
@ -1236,6 +1238,7 @@ update_cell_data(Screen *self, PyObject *args) {
|
|||||||
linebuf_init_line(self->linebuf, y - self->scrolled_by);
|
linebuf_init_line(self->linebuf, y - self->scrolled_by);
|
||||||
update_line_data(self->linebuf->line, y, data);
|
update_line_data(self->linebuf->line, y, data);
|
||||||
}
|
}
|
||||||
|
if (selection_must_be_cleared) self->selection = EMPTY_SELECTION;
|
||||||
return Py_BuildValue("OO", cursor_changed, self->modes.mDECSCNM ? Py_True : Py_False);
|
return Py_BuildValue("OO", cursor_changed, self->modes.mDECSCNM ? Py_True : Py_False);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1244,37 +1247,66 @@ is_selection_empty(Screen *self, unsigned int start_x, unsigned int start_y, uns
|
|||||||
return (start_x >= self->columns || start_y >= self->lines || end_x >= self->columns || end_y >= self->lines || (start_x == end_x && start_y == end_y)) ? true : false;
|
return (start_x >= self->columns || start_y >= self->lines || end_x >= self->columns || end_y >= self->lines || (start_x == end_x && start_y == end_y)) ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
unsigned int x, y;
|
||||||
|
} SelectionBoundary;
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
selection_coord(Screen *self, unsigned int x, unsigned int y, unsigned int ydelta, SelectionBoundary *ans) {
|
||||||
|
if (y + self->scrolled_by < ydelta) {
|
||||||
|
ans->x = 0; ans->y = 0;
|
||||||
|
} else {
|
||||||
|
y = y - ydelta + self->scrolled_by;
|
||||||
|
if (y >= self->lines) {
|
||||||
|
ans->x = self->columns - 1; ans->y = self->lines - 1;
|
||||||
|
} else {
|
||||||
|
ans->x = x; ans->y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
selection_limits_(Screen *self, SelectionBoundary *left, SelectionBoundary *right) {
|
||||||
|
SelectionBoundary a, b;
|
||||||
|
selection_coord(self, self->selection.start_x, self->selection.start_y, self->selection.start_scrolled_by, &a);
|
||||||
|
selection_coord(self, self->selection.end_x, self->selection.end_y, self->selection.end_scrolled_by, &b);
|
||||||
|
if (a.y < b.y || (a.y == b.y && a.x <= b.x)) { *left = a; *right = b; }
|
||||||
|
else { *left = b; *right = a; }
|
||||||
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
apply_selection(Screen *self, PyObject *args) {
|
apply_selection(Screen *self, PyObject *args) {
|
||||||
unsigned int size, start_x, end_x, start_y, end_y;
|
unsigned int size;
|
||||||
PyObject *l;
|
PyObject *l;
|
||||||
if (!PyArg_ParseTuple(args, "O!IIIII", &PyLong_Type, &l, &size, &start_x, &start_y, &end_x, &end_y)) return NULL;
|
SelectionBoundary start, end;
|
||||||
|
if (!PyArg_ParseTuple(args, "O!I", &PyLong_Type, &l, &size)) return NULL;
|
||||||
float *data = PyLong_AsVoidPtr(l);
|
float *data = PyLong_AsVoidPtr(l);
|
||||||
memset(data, 0, size);
|
memset(data, 0, size);
|
||||||
if (is_selection_empty(self, start_x, start_y, end_x, end_y)) { Py_RETURN_NONE; }
|
selection_limits_(self, &start, &end);
|
||||||
for (index_type y = start_y; y <= end_y; y++) {
|
if (is_selection_empty(self, start.x, start.y, end.x, end.y)) { Py_RETURN_NONE; }
|
||||||
|
for (index_type y = start.y; y <= end.y; y++) {
|
||||||
Line *line = visual_line_(self, y);
|
Line *line = visual_line_(self, y);
|
||||||
index_type xlimit = xlimit_for_line(line);
|
index_type xlimit = xlimit_for_line(line);
|
||||||
if (y == end_y) xlimit = MIN(end_x + 1, xlimit);
|
if (y == end.y) xlimit = MIN(end.x + 1, xlimit);
|
||||||
float *line_start = data + self->columns * y;
|
float *line_start = data + self->columns * y;
|
||||||
for (index_type x = (y == start_y ? start_x : 0); x < xlimit; x++) line_start[x] = 1.0;
|
for (index_type x = (y == start.y ? start.x : 0); x < xlimit; x++) line_start[x] = 1.0;
|
||||||
}
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
text_for_selection(Screen *self, PyObject *args) {
|
text_for_selection(Screen *self) {
|
||||||
unsigned int start_x, end_x, start_y, end_y;
|
SelectionBoundary start, end;
|
||||||
if (!PyArg_ParseTuple(args, "IIII", &start_x, &start_y, &end_x, &end_y)) return NULL;
|
selection_limits_(self, &start, &end);
|
||||||
if (is_selection_empty(self, start_x, start_y, end_x, end_y)) return PyTuple_New(0);
|
if (is_selection_empty(self, start.x, start.y, end.x, end.y)) return PyTuple_New(0);
|
||||||
Py_ssize_t i = 0, num_of_lines = end_y - start_y + 1;
|
Py_ssize_t i = 0, num_of_lines = end.y - start.y + 1;
|
||||||
PyObject *ans = PyTuple_New(num_of_lines);
|
PyObject *ans = PyTuple_New(num_of_lines);
|
||||||
if (ans == NULL) return PyErr_NoMemory();
|
if (ans == NULL) return PyErr_NoMemory();
|
||||||
for (index_type y = start_y; y <= end_y; y++, i++) {
|
for (index_type y = start.y; y <= end.y; y++, i++) {
|
||||||
Line *line = visual_line_(self, y);
|
Line *line = visual_line_(self, y);
|
||||||
index_type xlimit = xlimit_for_line(line);
|
index_type xlimit = xlimit_for_line(line);
|
||||||
if (y == end_y) xlimit = MIN(end_x + 1, xlimit);
|
if (y == end.y) xlimit = MIN(end.x + 1, xlimit);
|
||||||
index_type xstart = (y == start_y ? start_x : 0);
|
index_type xstart = (y == start.y ? start.x : 0);
|
||||||
char leading_char = i > 0 && !line->continued ? '\n' : 0;
|
char leading_char = i > 0 && !line->continued ? '\n' : 0;
|
||||||
PyObject *text = unicode_in_range(line, xstart, xlimit, true, leading_char);
|
PyObject *text = unicode_in_range(line, xstart, xlimit, true, leading_char);
|
||||||
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
|
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
|
||||||
@ -1357,8 +1389,48 @@ scroll(Screen *self, PyObject *args) {
|
|||||||
Py_RETURN_FALSE;
|
Py_RETURN_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static PyObject*
|
||||||
PyObject* mark_as_dirty(Screen *self) {
|
clear_selection(Screen *self) {
|
||||||
|
self->selection = EMPTY_SELECTION;
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
is_selection_in_progress(Screen *self) {
|
||||||
|
PyObject *ans = self->selection.in_progress ? Py_True : Py_False;
|
||||||
|
Py_INCREF(ans);
|
||||||
|
return ans;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
selection_limits(Screen *self) {
|
||||||
|
SelectionBoundary start, end;
|
||||||
|
selection_limits_(self, &start, &end);
|
||||||
|
return Py_BuildValue("(II)(II)", start.x, start.y, end.x, end.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
start_selection(Screen *self, PyObject *args) {
|
||||||
|
unsigned int x, y;
|
||||||
|
if (!PyArg_ParseTuple(args, "II", &x, &y)) return NULL;
|
||||||
|
#define A(attr, val) self->selection.attr = val;
|
||||||
|
A(start_x, x); A(end_x, x); A(start_y, y); A(end_y, y); A(start_scrolled_by, self->scrolled_by); A(end_scrolled_by, self->scrolled_by); A(in_progress, true);
|
||||||
|
#undef A
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
update_selection(Screen *self, PyObject *args) {
|
||||||
|
unsigned int x, y;
|
||||||
|
int ended;
|
||||||
|
if (!PyArg_ParseTuple(args, "IIp", &x, &y, &ended)) return NULL;
|
||||||
|
self->selection.end_x = x; self->selection.end_y = y; self->selection.end_scrolled_by = self->scrolled_by;
|
||||||
|
if (ended) self->selection.in_progress = false;
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject*
|
||||||
|
mark_as_dirty(Screen *self) {
|
||||||
self->is_dirty = Py_True;
|
self->is_dirty = Py_True;
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
@ -1444,7 +1516,12 @@ static PyMethodDef methods[] = {
|
|||||||
MND(apply_selection, METH_VARARGS)
|
MND(apply_selection, METH_VARARGS)
|
||||||
MND(selection_range_for_line, METH_VARARGS)
|
MND(selection_range_for_line, METH_VARARGS)
|
||||||
MND(selection_range_for_word, METH_VARARGS)
|
MND(selection_range_for_word, METH_VARARGS)
|
||||||
MND(text_for_selection, METH_VARARGS)
|
MND(text_for_selection, METH_NOARGS)
|
||||||
|
MND(clear_selection, METH_NOARGS)
|
||||||
|
MND(is_selection_in_progress, METH_NOARGS)
|
||||||
|
MND(selection_limits, METH_NOARGS)
|
||||||
|
MND(start_selection, METH_VARARGS)
|
||||||
|
MND(update_selection, METH_VARARGS)
|
||||||
MND(scroll, METH_VARARGS)
|
MND(scroll, METH_VARARGS)
|
||||||
MND(toggle_alt_screen, METH_NOARGS)
|
MND(toggle_alt_screen, METH_NOARGS)
|
||||||
MND(reset_callbacks, METH_NOARGS)
|
MND(reset_callbacks, METH_NOARGS)
|
||||||
|
|||||||
@ -243,7 +243,7 @@ class Window:
|
|||||||
if ev:
|
if ev:
|
||||||
self.write_to_child(ev)
|
self.write_to_child(ev)
|
||||||
else:
|
else:
|
||||||
if self.char_grid.current_selection.in_progress:
|
if self.screen.is_selection_in_progress():
|
||||||
self.char_grid.update_drag(None, x, y)
|
self.char_grid.update_drag(None, x, y)
|
||||||
margin = cell_size.height // 2
|
margin = cell_size.height // 2
|
||||||
if y <= margin or y >= self.geometry.bottom - margin:
|
if y <= margin or y >= self.geometry.bottom - margin:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user