mirror of
https://github.com/lihop/godot-xterm.git
synced 2025-05-04 12:14:24 +02:00
Add more features, bug fixes and bugs ;-)
Most notably: - Reflow is now working. Terminal size will fill the window and cols/rows will be resized/calculated based on window and font size. - Added support for different fonts (i.e. bold, italic, bolditalic). - Enabled blinking characters. - Adde more tests and caught a few subtle bugs. - Removed renderer code (which was part of xterm.js) and just doing naive rendering in terminal.gd, but it seems to perform a lot faster. Still not working completely: - vim (some weirdness going on). - vttest (more weirdness). Todo: - Fix the above. - Draw the cursor! - Improve performance. Performance is still not great. The terminal becomes unusable when running `yes` or `cmatrix -r`.
This commit is contained in:
parent
0769592a1b
commit
0d4e10f5ab
30 changed files with 2640 additions and 1157 deletions
|
@ -15,6 +15,12 @@ var bg = 0
|
|||
var extended = ExtendedAttrs.new()
|
||||
|
||||
|
||||
static func to_color_rgb(value: int) -> Color:
|
||||
# Create color from RGB format.
|
||||
return Color("%02x%02x%02x" % [value >> Attributes.RED_SHIFT & 255,
|
||||
value >> Attributes.GREEN_SHIFT & 255, value & 255])
|
||||
|
||||
|
||||
# flags
|
||||
func is_inverse() -> int:
|
||||
return fg & FgFlags.INVERSE
|
||||
|
@ -27,7 +33,7 @@ func is_blink() -> int:
|
|||
func is_invisible() -> int:
|
||||
return fg & FgFlags.INVISIBLE
|
||||
func is_italic() -> int:
|
||||
return fg & BgFlags.ITALIC
|
||||
return bg & BgFlags.ITALIC
|
||||
func is_dim() -> int:
|
||||
return fg & BgFlags.DIM
|
||||
|
||||
|
@ -60,13 +66,30 @@ func get_fg_color() -> int:
|
|||
Attributes.CM_RGB:
|
||||
return fg & Attributes.RGB_MASK
|
||||
_:
|
||||
return -1
|
||||
return -1 # CM_DEFAULT defaults to -1
|
||||
|
||||
|
||||
func get_bg_color() -> int:
|
||||
match bg & Attributes.CM_MASK:
|
||||
Attributes.CM_P16, Attributes.CM_P256:
|
||||
return bg & Attributes.PCOLOR_MASK
|
||||
Attributes.CM_RGB:
|
||||
return bg & Attributes.RGB_MASK
|
||||
_:
|
||||
return -1 # CM_DEFAULT defaults to -1
|
||||
|
||||
|
||||
func has_extended_attrs() -> int:
|
||||
return bg & BgFlags.HAS_EXTENDED
|
||||
|
||||
|
||||
func update_extended() -> void:
|
||||
if extended.is_empty():
|
||||
bg &= ~BgFlags.HAS_EXTENDED
|
||||
else:
|
||||
bg |= BgFlags.HAS_EXTENDED
|
||||
|
||||
|
||||
func get_underline_color() -> int:
|
||||
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
|
||||
match extended.underline_color & Attributes.CM_MASK:
|
||||
|
@ -117,6 +140,9 @@ func get_underline_style():
|
|||
|
||||
|
||||
class ExtendedAttrs:
|
||||
extends Reference
|
||||
# Extended attributes for a cell.
|
||||
# Holds information about different underline styles and color.
|
||||
|
||||
|
||||
var underline_style = UnderlineStyle.NONE
|
||||
|
@ -125,3 +151,18 @@ class ExtendedAttrs:
|
|||
|
||||
func _init():
|
||||
underline_style
|
||||
|
||||
|
||||
func duplicate():
|
||||
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
|
||||
var AttributeData = load("res://addons/godot_xterm/buffer/attribute_data.gd")
|
||||
var clone = AttributeData.ExtendedAttrs.new()
|
||||
clone.underline_style = underline_style
|
||||
clone.underline_color = underline_color
|
||||
return clone
|
||||
|
||||
|
||||
# Convenient method to indicate whether the object holds no additional information,
|
||||
# that needs to be persistant in the buffer.
|
||||
func is_empty():
|
||||
return underline_style == UnderlineStyle.NONE
|
||||
|
|
|
@ -10,7 +10,7 @@ const Charsets = preload("res://addons/godot_xterm/data/charsets.gd")
|
|||
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
|
||||
const CircularList = preload("res://addons/godot_xterm/circular_list.gd")
|
||||
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
|
||||
|
||||
const BufferReflow = preload("res://addons/godot_xterm/buffer/buffer_reflow.gd")
|
||||
|
||||
const MAX_BUFFER_SIZE = 4294967295 # 2^32 - 1
|
||||
|
||||
|
@ -51,6 +51,286 @@ func _init(has_scrollback: bool, options_service, buffer_service):
|
|||
setup_tab_stops()
|
||||
|
||||
|
||||
# Resizes the buffer, adjusting its data accordingly.
|
||||
# @param new_cols The new number of columns.
|
||||
# @param new_rows The new number of rows.
|
||||
func resize(new_cols: int, new_rows: int) -> void:
|
||||
# store reference to null cell with default attrs
|
||||
var null_cell = get_null_cell(AttributeData.new())
|
||||
|
||||
# Increase max length if needed before adjustments to allow space to fill
|
||||
# as required.
|
||||
var new_max_length = _get_correct_buffer_length(new_rows)
|
||||
if new_max_length > lines.max_length:
|
||||
lines.max_length = new_max_length
|
||||
|
||||
# The following adjustments should only happen if the buffer has been
|
||||
# initialized/filled.
|
||||
if lines.length > 0:
|
||||
# Deal with columns increasing (reducing needs to happen after reflow)
|
||||
if _cols < new_cols:
|
||||
for i in range(lines.length):
|
||||
lines.get_line(i).resize(new_cols, null_cell)
|
||||
|
||||
# Resize rows in both directions as needed
|
||||
var add_to_y = 0
|
||||
if _rows < new_rows:
|
||||
for y in range(_rows, new_rows):
|
||||
if lines.length < new_rows + ybase:
|
||||
if _options_service.options.windows_mode:
|
||||
# Just add the new missing rows on Windows as conpty reprints the screen with it's
|
||||
# view of the world. Once a line enters scrollback for conpty it remains there
|
||||
lines.push(BufferLine.new(new_cols, null_cell))
|
||||
else:
|
||||
if ybase > 0 and lines.length <= ybase + y + add_to_y + 1:
|
||||
# There is room above the buffer and there are no empty elements below the line,
|
||||
# scroll up
|
||||
ybase -= 1
|
||||
add_to_y += 1
|
||||
if ydisp > 0:
|
||||
# Viewport is at the top of the buffer, must increase downwards
|
||||
ydisp -= 1
|
||||
else:
|
||||
# Add a blank line if tere is no buffer left at the top to srcoll to, or if there
|
||||
# are blank lines after the cursor
|
||||
lines.push(BufferLine.new(new_cols, null_cell))
|
||||
else: # _rows >= new_rows
|
||||
for _y in range(_rows, new_rows, -1):
|
||||
if lines.length > new_rows + ybase:
|
||||
if lines.length > ybase + y + 1:
|
||||
# The line is blank line below the cursor, remove it
|
||||
lines.pop()
|
||||
else:
|
||||
# The line is the cursor, scroll down
|
||||
ybase += 1
|
||||
ydisp += 1
|
||||
|
||||
# Reduce max length if needed after adjustments, this is done after as it
|
||||
# would otherwise cut data from the bottom of the buffer.
|
||||
if new_max_length < lines.max_length:
|
||||
# Trim from the top of th ebuffer and adjust ybase and ydisp.
|
||||
var amount_to_trim = lines.length - new_max_length
|
||||
if amount_to_trim > 0:
|
||||
lines.trim_start(amount_to_trim)
|
||||
ybase = max(ybase - amount_to_trim, 0)
|
||||
ydisp = max(ydisp - amount_to_trim, 0)
|
||||
saved_y = max(saved_y - amount_to_trim, 0)
|
||||
lines.max_length = new_max_length
|
||||
|
||||
# Make sure that the cursor stays on screen
|
||||
x = min(x, new_cols - 1)
|
||||
y = min(y, new_rows - 1)
|
||||
if add_to_y:
|
||||
y += add_to_y
|
||||
saved_x = min(saved_x, new_cols -1)
|
||||
|
||||
scroll_top = 0
|
||||
|
||||
scroll_bottom = new_rows - 1
|
||||
|
||||
if _is_reflow_enabled():
|
||||
_reflow(new_cols, new_rows)
|
||||
|
||||
# Trim the end of the line off if cols shrunk
|
||||
if _cols > new_cols:
|
||||
for i in range(lines.length):
|
||||
lines.get_line(i).resize(new_cols, null_cell)
|
||||
|
||||
_cols = new_cols
|
||||
_rows = new_rows
|
||||
|
||||
|
||||
func _is_reflow_enabled() -> bool:
|
||||
return _has_scrollback and not _options_service.options.windows_mode
|
||||
|
||||
|
||||
func _reflow(new_cols: int, new_rows: int) -> void:
|
||||
if _cols == new_cols:
|
||||
return
|
||||
|
||||
# Iterate through rows, ignore the last one as it cannot be wrapped
|
||||
if new_cols > _cols:
|
||||
_reflow_larger(new_cols, new_rows)
|
||||
else:
|
||||
_reflow_smaller(new_cols, new_rows)
|
||||
|
||||
|
||||
func _reflow_larger(new_cols: int, new_rows: int) -> void:
|
||||
var to_remove: PoolIntArray = BufferReflow.reflow_larger_get_lines_to_remove(lines,
|
||||
_cols, new_cols, ybase + y, get_null_cell(AttributeData.new()))
|
||||
if not to_remove.empty():
|
||||
var new_layout_result = BufferReflow.reflow_larger_create_new_layout(lines, to_remove)
|
||||
BufferReflow.reflow_larger_apply_new_layout(lines, new_layout_result.layout)
|
||||
_reflow_larger_adjust_viewport(new_cols, new_rows, new_layout_result.count_removed)
|
||||
|
||||
|
||||
func _reflow_larger_adjust_viewport(new_cols: int, new_rows: int, count_removed: int) -> void:
|
||||
var null_cell = get_null_cell(AttributeData.new())
|
||||
# Adjust viewport based on number of items removed
|
||||
var viewport_adjustments = count_removed
|
||||
while viewport_adjustments > 0:
|
||||
viewport_adjustments -= 1
|
||||
if ybase == 0:
|
||||
if y > 0:
|
||||
y -= 1
|
||||
if lines.length < new_rows:
|
||||
# Add an extra row at the bottom of the viewport
|
||||
lines.push(BufferLine.new(new_cols, null_cell))
|
||||
else:
|
||||
if ydisp == ybase:
|
||||
ydisp -= 1
|
||||
ybase -= 1
|
||||
|
||||
saved_y = max(saved_y - count_removed, 0)
|
||||
|
||||
|
||||
func _reflow_smaller(new_cols: int, new_rows: int) -> void:
|
||||
var null_cell = get_null_cell(AttributeData.new())
|
||||
# Gather all BufferLines that need to be inserted into the Buffer here so that they can be
|
||||
# batched up and only commited once
|
||||
var to_insert = []
|
||||
var count_to_insert = 0
|
||||
# Go backwards as many lines may be trimmed and this will avoid considering them
|
||||
var i = lines.length - 1
|
||||
while i >= 0:
|
||||
# Check wether this line is a problem
|
||||
var next_line = lines.get_line(i)
|
||||
if not next_line or not next_line.is_wrapped and next_line.get_trimmed_length() <= new_cols:
|
||||
i -= 1
|
||||
continue
|
||||
|
||||
# Gather wrapped lines and adjust y to be the starting line
|
||||
var wrapped_lines = [next_line]
|
||||
while next_line.is_wrapped and i > 0:
|
||||
i -= 1
|
||||
next_line = lines.get_line(i)
|
||||
wrapped_lines.push_front(next_line)
|
||||
|
||||
# If these lines contain the cursor don't touch them, the program will handle fixing up
|
||||
# wrapped lines with the cursor
|
||||
var absolute_y = ybase + y
|
||||
if absolute_y >= i and absolute_y < i + wrapped_lines.size():
|
||||
i -= 1
|
||||
continue
|
||||
|
||||
var last_line_length = wrapped_lines[wrapped_lines.size() - 1].get_trimmed_length()
|
||||
var dest_line_lengths = BufferReflow.reflow_smaller_get_new_line_lengths(wrapped_lines, _cols, new_cols)
|
||||
var lines_to_add = dest_line_lengths.size() - wrapped_lines.size()
|
||||
var trimmed_lines: int
|
||||
if ybase == 0 and y != lines.length - 1:
|
||||
# If the top section of the buffer is not yet filled
|
||||
trimmed_lines = max(0, y - lines.max_length + lines_to_add)
|
||||
else:
|
||||
trimmed_lines = max(0, lines.length - lines.max_length + lines_to_add)
|
||||
|
||||
# Add the new lines
|
||||
var new_lines = []
|
||||
for j in range(lines_to_add):
|
||||
var new_line = get_blank_line(AttributeData.new(), true)
|
||||
new_lines.append(new_line)
|
||||
if not new_lines.empty():
|
||||
to_insert.append({"start": i + wrapped_lines.size() + count_to_insert,
|
||||
"new_lines": new_lines})
|
||||
count_to_insert += new_lines.size()
|
||||
wrapped_lines += new_lines
|
||||
|
||||
# Copy buffer data to new locations, this needs to happen backwards to do in-place
|
||||
var dest_line_index = dest_line_lengths.size() - 1 # floor(cells_needed / new_cols)
|
||||
var dest_col = dest_line_lengths[dest_line_index] # cells_needed % new_cols
|
||||
if dest_col == 0:
|
||||
dest_line_index -= 1
|
||||
dest_col = dest_line_lengths[dest_line_index]
|
||||
var src_line_index = wrapped_lines.size() - lines_to_add - 1
|
||||
var src_col = last_line_length
|
||||
while src_line_index >= 0:
|
||||
var cells_to_copy = min(src_col, dest_col)
|
||||
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[src_line_index],
|
||||
src_col - cells_to_copy, dest_col - cells_to_copy, cells_to_copy, true)
|
||||
dest_col -= cells_to_copy
|
||||
if dest_col == 0:
|
||||
dest_line_index -= 1
|
||||
dest_col = dest_line_lengths[dest_line_index]
|
||||
src_col -= cells_to_copy
|
||||
if src_col == 0:
|
||||
src_line_index -= 1
|
||||
var wrapped_lines_index = max(src_line_index, 0)
|
||||
src_col = BufferReflow.get_wrapped_line_trimmed_length(wrapped_lines, wrapped_lines_index, _cols)
|
||||
|
||||
# Null out the end of the line ends if a wide character wrapped to the following line
|
||||
for j in range(wrapped_lines.size()):
|
||||
if dest_line_lengths[j] < new_cols:
|
||||
wrapped_lines[j].set_cell(dest_line_lengths[j], null_cell)
|
||||
|
||||
# Adjust viewport as needed
|
||||
var viewport_adjustments = lines_to_add - trimmed_lines
|
||||
while viewport_adjustments > 0:
|
||||
if ybase == 0:
|
||||
if y < new_rows - 1:
|
||||
y += 1
|
||||
lines.pop()
|
||||
else:
|
||||
ybase += 1
|
||||
ydisp += 1
|
||||
else:
|
||||
# Ensure ybase does not exceed its maximum value
|
||||
if ybase < min(lines.max_length, (lines.length + count_to_insert) - new_rows):
|
||||
if ybase == ydisp:
|
||||
ydisp += 1
|
||||
ybase += 1
|
||||
viewport_adjustments -= 1
|
||||
saved_y = min(saved_y + lines_to_add, ybase + new_rows - 1)
|
||||
i -= 1
|
||||
|
||||
# Rearrange lines in the buffer if there are any insertions, this is done at the end rather
|
||||
# than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
|
||||
# costly calls to CircularList.splice.
|
||||
if not to_insert.empty():
|
||||
# Record buffer insert events and then play them backwards so that the indexes are
|
||||
# correct
|
||||
var insert_events = []
|
||||
|
||||
# Record original lines so they don't get overridden when we rearrange the list
|
||||
var original_lines = []
|
||||
for i in range(lines.length):
|
||||
original_lines.append(lines.get_line(i))
|
||||
var original_lines_length = lines.length
|
||||
|
||||
var original_line_index = original_lines_length - 1
|
||||
var next_to_insert_index = 0
|
||||
var next_to_insert = to_insert[next_to_insert_index]
|
||||
lines.length = min(lines.max_length, lines.length + count_to_insert)
|
||||
var count_inserted_so_far = 0
|
||||
var j = min(lines.max_length - 1, original_lines_length + count_to_insert - 1)
|
||||
while j >= 0:
|
||||
if next_to_insert and next_to_insert.start > original_line_index + count_inserted_so_far:
|
||||
# Insert extra lines here, adjusting i as needed
|
||||
for next_i in range(next_to_insert.new_lines.size() - 1, -1, -1):
|
||||
lines.set_line(j, next_to_insert.new_lines[next_i])
|
||||
j -= 1
|
||||
j += 1
|
||||
|
||||
# Create insert events for later
|
||||
insert_events.append({"index": original_line_index + 1,
|
||||
"amount": next_to_insert.new_lines.size()})
|
||||
count_inserted_so_far += next_to_insert.new_lines.size()
|
||||
next_to_insert_index += 1
|
||||
next_to_insert = to_insert[next_to_insert_index] if to_insert.size() > next_to_insert_index else null
|
||||
else:
|
||||
lines.set_line(j, original_lines[original_line_index])
|
||||
original_line_index -= 1
|
||||
j -= 1
|
||||
|
||||
# Update markers
|
||||
var insert_count_emitted = 0
|
||||
for i in range(insert_events.size() - 1, -1, -1):
|
||||
insert_events[i].index += insert_count_emitted
|
||||
lines.emit_signal("inserted", insert_events[i])
|
||||
insert_count_emitted += insert_events[i].amount
|
||||
var amount_to_trim = max(0, original_lines_length + count_to_insert - lines.max_length)
|
||||
if amount_to_trim > 0:
|
||||
lines.emit_signal("trimmed", amount_to_trim)
|
||||
|
||||
|
||||
func get_null_cell(attr = null):
|
||||
if attr:
|
||||
_null_cell.fg = attr.fg
|
||||
|
@ -111,21 +391,23 @@ func get_wrapped_range_for_line(y: int) -> Dictionary:
|
|||
return {"first": first, "last": last}
|
||||
|
||||
|
||||
# Setup the tab stops.
|
||||
# @param i The index to start setting up tab stops from.
|
||||
func setup_tab_stops(i = null) -> void:
|
||||
if i == null:
|
||||
return
|
||||
|
||||
if not tabs.get(i):
|
||||
i = prev_stop(i)
|
||||
if i != null:
|
||||
if not tabs.get(i):
|
||||
i = prev_stop(i)
|
||||
else:
|
||||
tabs = {}
|
||||
i = 0
|
||||
|
||||
|
||||
while i < _cols:
|
||||
tabs[i] = true
|
||||
i += _options_service.options.tab_stop_width
|
||||
i += max(_options_service.options.tab_stop_width, 1)
|
||||
|
||||
|
||||
# Move the cursor to the previous tab stop from the given position (default is current).
|
||||
# @param x The position to move the cursor to the previous tab stop.
|
||||
func prev_stop(x: int) -> int:
|
||||
if x == null:
|
||||
x = self.x
|
||||
|
@ -135,7 +417,14 @@ func prev_stop(x: int) -> int:
|
|||
|
||||
return _cols - 1 if x > _cols else 0 if x < 0 else x
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Move the cursor one tab stop forward from the given position (default is current).
|
||||
# @param x The position to move the cursor one tab stop forward.
|
||||
func next_stop(x = null) -> int:
|
||||
if x == null:
|
||||
x = self.x
|
||||
|
||||
x += 1
|
||||
while not tabs.get(x) and x < _cols:
|
||||
x += 1
|
||||
|
||||
return _cols - 1 if x >= _cols else 0 if x < 0 else x
|
||||
|
|
|
@ -41,7 +41,10 @@ func get_cell(index: int):
|
|||
|
||||
|
||||
func get_width(index: int) -> int:
|
||||
return _data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT
|
||||
if (index * CELL_SIZE + Cell.CONTENT) < _data.size():
|
||||
return _data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
func has_content(index: int) -> int:
|
||||
|
@ -221,13 +224,43 @@ func fill(fill_cell_data) -> void:
|
|||
set_cell(i, fill_cell_data)
|
||||
|
||||
|
||||
# alter to a full copy of line
|
||||
func copy_from(line) -> void:
|
||||
_data = line._data.duplicate()
|
||||
length = line.length
|
||||
_combined = {}
|
||||
for k in line._combined.keys():
|
||||
_combined[k] = line._combined[k]
|
||||
is_wrapped = line.is_wrapped
|
||||
|
||||
|
||||
func get_trimmed_length() -> int:
|
||||
for i in range(length - 1, 0, -1):
|
||||
for i in range(length - 1, -1, -1):
|
||||
if _data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK:
|
||||
return i + (_data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT)
|
||||
return 0
|
||||
|
||||
|
||||
func copy_cells_from(src, src_col: int, dest_col: int, length: int, apply_in_reverse: bool) -> void:
|
||||
var src_data = src._data
|
||||
|
||||
if apply_in_reverse:
|
||||
for cell in range(length - 1, -1, -1):
|
||||
for i in range(CELL_SIZE):
|
||||
_data[(dest_col + cell) * CELL_SIZE + i] = src_data[(src_col + cell) * CELL_SIZE + i]
|
||||
else:
|
||||
for cell in range(length):
|
||||
for i in range(CELL_SIZE):
|
||||
_data[(dest_col + cell) * CELL_SIZE + i] = src_data[(src_col + cell) * CELL_SIZE + i]
|
||||
|
||||
# Move any combined data over as needed, FIXME: repeat for extended attrs
|
||||
var src_combined_keys = src._combined.keys()
|
||||
for i in range(src_combined_keys.size()):
|
||||
var key = int(src_combined_keys[i])
|
||||
if key >= src_col:
|
||||
_combined[key + src_col + dest_col] = src._combined[key]
|
||||
|
||||
|
||||
func translate_to_string(trim_right: bool = false, start_col: int = 0, end_col: int = -1) -> String:
|
||||
if end_col == -1:
|
||||
end_col = length
|
||||
|
@ -245,3 +278,14 @@ func translate_to_string(trim_right: bool = false, start_col: int = 0, end_col:
|
|||
result += Constants.WHITESPACE_CELL_CHAR
|
||||
start_col += max(content >> Content.WIDTH_SHIFT, 1) # always advance by 1
|
||||
return result
|
||||
|
||||
|
||||
func duplicate():
|
||||
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
|
||||
var duplicant = load("res://addons/godot_xterm/buffer/buffer_line.gd").new(length)
|
||||
duplicant._data = _data.duplicate(true)
|
||||
duplicant._combined = _combined.duplicate(true)
|
||||
duplicant._extended_attrs = _extended_attrs.duplicate(true)
|
||||
duplicant.length = length
|
||||
duplicant.is_wrapped = is_wrapped
|
||||
return duplicant
|
||||
|
|
198
addons/godot_xterm/buffer/buffer_reflow.gd
Normal file
198
addons/godot_xterm/buffer/buffer_reflow.gd
Normal file
|
@ -0,0 +1,198 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Object
|
||||
|
||||
|
||||
# Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed
|
||||
# when a wrapped line unwraps.
|
||||
# @param lines The buffer lines.
|
||||
# @param newCols The columns after resize.
|
||||
static func reflow_larger_get_lines_to_remove(lines, old_cols: int, new_cols: int,
|
||||
buffer_absolute_y: int, null_cell) -> PoolIntArray:
|
||||
# Gather all BufferLines that need to be removed from the Buffer here so that they can be
|
||||
# batched up and only committed once
|
||||
var to_remove = PoolIntArray([])
|
||||
|
||||
var y = 0
|
||||
while y < lines.length - 1:
|
||||
# Check if this row is wrapped
|
||||
var i = y
|
||||
i += 1
|
||||
var next_line = lines.get_line(i)
|
||||
if not next_line.is_wrapped:
|
||||
y += 1
|
||||
continue
|
||||
|
||||
# Check how many lines it's wrapped for
|
||||
var wrapped_lines = [lines.get_line(y)]
|
||||
while i < lines.length and next_line.is_wrapped:
|
||||
wrapped_lines.append(next_line)
|
||||
i += 1
|
||||
next_line = lines.get_line(i)
|
||||
|
||||
# If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
|
||||
# lines with the cursor
|
||||
if buffer_absolute_y >= y and buffer_absolute_y < i:
|
||||
y += wrapped_lines.size() - 1
|
||||
y += 1
|
||||
continue
|
||||
|
||||
# Copy buffer data to new locations
|
||||
var dest_line_index = 0
|
||||
var dest_col = get_wrapped_line_trimmed_length(wrapped_lines, dest_line_index, old_cols)
|
||||
var src_line_index = 1
|
||||
var src_col = 0
|
||||
while src_line_index < wrapped_lines.size():
|
||||
var src_trimmed_line_length = get_wrapped_line_trimmed_length(wrapped_lines, src_line_index, old_cols)
|
||||
var src_remaining_cells = src_trimmed_line_length - src_col
|
||||
var dest_remaining_cells = new_cols - dest_col
|
||||
var cells_to_copy = min(src_remaining_cells, dest_remaining_cells)
|
||||
|
||||
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[src_line_index], src_col, dest_col, cells_to_copy, false)
|
||||
|
||||
dest_col += cells_to_copy
|
||||
if dest_col == new_cols:
|
||||
dest_line_index += 1
|
||||
dest_col = 0
|
||||
|
||||
src_col += cells_to_copy
|
||||
if src_col == src_trimmed_line_length:
|
||||
src_line_index += 1
|
||||
src_col = 0
|
||||
|
||||
# Make sure the last cell isn't wide, if it is copy it to the current dest
|
||||
if dest_col == 0 and dest_line_index != 0:
|
||||
if wrapped_lines[dest_line_index - 1].get_width(new_cols - 1) == 2:
|
||||
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[dest_line_index - 1], new_cols - 1, dest_col, 1, false)
|
||||
dest_col += 1
|
||||
# Null out the end of the last row
|
||||
wrapped_lines[dest_line_index - 1].set_cell(new_cols - 1, null_cell)
|
||||
|
||||
# Clear out remaining cells or fragments could remain;
|
||||
var replaced = wrapped_lines[dest_line_index].translate_to_string()
|
||||
wrapped_lines[dest_line_index].replace_cells(dest_col, new_cols, null_cell)
|
||||
|
||||
# Work backwards and remove any rows at the end that only contain null cells
|
||||
var count_to_remove = 0
|
||||
for i in range(wrapped_lines.size() - 1, 0, -1):
|
||||
if i > dest_line_index or wrapped_lines[i].get_trimmed_length() == 0:
|
||||
count_to_remove += 1
|
||||
else:
|
||||
break
|
||||
|
||||
if count_to_remove > 0:
|
||||
to_remove.append(y + wrapped_lines.size() - count_to_remove) # index
|
||||
to_remove.append(count_to_remove)
|
||||
|
||||
y += wrapped_lines.size() - 1
|
||||
y += 1
|
||||
|
||||
return to_remove
|
||||
|
||||
|
||||
# Creates and return the new layout for lines given an array of indexes to be removed.
|
||||
# @param lines The buffer lines.
|
||||
# @param to_remove The indexes to remove.
|
||||
static func reflow_larger_create_new_layout(lines, to_remove: PoolIntArray):
|
||||
var layout = PoolIntArray([])
|
||||
# First iterate through the list and get the actual indexes to use for rows
|
||||
var next_to_remove_index = 0
|
||||
var next_to_remove_start = to_remove[next_to_remove_index]
|
||||
var count_removed_so_far = 0
|
||||
var i = 0
|
||||
while i < lines.length:
|
||||
if next_to_remove_start == i:
|
||||
next_to_remove_index += 1
|
||||
var count_to_remove = to_remove[next_to_remove_index]
|
||||
|
||||
# Tell markers that there was a deletion
|
||||
lines.emit_signal("deleted", i - count_removed_so_far, count_to_remove)
|
||||
|
||||
i += count_to_remove - 1
|
||||
count_removed_so_far += count_to_remove
|
||||
next_to_remove_index += 1
|
||||
next_to_remove_start = to_remove[next_to_remove_index] if next_to_remove_index < to_remove.size() else null
|
||||
else:
|
||||
layout.append(i)
|
||||
|
||||
i += 1
|
||||
|
||||
return { "layout": layout, "count_removed": count_removed_so_far }
|
||||
|
||||
|
||||
# Applies a new layout to the buffer. This essentially does the same as many splice calls but it's
|
||||
# done all at once in a single iteration through the list since splice is very expensive.
|
||||
# @param lines The buffer lines.
|
||||
# @param new_layout The new layout to apply.
|
||||
static func reflow_larger_apply_new_layout(lines, new_layout: PoolIntArray) -> void:
|
||||
# Record original lines so they don't get overridden when we rearrange the list
|
||||
var new_layout_lines = []
|
||||
for i in range(new_layout.size()):
|
||||
new_layout_lines.append(lines.get_line(new_layout[i]))
|
||||
|
||||
# Rearrange the list
|
||||
for i in range(new_layout_lines.size()):
|
||||
lines.set_line(i, new_layout_lines[i])
|
||||
lines.length = new_layout.size()
|
||||
|
||||
|
||||
# Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
|
||||
# compute the wrapping points since wide characters may need to be wrapped onto the following line.
|
||||
# This function will return an array of numbers of where each line wraps to, the resulting array
|
||||
# will only contain the values `newCols` (when the line does not end with a wide character) and
|
||||
# `new_cols - 1` (when the line does end with a wide character), except for the last value which
|
||||
# will contain the remaining items to fill the line.
|
||||
#
|
||||
# Calling this with a `new_cols` value of `1` will lock up.
|
||||
#
|
||||
# @param wrapped_lines The wrapped lines to evaluate.
|
||||
# @param old_cols The columns before resize.
|
||||
# @param new_cols The columns after resize.
|
||||
static func reflow_smaller_get_new_line_lengths(wrapped_lines: Array, old_cols: int, new_cols: int) -> PoolIntArray:
|
||||
var new_line_lengths = PoolIntArray([])
|
||||
var cells_needed: int
|
||||
for i in range(wrapped_lines.size()):
|
||||
cells_needed += get_wrapped_line_trimmed_length(wrapped_lines, i, old_cols)
|
||||
|
||||
# Use src_col and scr_line to find the new wrapping point, use that to get the cells_available and
|
||||
# lines_needed
|
||||
var src_col = 0
|
||||
var src_line = 0
|
||||
var cells_available = 0
|
||||
while cells_available < cells_needed:
|
||||
if cells_needed - cells_available < new_cols:
|
||||
# Add the final line and exit the loop
|
||||
new_line_lengths.append(cells_needed - cells_available)
|
||||
break
|
||||
|
||||
src_col += new_cols
|
||||
var old_trimmed_length = get_wrapped_line_trimmed_length(wrapped_lines, src_line, old_cols)
|
||||
if src_col > old_trimmed_length:
|
||||
src_col -= old_trimmed_length
|
||||
src_line += 1
|
||||
|
||||
var ends_with_wide = wrapped_lines[src_line].get_width(src_col - 1) == 2
|
||||
if ends_with_wide:
|
||||
src_col -= 1
|
||||
|
||||
var line_length = new_cols - 1 if ends_with_wide else new_cols
|
||||
new_line_lengths.append(line_length)
|
||||
cells_available += line_length
|
||||
|
||||
return new_line_lengths
|
||||
|
||||
|
||||
static func get_wrapped_line_trimmed_length(lines: Array, i: int, cols: int) -> int:
|
||||
# If this is the last row in the wrapped line, get the actual trimmed length
|
||||
if i == lines.size() - 1:
|
||||
return lines[i].get_trimmed_length()
|
||||
|
||||
# Detect whether the following line starts with a wide character and the end of the current line
|
||||
# is null, if so then we can be pretty sure the null character should be excluded from the line
|
||||
# length
|
||||
var ends_in_null = not (lines[i].has_content(cols - 1)) and lines[i].get_width(cols - 1) == 1
|
||||
var following_line_starts_with_wide = lines[i + 1].get_width(0) == 2
|
||||
if ends_in_null and following_line_starts_with_wide:
|
||||
return cols - 1
|
||||
return cols
|
|
@ -21,8 +21,7 @@ func _init(options_service, buffer_service):
|
|||
alt = Buffer.new(false, options_service, buffer_service)
|
||||
active = normal
|
||||
|
||||
# TODO
|
||||
#setup_tab_stops()
|
||||
setup_tab_stops()
|
||||
|
||||
|
||||
# Sets the normal Bufer of the BufferSet as its currently active Buffer.
|
||||
|
@ -53,3 +52,18 @@ func activate_alt_buffer(fill_attr = null) -> void:
|
|||
alt.y = normal.y
|
||||
active = alt
|
||||
emit_signal("buffer_activated", alt, normal)
|
||||
|
||||
|
||||
# Resizes both normal and alt buffers, adjusting their data accordingly.
|
||||
# @param new_cols The new number of columns.
|
||||
# @param new_rows The new number of rows.
|
||||
func resize(new_cols: int, new_rows: int) -> void:
|
||||
normal.resize(new_cols, new_rows)
|
||||
alt.resize(new_cols, new_rows)
|
||||
|
||||
|
||||
# Setup the tab stops.
|
||||
# @param i The index to start setting up tab stops from.
|
||||
func setup_tab_stops(i = null) -> void:
|
||||
normal.setup_tab_stops(i)
|
||||
alt.setup_tab_stops(i)
|
||||
|
|
|
@ -91,3 +91,16 @@ enum UnderlineStyle {
|
|||
DOTTED
|
||||
DASHED
|
||||
}
|
||||
|
||||
enum CursorStyle {
|
||||
BLOCK
|
||||
UNDERLINE
|
||||
BAR
|
||||
}
|
||||
|
||||
enum BellStyle {
|
||||
NONE
|
||||
VISUAL
|
||||
SOUND
|
||||
BOTH
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue