From b436efc6c9ccad2305e9f586e4334e92ff4ec666 Mon Sep 17 00:00:00 2001 From: sasha80 Date: Fri, 7 Jul 2023 15:48:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D0=B2?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D0=B5.=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=82=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=B8=D1=86=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- table/header.tscn | 10 + table/nine-patch.png | Bin 0 -> 125 bytes table/nine-patch.png.import | 34 +++ table/node.tscn | 10 + table/selector.tscn | 15 + table/table.gd | 539 ++++++++++++++++++++++++++++++++++++ 6 files changed, 608 insertions(+) create mode 100644 table/header.tscn create mode 100644 table/nine-patch.png create mode 100644 table/nine-patch.png.import create mode 100644 table/node.tscn create mode 100644 table/selector.tscn create mode 100644 table/table.gd diff --git a/table/header.tscn b/table/header.tscn new file mode 100644 index 00000000..b65e7dcc --- /dev/null +++ b/table/header.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=2 format=3 uid="uid://ch14g46swx74h"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2thpi"] +bg_color = Color(0.8, 0.533333, 0.27451, 1) + +[node name="heder" type="Label"] +offset_right = 128.0 +offset_bottom = 23.0 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_styles/normal = SubResource("StyleBoxFlat_2thpi") diff --git a/table/nine-patch.png b/table/nine-patch.png new file mode 100644 index 0000000000000000000000000000000000000000..1649a7f27bffc6bd73e480ab6979e43be73e8fbe GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3E}kxqAs)xyUf(Flpupo4_~A}{ zYL4x1kI#`0T0=TRx>Pki_bu8MzWQUG`phHm&R&yZh6DclSMBY)q8>-_nZDq$u-UlN Q9%vqer>mdKI;Vst0JilfxBvhE literal 0 HcmV?d00001 diff --git a/table/nine-patch.png.import b/table/nine-patch.png.import new file mode 100644 index 00000000..2d8c80aa --- /dev/null +++ b/table/nine-patch.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bw3yv3smvaxqn" +path="res://.godot/imported/nine-patch.png-798f7950fb3126b4e1ed0c39d9e4beba.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://table/nine-patch.png" +dest_files=["res://.godot/imported/nine-patch.png-798f7950fb3126b4e1ed0c39d9e4beba.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/table/node.tscn b/table/node.tscn new file mode 100644 index 00000000..96c26f52 --- /dev/null +++ b/table/node.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=2 format=3 uid="uid://c1i4rwaajq7d5"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7jiwk"] + +[node name="Label" type="Label"] +offset_right = 121.0 +offset_bottom = 23.0 +theme_override_colors/font_color = Color(1, 0.596078, 0.4, 1) +theme_override_styles/normal = SubResource("StyleBoxFlat_7jiwk") +clip_text = true diff --git a/table/selector.tscn b/table/selector.tscn new file mode 100644 index 00000000..47a6c745 --- /dev/null +++ b/table/selector.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=2 format=3 uid="uid://pd3fcx55duj2"] + +[ext_resource type="Texture2D" uid="uid://bw3yv3smvaxqn" path="res://table/nine-patch.png" id="1_ik10p"] + +[node name="NinePatchRect" type="NinePatchRect"] +offset_right = 17.0 +offset_bottom = 16.0 +texture = ExtResource("1_ik10p") +draw_center = false +patch_margin_left = 8 +patch_margin_top = 8 +patch_margin_right = 8 +patch_margin_bottom = 8 +axis_stretch_horizontal = 1 +axis_stretch_vertical = 1 diff --git a/table/table.gd b/table/table.gd new file mode 100644 index 00000000..8805e5cc --- /dev/null +++ b/table/table.gd @@ -0,0 +1,539 @@ +extends GridContainer + +@export var selector_color: = Color(1.0, 1.0, 1.0, 1.0) +@export var node_gap: = Vector2(0.0, 0.0) # +@export_file var node_type_file: = 'res://table/node.tscn': get = get_node_scene, set = load_node_scene +@export_file var selector_type_file: = 'res://table/selector.tscn': set = load_selector_scene +@export var selector_key_up: Key = KEY_UP # Клавиша уменьшения индекса выбранного ряда +@export var selector_key_down: Key = KEY_DOWN # Клавиша увеличения индекса выбранного ряда + +const MAX_INDEX = 99 # Максимальное значение индекса для колонки и строки. Связано с форматом индекса ключа +const INDEX_FORMAT = '%02d_%02d' # Формат индекса ключа +const NODE_PREFIX = 'NODE_' # Префикс ключа ячейки таблицы +const HEAD_PREFIX = 'HEAD_' # Префикс ключа заголовка таблицы +const DATA_PREFIX = 'DATA_' # Префикс ключа данных ячейки таблицы +const CONN_PREFIX = 'CONN_' # Префикс ключа обработчиков событий ячейки таблицы +const SLCT_PREFIX = 'SLCT_' # Префикс ключа индикатора выбранного ряда таблицы + +const NODE_KEY_FORMAT = NODE_PREFIX + INDEX_FORMAT +const HEAD_KEY_FORMAT = HEAD_PREFIX + INDEX_FORMAT +const DATA_KEY_FORMAT = DATA_PREFIX + INDEX_FORMAT +const CONNECTED_KEY_FORMAT = CONN_PREFIX + INDEX_FORMAT +const ROW_SELECTOR_KEY_FORMAT = SLCT_PREFIX + INDEX_FORMAT + +var nodes: = {} # Все ячейки таблицы и дополнительные элементы +var selector_color_proc: Callable # Ссылка на процедуру генератора цвета индикатора выбранного ряда +var selector_size_proc: Callable # Ссылка на процедуру генератора размера индикатора выбранного ряда +var ScnSelector: PackedScene # Прототип индикатора выбранного ряда +var ScnNode: PackedScene # Прототип ячейки таблицы +var in_focus: = false # Фокус пользовательского ввода +var header: GridContainer # Заголовок таблицы + +signal row_pressed(i_row: int) # Вызывается при нажатии кнопкой мыши на строку +signal selected_changed(i_row: int) # Вызывается при изменении выбранного ряда. i_row - текущий выбранный ряд + + +func selector_up(): + var i_rows: = [] + get_selected_rows(i_rows) + for i in i_rows: + set_row_selected(i, false) + var rows_count: = get_rows_count() + if not (i_rows.size() and (rows_count > 1)): + return + var i_row = i_rows[0] + i_row -= 1 + if i_row < 0: + i_row = rows_count - 1 + set_row_selected(i_row, true) + emit_signal('selected_changed', i_row) + + +func selector_down(): + var i_rows: = [] + get_selected_rows(i_rows) + for i in i_rows: + set_row_selected(i, false) + var rows_count: = get_rows_count() + if not (i_rows.size() and (rows_count > 1)): + return + var i_row = i_rows[0] + i_row += 1 + i_row %= rows_count + set_row_selected(i_row, true) + emit_signal('selected_changed', i_row) + + +func set_columns_alignments(alignments): + assert(columns == alignments.size()) + var rows_count: = get_rows_count() + for i_row in rows_count: + for i_col in alignments.size(): + set_node_alligment(i_col, i_row, alignments[i_col]) + + +func load_selector_scene(file_name: String): + selector_type_file = file_name + ScnSelector = load(file_name) + + +func load_node_scene(file_name: String): + node_type_file = file_name + ScnNode = load(file_name) + + +func get_node_scene(): + return node_type_file + + +func _ready(): + ScnSelector = load(selector_type_file) + ScnNode = load(node_type_file) + assert(ScnSelector) + assert(ScnNode) + + +func align_to_left_bottom(header, gap: = Vector2(0, 0)): + """ Выравнивает по нижнему левому углу. + Используется для выравнивания по заголовку """ + position.x = header.position.x + gap.x + position.y = header.position.y + header.size.y + gap.y + + +func _assert_meta(i_col: int, i_row: int, pname: String, node): + assert(node.has_meta(pname), 'ошибка: нет мета-свойства \"%s\" у ячейки (%s): колонка:%d ряд:%d' % [pname, typeof(node), i_col, i_row]) + + +func set_node_meta(i_col: int, i_row: int, pname: String, val) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + node.set_meta(pname, val) + + +func get_node_meta(i_col: int, i_row: int, pname: String): + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + _assert_meta(i_col, i_row, pname, node) + return node.get_meta(pname) + + +func set_node_property(i_col: int, i_row: int, pname: String, val) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + node.set(pname, val) + + +func get_node_property(i_col: int, i_row: int, pname: String): + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + return node.get(pname) + + +func get_node_key(format: String, i_col: int, i_row: int) -> String: + var key: String = format % [i_row, i_col] + assert(nodes.has(key), 'ошибка: нет такой ячейки: колонка:%d ряд:%d' % [i_col, i_row]) + return key + + +func get_key_indexes(key: String) -> Array: + var tokens: = key.split('_') + assert(tokens.size() == 3, 'ошибка: неверный ключ: \"%s\"' % key) + return [int(tokens[2]), int(tokens[1])] + + +func set_columns_min_size(sizes: Array) -> void: + var rows_count: = get_rows_count() + for i_row in rows_count: + set_nodes_min_size(i_row, sizes) + set_nodes_min_size(0, sizes, HEAD_KEY_FORMAT) + + +func set_columns_aligment(alignments: Array) -> void: + var rows_count: = get_rows_count() + for i_row in rows_count: + for i_col in alignments.size(): + set_node_alligment(i_col, i_row, alignments[i_col]) + + +func _input(event) -> void: + if event is InputEventKey: + if in_focus and event.pressed: + if event.keycode == selector_key_up: + selector_up() + elif event.keycode == selector_key_down: + selector_down() + elif event is InputEventMouseButton: + if not event.pressed: + return + if (event.position.x < position.x) \ + or (event.position.x > (position.x + size.x)) \ + or (event.position.y < position.y) \ + or (event.position.y > (position.y + size.y)): + in_focus = false + emit_signal('focus_exited') + return + var rows_count: = get_rows_count() + in_focus = true + emit_signal('focus_entered') + for i_row in rows_count: + var rect = [0, 0, 0, 0] + get_node_rect(0, i_row, rect) + var y = rect[1] + position.y + if event.position.y < y: + continue + if event.position.y > (y + rect[3]): + continue + emit_signal('row_pressed', i_row) + break + + +func _init() -> void: + selector_color_proc = Callable(self, 'defaultselector_color_proc') + assert(selector_color_proc.is_valid()) + selector_size_proc = Callable(self, 'default_selector_size_proc') + assert(selector_size_proc.is_valid()) + + +func defaultselector_color_proc(i_row: int): + return selector_color + + +func setselector_color_proc(instance: Object, function: String) -> void: + selector_color_proc = Callable(instance, function) + assert(selector_color_proc.is_valid()) + + +func set_selector_size_proc(instance: Object, function: String) -> void: + selector_size_proc = Callable(instance, function) + assert(selector_size_proc.is_valid()) + + +func default_selector_size_proc(i_row: int, rect: Array) -> void: + get_row_rect(i_row, rect) + rect[0] = rect[0] + position.x - node_gap.x / 2.0 + rect[1] = rect[1] + position.y - node_gap.y / 2.0 + rect[2] = rect[2] + node_gap.x + rect[3] = rect[3] + node_gap.y + + +func clear_rows_selected(): + var rows_count: = get_rows_count() + for i_row in rows_count: + set_row_selected(i_row, false) + + +func set_row_selected(i_row: int, selected: bool = true) -> void: + var key: = ROW_SELECTOR_KEY_FORMAT % [i_row, 0] + var parent = get_parent() + if nodes.has(key) and (not selected): + var selector = nodes[key] + parent.remove_child(selector) + selector.queue_free() + nodes.erase(key) + elif (not nodes.has(key)) and selected: + var selector = ScnSelector.instantiate() + var rect: = [0, 0, 0, 0] + selector_size_proc.call(i_row, rect) + selector.position.x = rect[0] + selector.position.y = rect[1] + selector.size.x = rect[2] + selector.size.y = rect[3] + selector.modulate = selector_color_proc.call(i_row) + nodes[key] = selector + parent.add_child(selector) + + +func is_row_selected(i_row: int) -> bool: + var key: = get_node_key(ROW_SELECTOR_KEY_FORMAT, 0, i_row) + return nodes.has(key) + + +func get_selected_rows(rows: Array) -> void: + for key in nodes.keys(): + if SLCT_PREFIX in key: + var indexes = get_key_indexes(key) + rows.append(indexes[1]) + + +func get_rows_count() -> int: + var rows_count: int = 0 + for key in nodes: + rows_count += int(NODE_PREFIX in key) + return rows_count / columns + + +func get_node2(i_col: int, i_row: int) -> Node: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + return nodes[key] + + +func set_node_user_data(i_col: int, i_row: int, user_data: Object) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key] = user_data + + +func add_row(row_types: Array) -> void: + assert(row_types.size() < MAX_INDEX, 'ошибка: слишком много колонок, больше %d' % (MAX_INDEX + 1)) + if columns == 1: + set_columns(row_types.size()) + else: + assert(columns == row_types.size(), 'ошибка: %s количество колонок установлено ранее - %d, но требуется %d' % [self, columns, row_types.size()]) + var rows_count: int = get_rows_count() + assert(rows_count < MAX_INDEX, 'ошибка: слишком много рядов, больше %d' % (MAX_INDEX + 1)) + for i_col in columns: + var node = row_types[i_col].instantiate() + var key: String = NODE_KEY_FORMAT % [rows_count, i_col] + node.name = key + add_child(node) + nodes[key] = node + + +func set_header(header_types: Array): + assert(header_types.size() < MAX_INDEX, 'ошибка: слишком много колонок, больше %d' % (MAX_INDEX + 1)) + if columns == 1: + set_columns(header_types.size()) + else: + assert(columns == header_types.size()) + var do_adj: = false + if not header: + header = GridContainer.new() + header.position = position + header.columns = header_types.size() + header.size.x = size.x + var parent = get_parent() + parent.add_child(header) + do_adj = true + else: + var childrens = header.get_children() + for node in childrens: + childrens.erase(node.name) + childrens.remove_child(node) + node.queue_free() + for i_col in columns: + var node = header_types[i_col].instantiate() + var key: String = HEAD_KEY_FORMAT % [0, i_col] + node.name = key + nodes[key] = node + header.add_child(node) + if do_adj: + var rect = [0, 0, 0, 0] + get_node_rect(0, 0, rect, HEAD_KEY_FORMAT) + position.y += rect[3] + + +func set_header_text(headers: Array): + assert(columns == headers.size()) + set_row_text(0, headers, 0, HEAD_KEY_FORMAT) + + +func get_node_user_data(i_col: int, i_row: int) -> Object: + var key: = get_node_key(DATA_KEY_FORMAT, i_col, i_row) + return nodes[key] + + +func remove_row(i_row: int): + var sel_key: String = ROW_SELECTOR_KEY_FORMAT % [i_row, 0] + if nodes.has(sel_key): + var selector = nodes[sel_key] + get_parent().remove_child(selector) + selector.queue_free() + nodes.erase(sel_key) + for i_col in columns: + var node_key: String = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var conn_key: String = CONNECTED_KEY_FORMAT % [i_row, i_col] + var data_key: String = DATA_KEY_FORMAT % [i_row, i_col] + var node = nodes[node_key] + nodes.erase(node_key) + remove_child(node) + node.queue_free() + + if nodes.has(data_key): + nodes.erase(data_key) + if nodes.has(conn_key): + var conn = nodes[conn_key] + var callback = conn[0] + var dest_node = conn[1] + node.disconnect('pressed', Callable(dest_node, callback)) + nodes.erase(conn_key) + + +func rehash(swaps: Array): + """ Переназначает ключи в соответствии с размерами таблицы. + После вызова swaps содержит массив пар [<прежний ключ>, <текущий ключ>] """ + assert(not swaps.size(), 'ошибка: swaps не пустой') + var nodes_copy: = {} + var i_col: = 0 + var i_row: = 0 + for key0 in nodes: + if NODE_PREFIX in key0: + var node_key1: String = NODE_KEY_FORMAT % [i_row, i_col] + nodes_copy[node_key1] = nodes[key0] + swaps.append([key0, node_key1]) + + var indxs = get_key_indexes(key0) + + var slct_key0: String = ROW_SELECTOR_KEY_FORMAT % [indxs[1], indxs[0]] + if nodes.has(slct_key0): + var slct_key1: String = ROW_SELECTOR_KEY_FORMAT % [i_row, i_col] + nodes_copy[slct_key1] = nodes[slct_key0] + swaps.append([slct_key0, slct_key1]) + + var conn_key0: String = CONNECTED_KEY_FORMAT % [indxs[1], indxs[0]] + if nodes.has(conn_key0): + var conn_key1: String = CONNECTED_KEY_FORMAT % [i_row, i_col] + nodes_copy[conn_key1] = nodes[conn_key0] + swaps.append([conn_key0, conn_key1]) + + var data_key0: String = DATA_KEY_FORMAT % [indxs[1], indxs[0]] + if nodes.has(data_key0): + var data_key1: String = DATA_KEY_FORMAT % [i_row, i_col] + nodes_copy[data_key1] = nodes[data_key0] + swaps.append([data_key0, data_key1]) + + i_col += 1 + if i_col == columns: + i_col = 0 + i_row += 1 + nodes.clear() + nodes.merge(nodes_copy) + + +func clear_rows() -> void: + """ Удаляет все строки таблицы """ + var rows_count: int = get_rows_count() + for i_row in rows_count: + remove_row(i_row) + + +func set_callbacks(dest_node: Node, event_name: String, callback: String, clmns: Array, user_data: Array) -> void: + """ Настраивает обратные вызовы для колонок. + :dest_node: Получатель события + :event_name: Имя события + :callback: Имя функции получателя события + :clmns: Индексы колонок + :user_data: Аргументы функции получателя события """ + var rows_count = get_rows_count() + for i_row in rows_count: + for i_col in clmns: + assert(i_col >= 0 and i_col < columns, 'ошибка: нет колонки %d' % i_col) + var node_key: String = NODE_KEY_FORMAT % [i_row, i_col] + var conn_key: String = CONNECTED_KEY_FORMAT % [i_row, i_col] + var node = nodes[node_key] + if nodes.has(conn_key): + var conn = nodes[conn_key] + var prev_callback = conn[0] + var prev_dest_node = conn[1] + node.disconnect(event_name, Callable(prev_dest_node, prev_callback)) + assert (OK == node.connect(event_name, Callable(dest_node, callback).bind([node] + user_data))) + nodes[conn_key] = [callback, dest_node] + + +func set_rows_text(rows: Array) -> void: + for i_row in rows.size(): + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + node.text = rows[i_row][i_col] + + +func set_nodes_min_size(i_row: int, sizes: Array, key_format: String = NODE_KEY_FORMAT) -> void: + for i_col in columns: + var key: = get_node_key(key_format, i_col, i_row) + var node = nodes[key] + node.custom_minimum_size.x = sizes[i_col] + node.size.x = sizes[i_col] + + +func set_node_text(i_col: int, i_row: int, val: String) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].text = val + + +func set_rows_state(rows: Array) -> void: + for i_row in rows.size(): + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].button_pressed = rows[i_row][i_col] + + +func set_node_state(i_col: int, i_row: int, val: bool) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].button_pressed = val + + +func set_node_frame(i_col: int, i_row: int, node_name: String, val: int) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + var sprite = node.get_node(node_name) + sprite.frame = val + + +func set_node_editable(i_col: int, i_row: int, val: bool) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].editable = val + + +func set_node_alligment(i_col: int, i_row: int, val: int) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + if 'horizontal_alignment' in nodes[key]: + nodes[key].horizontal_alignment = val + elif 'alignment' in nodes[key]: + nodes[key].alignment = val + + +func set_row_alligments(i_row: int, aligns: Array) -> void: + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].align = aligns[i_col] + + +func get_row_text(i_row: int) -> Array: + var row: = [] + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + row.append(nodes[key].text) + return row + + +func set_row_text(i_row: int, row: Array, i_begin: int = 0, key_format: String = NODE_KEY_FORMAT) -> void: + assert((i_begin + row.size()) < columns) + for i_col in columns - i_begin: + var key: = get_node_key(key_format, i_col + i_begin, i_row) + nodes[key].text = row[i_col] + + +func get_rows_text() -> Array: + var rows_count: = get_rows_count() + var rows: = [] + for i_row in rows_count: + var row = get_row_text(i_row) + rows.append(row) + return rows + + +func get_row(i_row: int) -> Array: + var row: = [] + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + row.append(nodes[key]) + return row + + +func get_node_rect(i_col: int, i_row: int, rect: Array, key_format: String = NODE_KEY_FORMAT) -> void: + assert(rect.size() >= 4, 'ошибка: мало элементов для возврата результата') + var key: = get_node_key(key_format, i_col, i_row) + var cell = nodes[key] + rect[0] = cell.position.x + rect[1] = cell.position.y + rect[2] = cell.size.x + rect[3] = cell.size.y + + +func get_row_rect(i_row: int, rect: Array, key_format: String = NODE_KEY_FORMAT) -> void: + assert(rect.size() >= 4, 'ошибка: мало элементов для возврата результата') + var cell0 = nodes[get_node_key(key_format, 0, i_row)] + var cell1 = nodes[get_node_key(key_format, columns - 1, i_row)] + rect[0] = cell0.position.x + rect[1] = cell0.position.y + rect[2] = (cell1.position.x - cell0.position.x) + cell1.size.x + rect[3] = cell0.size.y