diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 7da0e08856..c9bd67f110 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -5513,6 +5513,155 @@ The ``EditField`` class also provides the following functions: Inserts the given text at the current cursor position. +TextArea class +-------------- + +Subclass of Panel; implements a multi-line text field with features such as +text wrapping, mouse control, text selection, clipboard support, history, +and typical text editor shortcuts. + +Cursor Behavior +~~~~~~~~~~~~~~~ + +The cursor in the ``TextArea`` class is index-based, starting from 1, +consistent with Lua's text indexing conventions. + +Each character, including newlines (``string.char(10)``), +occupies a single index in the text content. + +Cursor movement and position are fully aware of line breaks, +meaning they count as one unit in the offset. + +The cursor always points to the position between characters, +with 1 being the position before the first character and +``#text + 1`` representing the position after the last character. + +Cursor positions are preserved during text operations like insertion, +deletion, or replacement. If changes affect the cursor's position, +it will be adjusted to the nearest valid index. + +TextArea Attributes: + +* ``init_text``: The initial text content for the text area. + +* ``init_cursor``: The initial cursor position within the text content. + If not specified, defaults to end of the text (length of ``init_text`` + 1). + +* ``text_pen``: Optional pen used to draw the text. Default is ``COLOR_LIGHTCYAN``. + +* ``select_pen``: Optional pen used for text selection. Default is ``COLOR_CYAN``. + +* ``ignore_keys``: List of input keys to ignore. + Functions similarly to the ``ignore_keys`` attribute in the ``EditField`` class. + +* ``on_text_change``: Callback function called whenever the text changes. + The function signature should be ``on_text_change(new_text, old_text)``. + +* ``on_cursor_change``: Callback function called whenever the cursor position changes. + Expected function signature is ``on_cursor_change(new_cursor, old_cursor)``. + +* ``one_line_mode``: If set to ``true``, disables multi-line text features. + In this mode the :kbd:`Enter` key is not handled by the widget + as if it were included in ``ignore_keys``. + If multiline text (including ``\n`` chars) is pasted into the widget, newlines are removed. + +TextArea Functions: + +* ``textarea:getText()`` + + Returns the current text content of the ``TextArea`` widget as a string. + "\n" characters (``string.char(10)``) should be interpreted as new lines + +* ``textarea:setText(text)`` + + Sets the content of the ``TextArea`` to the specified string ``text``. + The cursor position will not be adjusted, so should be set separately. + +* ``textarea:getCursor()`` + + Returns the current cursor position within the text content. + The position is represented as a single integer, starting from 1. + +* ``textarea:setCursor(cursor)`` + + Sets the cursor position within the text content. + +* ``textarea:scrollToCursor()`` + + Scrolls the text area view to ensure that the current cursor position is visible. + This happens automatically when the user interactively moves the cursor or + pastes text into the widget, but may need to be called when ``setCursor`` is + called programmatically. + +* ``textarea:clearHistory()`` + + Clears undo/redo history of the widget. + +Functionality +~~~~~~~~~~~~~ + +The TextArea widget provides a familiar and intuitive text editing experience with baseline features such as: + +- Text Wrapping: Automatically fits text within the display area. +- Mouse and Keyboard Support: Standard keys like :kbd:`Home`, :kbd:`End`, :kbd:`Backspace`, and :kbd:`Delete` are supported, + along with gestures like double-click to select a word or triple-click to select a line. +- Clipboard Operations: copy, cut, and paste, + with intuitive defaults when no text is selected. +- Undo/Redo: :kbd:`Ctrl` + :kbd:`Z` and :kbd:`Ctrl` + :kbd:`Y` for quick changes. +- Additional features include advanced navigation, line management, + and smooth scrolling for handling long text efficiently. + +Detailed list: + +- Cursor Control: Navigate through text using arrow keys (Left, Right, Up, + and Down) for precise cursor placement. +- Mouse Control: Use the mouse to position the cursor within the text, + providing an alternative to keyboard navigation. +- Text Selection: Select text with the mouse, with support for replacing or + removing selected text. +- Select Word/Line: Use double click to select current word, or triple click to + select current line. +- Move By Word: Use :kbd:`Ctrl` + :kbd:`Left` and :kbd:`Ctrl` + :kbd:`Right` to + move the cursor one word back or forward. +- Line Navigation: :kbd:`Home` moves the cursor to the beginning of the current + line, and :kbd:`End` moves it to the end. +- Jump to Beginning/End: Quickly move the cursor to the beginning or end of the + text using :kbd:`Ctrl` + :kbd:`Home` and :kbd:`Ctrl` + :kbd:`End`. +- Longest X Position Memory: The cursor remembers the longest x position when + moving up or down, making vertical navigation more intuitive. +- New Lines: Easily insert new lines using the :kbd:`Enter` key, supporting + multiline text input. +- Text Wrapping: Text automatically wraps within the editor, ensuring lines fit + within the display without manual adjustments. +- Scrolling for long text entries. +- Backspace Support: Use the backspace key to delete characters to the left of + the cursor. +- Delete Character: :kbd:`Delete` deletes the character under the cursor. +- Delete Current Line: :kbd:`Ctrl` + :kbd:`U` deletes the entire current line + where the cursor is located. +- Delete Rest of Line: :kbd:`Ctrl` + :kbd:`K` deletes text from the cursor to + the end of the line. +- Delete Last Word: :kbd:`Ctrl` + :kbd:`W` removes the word immediately before + the cursor. +- Select All: Select entire text by :kbd:`Ctrl` + :kbd:`A`. +- Undo/Redo: Undo/Redo changes by :kbd:`Ctrl` + :kbd:`Z` / :kbd:`Ctrl` + + :kbd:`Y`. +- Clipboard Operations: Perform OS clipboard cut, copy, and paste operations on + selected text, allowing you to paste the copied content into other + applications. +- Copy Text: Use :kbd:`Ctrl` + :kbd:`C` to copy selected text. + - copy selected text, if available + - if no text is selected it copy the entire current line, including the + terminating newline if present +- Cut Text: Use :kbd:`Ctrl` + :kbd:`X` to cut selected text. + - cut selected text, if available + - if no text is selected it will cut the entire current line, including the + terminating newline if present +- Paste Text: Use :kbd:`Ctrl` + :kbd:`V` to paste text from the clipboard into + the editor. + - replace selected text, if available + - If no text is selected, paste text in the cursor position + Scrollbar class --------------- diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index cc6377c098..8a5daab7b2 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -28,6 +28,7 @@ FilteredList = require('gui.widgets.filtered_list') TabBar = require('gui.widgets.tab_bar') RangeSlider = require('gui.widgets.range_slider') DimensionsTooltip = require('gui.widgets.dimensions_tooltip') +TextArea = require('gui.widgets.text_area') Tab = TabBar.Tab makeButtonLabelText = Label.makeButtonLabelText diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua new file mode 100644 index 0000000000..c76a7d03e8 --- /dev/null +++ b/library/lua/gui/widgets/text_area.lua @@ -0,0 +1,183 @@ +-- Multiline text area control + +local Panel = require('gui.widgets.containers.panel') +local Scrollbar = require('gui.widgets.scrollbar') +local TextAreaContent = require('gui.widgets.text_area.text_area_content') +local HistoryStore = require('gui.widgets.text_area.history_store') + +local HISTORY_ENTRY = HistoryStore.HISTORY_ENTRY + +TextArea = defclass(TextArea, Panel) + +TextArea.ATTRS{ + init_text = '', + init_cursor = DEFAULT_NIL, + text_pen = COLOR_LIGHTCYAN, + ignore_keys = {}, + select_pen = COLOR_CYAN, + on_text_change = DEFAULT_NIL, + on_cursor_change = DEFAULT_NIL, + one_line_mode = false, + debug = false +} + +function TextArea:init() + self.render_start_line_y = 1 + + self.text_area = TextAreaContent{ + frame={l=0,r=3,t=0}, + text=self.init_text, + + text_pen=self.text_pen, + ignore_keys=self.ignore_keys, + select_pen=self.select_pen, + debug=self.debug, + one_line_mode=self.one_line_mode, + + on_text_change=function (text, old_text) + self:updateLayout() + if self.on_text_change then + self.on_text_change(text, old_text) + end + end, + on_cursor_change=self:callback('onCursorChange') + } + self.scrollbar = Scrollbar{ + frame={r=0,t=1}, + on_scroll=self:callback('onScrollbar'), + visible=not self.one_line_mode + } + + self:addviews{ + self.text_area, + self.scrollbar, + } +end + +function TextArea:getText() + return self.text_area.text +end + +function TextArea:setText(text) + self.text_area.history:store( + HISTORY_ENTRY.OTHER, + self:getText(), + self:getCursor() + ) + + return self.text_area:setText(text) +end + +function TextArea:getCursor() + return self.text_area.cursor +end + +function TextArea:setCursor(cursor_offset) + return self.text_area:setCursor(cursor_offset) +end + +function TextArea:clearHistory() + return self.text_area.history:clear() +end + +function TextArea:onCursorChange(cursor, old_cursor) + local x, y = self.text_area.wrapped_text:indexToCoords( + self.text_area.cursor + ) + + if y >= self.render_start_line_y + self.text_area.frame_body.height then + self:updateScrollbar( + y - self.text_area.frame_body.height + 1 + ) + elseif (y < self.render_start_line_y) then + self:updateScrollbar(y) + end + + if self.on_cursor_change then + self.on_cursor_change(cursor, old_cursor) + end +end + +function TextArea:scrollToCursor(cursor_offset) + if self.scrollbar.visible then + local _, cursor_line_y = self.text_area.wrapped_text:indexToCoords( + cursor_offset + ) + self:updateScrollbar(cursor_line_y) + end +end + +function TextArea:getPreferredFocusState() + return true +end + +function TextArea:postUpdateLayout() + self:updateScrollbar(self.render_start_line_y) + + if self.text_area.cursor == nil then + local cursor = self.init_cursor or #self.init_text + 1 + self.text_area:setCursor(cursor) + self:scrollToCursor(cursor) + end +end + +function TextArea:onScrollbar(scroll_spec) + local height = self.text_area.frame_body.height + + local render_start_line = self.render_start_line_y + if scroll_spec == 'down_large' then + render_start_line = render_start_line + math.ceil(height / 2) + elseif scroll_spec == 'up_large' then + render_start_line = render_start_line - math.ceil(height / 2) + elseif scroll_spec == 'down_small' then + render_start_line = render_start_line + 1 + elseif scroll_spec == 'up_small' then + render_start_line = render_start_line - 1 + else + render_start_line = tonumber(scroll_spec) + end + + self:updateScrollbar(render_start_line) +end + +function TextArea:updateScrollbar(scrollbar_current_y) + local lines_count = #self.text_area.wrapped_text.lines + + local render_start_line_y = math.min( + #self.text_area.wrapped_text.lines - self.text_area.frame_body.height + 1, + math.max(1, scrollbar_current_y) + ) + + self.scrollbar:update( + render_start_line_y, + self.frame_body.height, + lines_count + ) + + if (self.frame_body.height >= lines_count) then + render_start_line_y = 1 + end + + self.render_start_line_y = render_start_line_y + self.text_area:setRenderStartLineY(self.render_start_line_y) +end + +function TextArea:renderSubviews(dc) + self.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) + -- only visible lines of text_area will be rendered + TextArea.super.renderSubviews(self, dc) +end + +function TextArea:onInput(keys) + if (self.scrollbar.is_dragging) then + return self.scrollbar:onInput(keys) + end + + if keys._MOUSE_L and self:getMousePos() then + self:setFocus(true) + end + + return TextArea.super.onInput(self, keys) +end + +return TextArea diff --git a/library/lua/gui/widgets/text_area/history_store.lua b/library/lua/gui/widgets/text_area/history_store.lua new file mode 100644 index 0000000000..abaf2de3ee --- /dev/null +++ b/library/lua/gui/widgets/text_area/history_store.lua @@ -0,0 +1,86 @@ +HistoryStore = defclass(HistoryStore) + +local HISTORY_ENTRY = { + TEXT_BLOCK = 1, + WHITESPACE_BLOCK = 2, + BACKSPACE = 2, + DELETE = 3, + OTHER = 4 +} + +HistoryStore.ATTRS{ + history_size = 25, +} + +function HistoryStore:init() + self.past = {} + self.future = {} +end + +function HistoryStore:store(history_entry_type, text, cursor) + local last_entry = self.past[#self.past] + + if not last_entry or history_entry_type == HISTORY_ENTRY.OTHER or + last_entry.entry_type ~= history_entry_type then + table.insert(self.past, { + entry_type=history_entry_type, + text=text, + cursor=cursor + }) + end + + self.future = {} + + if #self.past > self.history_size then + table.remove(self.past, 1) + end +end + +function HistoryStore:undo(curr_text, curr_cursor) + if #self.past == 0 then + return nil + end + + local history_entry = table.remove(self.past, #self.past) + + table.insert(self.future, { + entry_type=HISTORY_ENTRY.OTHER, + text=curr_text, + cursor=curr_cursor + }) + + if #self.future > self.history_size then + table.remove(self.future, 1) + end + + return history_entry +end + +function HistoryStore:redo(curr_text, curr_cursor) + if #self.future == 0 then + return true + end + + local history_entry = table.remove(self.future, #self.future) + + table.insert(self.past, { + entry_type=HISTORY_ENTRY.OTHER, + text=curr_text, + cursor=curr_cursor + }) + + if #self.past > self.history_size then + table.remove(self.past, 1) + end + + return history_entry +end + +function HistoryStore:clear() + self.past = {} + self.future = {} +end + +HistoryStore.HISTORY_ENTRY = HISTORY_ENTRY + +return HistoryStore diff --git a/library/lua/gui/widgets/text_area/text_area_content.lua b/library/lua/gui/widgets/text_area/text_area_content.lua new file mode 100644 index 0000000000..48877ccaf7 --- /dev/null +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -0,0 +1,673 @@ +local gui = require('gui') +local common = require('gui.widgets.common') +local Widget = require('gui.widgets.widget') +local WrappedText = require('gui.widgets.text_area.wrapped_text') +local HistoryStore = require('gui.widgets.text_area.history_store') + +local CLIPBOARD_MODE = {LOCAL = 1, LINE = 2} +local HISTORY_ENTRY = HistoryStore.HISTORY_ENTRY + +TextAreaContent = defclass(TextAreaContent, Widget) + +TextAreaContent.ATTRS{ + text = '', + text_pen = COLOR_LIGHTCYAN, + ignore_keys = {}, + pen_selection = COLOR_CYAN, + on_text_change = DEFAULT_NIL, + on_cursor_change = DEFAULT_NIL, + enable_cursor_blink = true, + debug = false, + one_line_mode = false, + history_size = 25, +} + +function TextAreaContent:init() + self.sel_end = nil + self.clipboard = nil + self.clipboard_mode = CLIPBOARD_MODE.LOCAL + self.render_start_line_y = 1 + + self.cursor = nil + + self.main_pen = dfhack.pen.parse({ + fg=self.text_pen, + bg=COLOR_RESET, + bold=true + }) + self.sel_pen = dfhack.pen.parse({ + fg=self.text_pen, + bg=self.pen_selection, + bold=true + }) + + self.text = self:normalizeText(self.text) + + self.wrapped_text = WrappedText{ + text=self.text, + wrap_width=256 + } + + self.history = HistoryStore{history_size=self.history_size} +end + +function TextAreaContent:normalizeText(text) + if self.one_line_mode then + return text:gsub("\r?\n", "") + end + + return text +end + +function TextAreaContent:setRenderStartLineY(render_start_line_y) + self.render_start_line_y = render_start_line_y +end + +function TextAreaContent:postComputeFrame() + self:recomputeLines() +end + +function TextAreaContent:recomputeLines() + self.wrapped_text:update( + self.text, + -- something cursor '_' need to be add at the end of a line + self.frame_body.width - 1 + ) +end + +function TextAreaContent:setCursor(cursor_offset) + local old_cursor = self.cursor + + self.cursor = math.max( + 1, + math.min(#self.text + 1, cursor_offset) + ) + + if self.debug then + print('cursor', self.cursor) + end + + self.sel_end = nil + self.last_cursor_x = nil + + if self.on_cursor_change and self.cursor ~= old_cursor then + self.on_cursor_change(self.cursor, old_cursor) + end +end + +function TextAreaContent:setSelection(from_offset, to_offset) + -- text selection is always start on self.cursor and on self.sel_end + self:setCursor(from_offset) + self.sel_end = to_offset + + if self.debug and to_offset then + print('sel_end', to_offset) + end +end + +function TextAreaContent:hasSelection() + return not not self.sel_end +end + +function TextAreaContent:eraseSelection() + if (self:hasSelection()) then + local from, to = self.cursor, self.sel_end + if (from > to) then + from, to = to, from + end + + local new_text = self.text:sub(1, from - 1) .. self.text:sub(to + 1) + self:setText(new_text) + + self:setCursor(from) + self.sel_end = nil + end +end + +function TextAreaContent:setClipboard(text) + dfhack.internal.setClipboardTextCp437Multiline(text) +end + +function TextAreaContent:copy() + if self.sel_end then + self.clipboard_mode = CLIPBOARD_MODE.LOCAL + + local from = self.cursor + local to = self.sel_end + + if from > to then + from, to = to, from + end + + self:setClipboard(self.text:sub(from, to)) + + return from, to + else + self.clipboard_mode = CLIPBOARD_MODE.LINE + + local curr_line = self.text:sub( + self:lineStartOffset(), + self:lineEndOffset() + ) + if curr_line:sub(-1,-1) ~= NEWLINE then + curr_line = curr_line .. NEWLINE + end + + self:setClipboard(curr_line) + + return self:lineStartOffset(), self:lineEndOffset() + end +end + +function TextAreaContent:cut() + local from, to = self:copy() + if not self:hasSelection() then + self:setSelection(from, to) + end + self:eraseSelection() +end + +function TextAreaContent:paste() + local clipboard_lines = dfhack.internal.getClipboardTextCp437Multiline() + local clipboard = table.concat(clipboard_lines, '\n') + if clipboard then + if self.clipboard_mode == CLIPBOARD_MODE.LINE and not self:hasSelection() then + local origin_offset = self.cursor + self:setCursor(self:lineStartOffset()) + self:insert(clipboard) + self:setCursor(#clipboard + origin_offset) + else + self:eraseSelection() + self:insert(clipboard) + end + + end +end + +function TextAreaContent:setText(text) + local old_text = self.text + + self.text = self:normalizeText(text) + + self:recomputeLines() + + if self.on_text_change and text ~= old_text then + self.on_text_change(text, old_text) + end +end + +function TextAreaContent:insert(text) + self:eraseSelection() + local new_text = + self.text:sub(1, self.cursor - 1) .. + text .. + self.text:sub(self.cursor) + + self:setText(new_text) + self:setCursor(self.cursor + #text) +end + +function TextAreaContent:onRenderBody(dc) + dc:pen(self.main_pen) + + local max_width = dc.width + local new_line = self.debug and NEWLINE or '' + + local lines_to_render = math.min( + dc.height, + #self.wrapped_text.lines - self.render_start_line_y + 1 + ) + + dc:seek(0, self.render_start_line_y - 1) + for i = self.render_start_line_y, self.render_start_line_y + lines_to_render - 1 do + -- do not render new lines symbol + local line = self.wrapped_text.lines[i]:gsub(NEWLINE, new_line) + dc:string(line) + dc:newline() + end + + local show_focus = not self.enable_cursor_blink + or ( + not self:hasSelection() + and self.parent_view.focus + and gui.blink_visible(530) + ) + + if show_focus then + local x, y = self.wrapped_text:indexToCoords(self.cursor) + dc:seek(x - 1, y - 1) + :char('_') + end + + if self:hasSelection() then + local sel_new_line = self.debug and PERIOD or '' + local from, to = self.cursor, self.sel_end + if (from > to) then + from, to = to, from + end + + local from_x, from_y = self.wrapped_text:indexToCoords(from) + local to_x, to_y = self.wrapped_text:indexToCoords(to) + + local line = self.wrapped_text.lines[from_y] + :sub(from_x, to_y == from_y and to_x or nil) + :gsub(NEWLINE, sel_new_line) + + dc:pen(self.sel_pen) + :seek(from_x - 1, from_y - 1) + :string(line) + + for y = from_y + 1, to_y - 1 do + line = self.wrapped_text.lines[y]:gsub(NEWLINE, sel_new_line) + dc:seek(0, y - 1) + :string(line) + end + + if (to_y > from_y) then + local line = self.wrapped_text.lines[to_y] + :sub(1, to_x) + :gsub(NEWLINE, sel_new_line) + dc:seek(0, to_y - 1) + :string(line) + end + + dc:pen({fg=self.text_pen, bg=COLOR_RESET}) + end + + if self.debug then + local cursor_char = self:charAtCursor() + local x, y = self.wrapped_text:indexToCoords(self.cursor) + local debug_msg = string.format( + 'x: %s y: %s ind: %s #line: %s char: %s hist-: %s hist+: %s', + x, + y, + self.cursor, + self:lineEndOffset() - self:lineStartOffset(), + (cursor_char == NEWLINE and 'NEWLINE') or + (cursor_char == ' ' and 'SPACE') or + (cursor_char == '' and 'nil') or + cursor_char, + #self.history.past, + #self.history.future + ) + local sel_debug_msg = self.sel_end and string.format( + 'sel_end: %s', + self.sel_end + ) or '' + + dc:pen({fg=COLOR_LIGHTRED, bg=COLOR_RESET}) + :seek(0, self.parent_view.frame_body.height + self.render_start_line_y - 2) + :string(debug_msg) + :seek(0, self.parent_view.frame_body.height + self.render_start_line_y - 3) + :string(sel_debug_msg) + end +end + +function TextAreaContent:charAtCursor() + return self.text:sub(self.cursor, self.cursor) +end + +function TextAreaContent:getMultiLeftClick(x, y) + if self.last_click then + local from_last_click_ms = dfhack.getTickCount() - self.last_click.tick + + if ( + self.last_click.x ~= x or + self.last_click.y ~= y or + from_last_click_ms > common.DOUBLE_CLICK_MS + ) then + self.clicks_count = 0; + end + end + + return self.clicks_count or 0 +end + +function TextAreaContent:triggerMultiLeftClick(x, y) + local clicks_count = self:getMultiLeftClick(x, y) + + self.clicks_count = clicks_count + 1 + if (self.clicks_count >= 4) then + self.clicks_count = 1 + end + + self.last_click = { + tick=dfhack.getTickCount(), + x=x, + y=y, + } + return self.clicks_count +end + +function TextAreaContent:currentSpacesRange() + -- select "word" only from spaces + local prev_word_end, _ = self.text + :sub(1, self.cursor) + :find('[^%s]%s+$') + local _, next_word_start = self.text:find('%s[^%s]', self.cursor) + + return prev_word_end + 1 or 1, next_word_start - 1 or #self.text +end + +function TextAreaContent:currentWordRange() + -- select current word + local _, prev_word_end = self.text + :sub(1, self.cursor - 1) + :find('.*[%s,."\']') + local next_word_start, _ = self.text:find('[%s,."\']', self.cursor) + + return (prev_word_end or 0) + 1, (next_word_start or #self.text + 1) - 1 +end + +function TextAreaContent:lineStartOffset(offset) + local loc_offset = offset or self.cursor + return self.text:sub(1, loc_offset - 1):match(".*\n()") or 1 +end + +function TextAreaContent:lineEndOffset(offset) + local loc_offset = offset or self.cursor + return self.text:find("\n", loc_offset) or #self.text + 1 +end + +function TextAreaContent:wordStartOffset(offset) + return self.text + :sub(1, offset or self.cursor - 1) + :match('.*%s()[^%s]') or 1 +end + +function TextAreaContent:wordEndOffset(offset) + return self.text + :match( + '%s*[^%s]*()', + offset or self.cursor + ) or #self.text + 1 +end + +function TextAreaContent:onInput(keys) + for _,ignore_key in ipairs(self.ignore_keys) do + if keys[ignore_key] then + return false + end + end + + if self:onMouseInput(keys) then + return true + elseif self:onHistoryInput(keys) then + return true + elseif self:onTextManipulationInput(keys) then + return true + elseif self:onCursorInput(keys) then + return true + elseif keys.CUSTOM_CTRL_C then + self:copy() + return true + elseif keys.CUSTOM_CTRL_X then + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + self:cut() + return true + elseif keys.CUSTOM_CTRL_V then + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + self:paste() + return true + else + return TextAreaContent.super.onInput(self, keys) + end +end + +function TextAreaContent:onHistoryInput(keys) + if keys.CUSTOM_CTRL_Z then + local history_entry = self.history:undo(self.text, self.cursor) + + if history_entry then + self:setText(history_entry.text) + self:setCursor(history_entry.cursor) + end + + return true + elseif keys.CUSTOM_CTRL_Y then + local history_entry = self.history:redo(self.text, self.cursor) + + if history_entry then + self:setText(history_entry.text) + self:setCursor(history_entry.cursor) + end + + return true + end +end + +function TextAreaContent:onMouseInput(keys) + if keys._MOUSE_L then + local mouse_x, mouse_y = self:getMousePos() + if mouse_x and mouse_y then + + local clicks_count = self:triggerMultiLeftClick( + mouse_x + 1, + mouse_y + 1 + ) + if clicks_count == 3 then + self:setSelection( + self:lineStartOffset(), + self:lineEndOffset() + ) + elseif clicks_count == 2 then + local cursor_char = self:charAtCursor() + + local is_white_space = ( + cursor_char == ' ' or cursor_char == NEWLINE + ) + + local from, to + if is_white_space then + from, to = self:currentSpacesRange() + else + from, to = self:currentWordRange() + end + + self:setSelection(from, to) + else + self:setCursor(self.wrapped_text:coordsToIndex( + mouse_x + 1, + mouse_y + 1 + )) + end + + return true + end + + elseif keys._MOUSE_L_DOWN then + + local mouse_x, mouse_y = self:getMousePos() + if mouse_x and mouse_y then + if (self:getMultiLeftClick(mouse_x + 1, mouse_y + 1) > 1) then + return true + end + + local offset = self.wrapped_text:coordsToIndex( + mouse_x + 1, + mouse_y + 1 + ) + + if self.cursor ~= offset then + self:setSelection(self.cursor, offset) + else + self.sel_end = nil + end + + return true + end + end +end + +function TextAreaContent:onCursorInput(keys) + if keys.KEYBOARD_CURSOR_LEFT then + self:setCursor(self.cursor - 1) + return true + elseif keys.KEYBOARD_CURSOR_RIGHT then + self:setCursor(self.cursor + 1) + return true + elseif keys.KEYBOARD_CURSOR_UP then + local x, y = self.wrapped_text:indexToCoords(self.cursor) + local last_cursor_x = self.last_cursor_x or x + local offset = y > 1 and + self.wrapped_text:coordsToIndex(last_cursor_x, y - 1) or + 1 + self:setCursor(offset) + self.last_cursor_x = last_cursor_x + return true + elseif keys.KEYBOARD_CURSOR_DOWN then + local x, y = self.wrapped_text:indexToCoords(self.cursor) + local last_cursor_x = self.last_cursor_x or x + local offset = y < #self.wrapped_text.lines and + self.wrapped_text:coordsToIndex(last_cursor_x, y + 1) or + #self.text + 1 + self:setCursor(offset) + self.last_cursor_x = last_cursor_x + return true + elseif keys.CUSTOM_CTRL_HOME then + self:setCursor(1) + return true + elseif keys.CUSTOM_CTRL_END then + -- go to text end + self:setCursor(#self.text + 1) + return true + elseif keys.CUSTOM_CTRL_LEFT then + -- back one word + local word_start = self:wordStartOffset() + self:setCursor(word_start) + return true + elseif keys.CUSTOM_CTRL_RIGHT then + -- forward one word + local word_end = self:wordEndOffset() + self:setCursor(word_end) + return true + elseif keys.CUSTOM_HOME then + -- line start + self:setCursor( + self:lineStartOffset() + ) + return true + elseif keys.CUSTOM_END then + -- line end + self:setCursor( + self:lineEndOffset() + ) + return true + end +end + +function TextAreaContent:onTextManipulationInput(keys) + if keys.SELECT then + -- handle enter + if not self.one_line_mode then + self.history:store( + HISTORY_ENTRY.WHITESPACE_BLOCK, + self.text, + self.cursor + ) + self:insert(NEWLINE) + end + + return true + + elseif keys._STRING then + if keys._STRING == 0 then + -- handle backspace + self.history:store(HISTORY_ENTRY.BACKSPACE, self.text, self.cursor) + + if (self:hasSelection()) then + self:eraseSelection() + else + if (self.cursor == 1) then + return true + end + + self:setSelection( + self.cursor - 1, + self.cursor - 1 + ) + self:eraseSelection() + end + + else + local cv = string.char(keys._STRING) + + if (self:hasSelection()) then + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + self:eraseSelection() + else + local entry_type = cv == ' ' and HISTORY_ENTRY.WHITESPACE_BLOCK + or HISTORY_ENTRY.TEXT_BLOCK + self.history:store(entry_type, self.text, self.cursor) + end + + self:insert(cv) + end + + return true + elseif keys.CUSTOM_CTRL_A then + -- select all + self:setSelection(#self.text + 1, 1) + return true + elseif keys.CUSTOM_CTRL_U then + -- delete current line + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + + if (self:hasSelection()) then + -- delete all lines that has selection + self:setSelection( + self:lineStartOffset(self.cursor), + self:lineEndOffset(self.sel_end) + ) + self:eraseSelection() + else + self:setSelection( + self:lineStartOffset(), + self:lineEndOffset() + ) + self:eraseSelection() + end + + return true + elseif keys.CUSTOM_CTRL_K then + -- delete from cursor to end of current line + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + + local line_end = self:lineEndOffset(self.sel_end or self.cursor) - 1 + self:setSelection( + self.cursor, + math.max(line_end, self.cursor) + ) + self:eraseSelection() + + return true + elseif keys.CUSTOM_DELETE then + self.history:store(HISTORY_ENTRY.DELETE, self.text, self.cursor) + + if (self:hasSelection()) then + self:eraseSelection() + else + self:setText( + self.text:sub(1, self.cursor - 1) .. + self.text:sub(self.cursor + 1) + ) + end + + return true + elseif keys.CUSTOM_CTRL_W then + -- delete one word backward + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + + if not self:hasSelection() and self.cursor ~= 1 then + self:setSelection( + self:wordStartOffset(), + math.max(self.cursor - 1, 1) + ) + end + self:eraseSelection() + + return true + end +end + +return TextAreaContent diff --git a/library/lua/gui/widgets/text_area/wrapped_text.lua b/library/lua/gui/widgets/text_area/wrapped_text.lua new file mode 100644 index 0000000000..b5ff928458 --- /dev/null +++ b/library/lua/gui/widgets/text_area/wrapped_text.lua @@ -0,0 +1,70 @@ +-- This class caches lines of text wrapped to a specified width for performance +-- and readability. It can convert a given text index to (x, y) coordinates in +-- the wrapped text and vice versa. + +-- Usage: +-- This class should only be used in the following scenarios. +-- 1. When text or text features need to be rendered +-- (wrapped {x, y} coordinates are required). +-- 2. When mouse input needs to be converted to the original text position. + +-- Using this class in other scenarios may lead to issues with the component's +-- behavior when the text is wrapped. +WrappedText = defclass(WrappedText) + +WrappedText.ATTRS{ + text = '', + wrap_width = math.huge, +} + +function WrappedText:init() + self:update(self.text, self.wrap_width) +end + +function WrappedText:update(text, wrap_width) + self.lines = text:wrap( + wrap_width, + { + return_as_table=true, + keep_trailing_spaces=true, + keep_original_newlines=true + } + ) +end + +function WrappedText:coordsToIndex(x, y) + local offset = 0 + + local normalized_y = math.max( + 1, + math.min(y, #self.lines) + ) + + local line_bonus_length = normalized_y == #self.lines and 1 or 0 + local normalized_x = math.max( + 1, + math.min(x, #self.lines[normalized_y] + line_bonus_length) + ) + + for i=1, normalized_y - 1 do + offset = offset + #self.lines[i] + end + + return offset + normalized_x +end + +function WrappedText:indexToCoords(index) + local offset = index + + for y, line in ipairs(self.lines) do + local line_bonus_length = y == #self.lines and 1 or 0 + if offset <= #line + line_bonus_length then + return offset, y + end + offset = offset - #line + end + + return #self.lines[#self.lines] + 1, #self.lines +end + +return WrappedText diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua new file mode 100644 index 0000000000..0223b05282 --- /dev/null +++ b/test/library/gui/widgets.TextArea.lua @@ -0,0 +1,3317 @@ +local gui = require('gui') +local widgets = require('gui.widgets') + +config.target = 'core' + +local function simulate_input_keys(...) + local keys = {...} + for _,key in ipairs(keys) do + gui.simulateInput(dfhack.gui.getCurViewscreen(true), key) + end +end + +local function simulate_input_text(text) + local screen = dfhack.gui.getCurViewscreen(true) + + for i = 1, #text do + local charcode = string.byte(text:sub(i,i)) + local code_key = string.format('STRING_A%03d', charcode) + + gui.simulateInput(screen, { [code_key]=true }) + end +end + +local function simulate_mouse_click(element, x, y) + local screen = dfhack.gui.getCurViewscreen(true) + + local g_x, g_y = element.frame_body:globalXY(x, y) + df.global.gps.mouse_x = g_x + df.global.gps.mouse_y = g_y + + if not element.frame_body:inClipGlobalXY(g_x, g_y) then + print('--- Click outside provided element area, re-check the test') + return + end + + gui.simulateInput(screen, { + _MOUSE_L=true, + _MOUSE_L_DOWN=true, + }) + gui.simulateInput(screen, '_MOUSE_L_DOWN') +end + +local function simulate_mouse_drag(element, x_from, y_from, x_to, y_to) + local g_x_from, g_y_from = element.frame_body:globalXY(x_from, y_from) + local g_x_to, g_y_to = element.frame_body:globalXY(x_to, y_to) + + df.global.gps.mouse_x = g_x_from + df.global.gps.mouse_y = g_y_from + + gui.simulateInput(dfhack.gui.getCurViewscreen(true), { + _MOUSE_L=true, + _MOUSE_L_DOWN=true, + }) + gui.simulateInput(dfhack.gui.getCurViewscreen(true), '_MOUSE_L_DOWN') + + df.global.gps.mouse_x = g_x_to + df.global.gps.mouse_y = g_y_to + gui.simulateInput(dfhack.gui.getCurViewscreen(true), '_MOUSE_L_DOWN') +end + +local function arrange_textarea(options) + options = options or {} + + local window_width = 50 + local window_height = 50 + + if options.w then + local border_width = 2 + local scrollbar_width = 3 + local cursor_buffor = 1 + window_width = options.w + border_width + scrollbar_width + cursor_buffor + end + + if options.h then + local border_width = 2 + window_height = options.h + border_width + end + + local screen = gui.ZScreen{} + + screen:addviews({ + widgets.Window{ + view_id='window', + resizable=true, + frame={w=window_width, h=window_height}, + frame_inset=0, + subviews={ + widgets.TextArea{ + view_id='text_area_widget', + init_text=options.text or '', + init_cursor=options.cursor or 1, + frame={l=0,r=0,t=0,b=0}, + on_cursor_change=options.on_cursor_change, + on_text_change=options.on_text_change, + } + } + } + }) + + local window = screen.subviews.window + local text_area = screen.subviews.text_area_widget.text_area + text_area.enable_cursor_blink = false + + screen:show() + screen:onRender() + + return text_area, screen, window, screen.subviews.text_area_widget +end + +local function read_rendered_text(text_area) + text_area.parent_view.parent_view.parent_view:onRender() + + local pen = nil + local text = '' + + local frame_body = text_area.frame_body + + for y=frame_body.clip_y1,frame_body.clip_y2 do + + for x=frame_body.clip_x1,frame_body.clip_x2 do + pen = dfhack.screen.readTile(x, y) + + if pen == nil or pen.ch == nil or pen.ch == 0 or pen.fg == 0 then + break + else + text = text .. string.char(pen.ch) + end + end + + text = text .. '\n' + end + + return text:gsub("\n+$", "") +end + +local function read_selected_text(text_area) + text_area.parent_view.parent_view.parent_view:onRender() + + local pen = nil + local text = '' + + for y=0,text_area.frame_body.height do + local has_sel = false + + for x=0,text_area.frame_body.width do + local g_x, g_y = text_area.frame_body:globalXY(x, y) + pen = dfhack.screen.readTile(g_x, g_y) + + local pen_char = string.char(pen.ch) + if pen == nil or pen.ch == nil or pen.ch == 0 then + break + elseif pen.bg == COLOR_CYAN then + has_sel = true + text = text .. pen_char + end + end + if has_sel then + text = text .. '\n' + end + end + + return text:gsub("\n+$", "") +end + +function test.load() + local text_area, screen = arrange_textarea() + + expect.eq(read_rendered_text(text_area), '_') + + screen:dismiss() +end + +function test.load_input_multiline_text() + local text_area, screen, window = arrange_textarea({w=80}) + + local text = table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Pellentesque dignissim volutpat orci, sed molestie metus elementum vel.', + 'Donec sit amet mattis ligula, ac vestibulum lorem.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), text .. '_') + + screen:dismiss() +end + +function test.handle_numpad_numbers_as_text() + local text_area, screen, window = arrange_textarea({w=80}) + + local text = table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + simulate_input_text(text) + + simulate_input_keys({ + STANDARDSCROLL_LEFT = true, + KEYBOARD_CURSOR_LEFT = true, + _STRING = 52, + STRING_A052 = true, + }) + + expect.eq(read_rendered_text(text_area), text .. '4_') + + simulate_input_keys({ + STRING_A054 = true, + STANDARDSCROLL_RIGHT = true, + KEYBOARD_CURSOR_RIGHT = true, + _STRING = 54, + }) + + expect.eq(read_rendered_text(text_area), text .. '46_') + + simulate_input_keys({ + KEYBOARD_CURSOR_DOWN = true, + STRING_A050 = true, + _STRING = 50, + STANDARDSCROLL_DOWN = true, + }) + + expect.eq(read_rendered_text(text_area), text .. '462_') + + simulate_input_keys({ + KEYBOARD_CURSOR_UP = true, + STRING_A056 = true, + STANDARDSCROLL_UP = true, + _STRING = 56, + }) + + expect.eq(read_rendered_text(text_area), text .. '4628_') + screen:dismiss() +end + +function test.wrap_text_to_available_width() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac._', + }, '\n')); + + screen:dismiss() +end + +function test.submit_new_line() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('SELECT') + + simulate_input_keys('SELECT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '', + '_', + }, '\n')); + + text_area:setCursor(58) + + simulate_input_keys('SELECT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'el', + '_t.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + -- empty end lines are not rendered + }, '\n')); + + text_area:setCursor(84) + + simulate_input_keys('SELECT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'el', + 'it.', + '112: Sed consectetur,', + -- wrapping changed + '_urna sit amet aliquet egestas, ante nibh porttitor ', + 'mi, vitae rutrum eros metus nec libero.', + -- empty end lines are not rendered + }, '\n')); + + screen:dismiss() +end + +function test.submit_new_line_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=55, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + expect.table_eq(cursor_change, {new=#text + 1, old=#text}) + + simulate_input_keys('SELECT') + expect.table_eq(cursor_change, {new=#text + 2, old=#text + 1}) + + expect.table_eq(text_change, { + new=table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '' + }, '\n'), + old=text + }) + + screen:dismiss() +end + +function test.keyboard_arrow_up_navigation() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('KEYBOARD_CURSOR_UP') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim _uismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_UP') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim li_ero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_UP') + simulate_input_keys('KEYBOARD_CURSOR_UP') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero._', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_UP') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor _i, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_UP') + simulate_input_keys('KEYBOARD_CURSOR_UP') + simulate_input_keys('KEYBOARD_CURSOR_UP') + simulate_input_keys('KEYBOARD_CURSOR_UP') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur_ urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + screen:dismiss() +end + +function test.keyboard_arrow_up_navigation_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=55, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', + }, '\n') + + simulate_input_text(text) + expect.table_eq(cursor_change, {new=#text + 1, old=#text}) + + text_change = {old=nil, new=nil} + simulate_input_keys('KEYBOARD_CURSOR_UP') + expect.table_eq(text_change, {new=nil, old=nil}) + + expect.table_eq(cursor_change, {new=284, old=#text + 1}) + + screen:dismiss() +end + +function test.keyboard_arrow_down_navigation() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', + }, '\n') + + simulate_input_text(text) + text_area:setCursor(11) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem _psum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit._', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed c_nsectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellen_esque ac.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac._', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_UP') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin _ignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + screen:dismiss() +end + +function test.keyboard_arrow_down_navigation_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=55, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(11) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem _psum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + '41: Etiam id congue urna, vel aliquet mi.', + '45: Nam dignissim libero a interdum porttitor.', + '73: Proin dignissim euismod augue, laoreet porttitor ', + 'est pellentesque ac.', + }, '\n')); + + text_change = {old=nil, new=nil} + + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + + expect.table_eq(cursor_change, {new=61, old=11}) + expect.table_eq(text_change, {new=nil, old=nil}) + + screen:dismiss() +end + +function test.keyboard_arrow_left_navigation() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero_', + }, '\n')); + + for i=1,6 do + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + '_ibero.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec_', + 'libero.', + }, '\n')); + + for i=1,105 do + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit._', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,60 do + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + screen:dismiss() +end + +function test.keyboard_arrow_left_navigation_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=55, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + expect.table_eq(cursor_change, {new=#text + 1, old=#text}) + + text_change = {old=nil, new=nil} + + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + expect.table_eq(cursor_change, {new=#text, old=#text + 1}) + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + expect.table_eq(cursor_change, {new=#text - 1, old=#text}) + + expect.table_eq(text_change, {new=nil, old=nil}) + + screen:dismiss() +end + +function test.keyboard_arrow_right_navigation() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(1) + + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '6_: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,53 do + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing_', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + '_lit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,5 do + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit._', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,113 do + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero._', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero._', + }, '\n')); + + screen:dismiss() +end + +function test.keyboard_arrow_right_navigation_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=55, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + expect.table_eq(cursor_change, {new=#text + 1, old=#text}) + + text_area:setCursor(1) + expect.table_eq(cursor_change, {new=1, old=#text + 1}) + + text_change = {old=nil, new=nil} + + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + expect.table_eq(cursor_change, {new=2, old=1}) + + expect.table_eq(text_change, {new=nil, old=nil}) + + screen:dismiss() +end + +function test.handle_backspace() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('STRING_A000') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero_', + }, '\n')); + + for i=1,3 do + simulate_input_keys('STRING_A000') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec lib_', + }, '\n')); + + text_area:setCursor(62) + + simulate_input_keys('STRING_A000') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit._12: Sed consectetur, urna sit amet aliquet ', + 'egestas, ante nibh porttitor mi, vitae rutrum eros ', + 'metus nec lib', + }, '\n')); + + text_area:setCursor(2) + + simulate_input_keys('STRING_A000') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.112: Sed consectetur, urna sit amet aliquet ', + 'egestas, ante nibh porttitor mi, vitae rutrum eros ', + 'metus nec lib', + }, '\n')); + + simulate_input_keys('STRING_A000') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.112: Sed consectetur, urna sit amet aliquet ', + 'egestas, ante nibh porttitor mi, vitae rutrum eros ', + 'metus nec lib', + }, '\n')); + + screen:dismiss() +end + +function test.handle_backspace_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=55, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + expect.table_eq(cursor_change, {new=#text + 1, old=#text}) + + simulate_input_keys('STRING_A000') + expect.table_eq(cursor_change, {new=#text, old=#text + 1}) + + expect.table_eq(text_change, {new=text:sub(1, -2), old=text}) + + screen:dismiss() +end + +function test.handle_delete() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(1) + + simulate_input_keys('CUSTOM_DELETE') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + text_area:setCursor(124) + simulate_input_keys('CUSTOM_DELETE') + + expect.eq(read_rendered_text(text_area), table.concat({ + '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + '_rttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + text_area:setCursor(123) + simulate_input_keys('CUSTOM_DELETE') + + expect.eq(read_rendered_text(text_area), table.concat({ + '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante ', + 'nibh_rttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + text_area:setCursor(171) + simulate_input_keys('CUSTOM_DELETE') + + expect.eq(read_rendered_text(text_area), table.concat({ + '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante ', + 'nibhorttitor mi, vitae rutrum eros metus nec libero._0: Lorem ', + 'ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + for i=1,59 do + simulate_input_keys('CUSTOM_DELETE') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante ', + 'nibhorttitor mi, vitae rutrum eros metus nec libero._', + }, '\n')); + + simulate_input_keys('CUSTOM_DELETE') + + expect.eq(read_rendered_text(text_area), table.concat({ + '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante ', + 'nibhorttitor mi, vitae rutrum eros metus nec libero._', + }, '\n')); + + screen:dismiss() +end + +function test.handle_delete_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=65, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(1) + + cursor_change = {old=nil, new=nil} + + simulate_input_keys('CUSTOM_DELETE') + + expect.table_eq(cursor_change, {new=nil, old=nil}) + expect.table_eq(text_change, {new=text:sub(2), old=text}) + + screen:dismiss() +end + +function test.line_end() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + text_area:setCursor(1) + + simulate_input_keys('CUSTOM_END') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + text_area:setCursor(70) + + simulate_input_keys('CUSTOM_END') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + text_area:setCursor(200) + + simulate_input_keys('CUSTOM_END') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + simulate_input_keys('CUSTOM_END') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + screen:dismiss() +end + +function test.line_end_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=65, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + text_area:setCursor(1) + expect.table_eq(cursor_change, {new=1, old=#text + 1}) + + text_change = {old=nil, new=nil} + + simulate_input_keys('CUSTOM_END') + expect.table_eq(cursor_change, {new=61, old=1}) + expect.table_eq(text_change, {new=nil, old=nil}) + + screen:dismiss() +end + +function test.line_beging() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_HOME') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + text_area:setCursor(173) + + simulate_input_keys('CUSTOM_HOME') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '_12: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + text_area:setCursor(1) + + simulate_input_keys('CUSTOM_HOME') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + screen:dismiss() +end + +function test.line_beging_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=65, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + expect.table_eq(cursor_change, {new=#text + 1, old=#text}) + + text_change = {old=nil, new=nil} + + simulate_input_keys('CUSTOM_HOME') + expect.table_eq(cursor_change, {new=#text + 1 - 60, old=#text + 1}) + expect.table_eq(text_change, {new=nil, old=nil}) + + screen:dismiss() +end + +function test.line_delete() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(65) + + simulate_input_keys('CUSTOM_CTRL_U') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_U') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '_' + }, '\n')); + + text_area:setCursor(1) + + simulate_input_keys('CUSTOM_CTRL_U') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_' + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_U') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_' + }, '\n')); + + screen:dismiss() +end + +function test.line_delete_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=65, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(65) + expect.table_eq(cursor_change, {new=65, old=#text + 1}) + + simulate_input_keys('CUSTOM_CTRL_U') + expect.table_eq(cursor_change, {new=62, old=65}) + + expect.table_eq(text_change, { + new=table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n'), + old=text + }) + + screen:dismiss() +end + +function test.line_delete_to_end() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(70) + + simulate_input_keys('CUSTOM_CTRL_K') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed_', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_K') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + }, '\n')); + + screen:dismiss() +end + +function test.line_delete_to_end_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=65, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(70) + + cursor_change = {old=nil, new=nil} + + simulate_input_keys('CUSTOM_CTRL_K') + printall(cursor_change) + expect.table_eq(cursor_change, {new=nil, old=nil}) + + expect.table_eq(text_change, { + new=table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n'), + old=text + }) + + screen:dismiss() +end + +function test.delete_last_word() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_W') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing _', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_W') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur _', + }, '\n')); + + text_area:setCursor(82) + + simulate_input_keys('CUSTOM_CTRL_W') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed _ urna sit amet aliquet egestas, ante nibh porttitor ', + 'mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur ', + }, '\n')); + + text_area:setCursor(37) + + simulate_input_keys('CUSTOM_CTRL_W') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, _ctetur adipiscing elit.', + '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', + 'mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur ', + }, '\n')); + + for i=1,6 do + simulate_input_keys('CUSTOM_CTRL_W') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '_ctetur adipiscing elit.', + '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', + 'mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur ', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_W') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_ctetur adipiscing elit.', + '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', + 'mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur ', + }, '\n')); + + screen:dismiss() +end + +function test.delete_last_word_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=65, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + expect.table_eq(cursor_change, {new=#text + 1, old=#text}) + + simulate_input_keys('CUSTOM_CTRL_W') + expect.table_eq(cursor_change, {new=#text + 1 - 5, old=#text + 1}) + + expect.table_eq(text_change, { + new=text:sub(1, -6), + old=text + }) + + screen:dismiss() +end + +function test.jump_to_text_end() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(1) + + simulate_input_keys('CUSTOM_CTRL_END') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_END') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + screen:dismiss() +end + +function test.jump_to_text_end_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=65, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + text_area:setCursor(1) + expect.table_eq(cursor_change, {new=1, old=#text + 1}) + + text_change = {old=nil, new=nil} + + simulate_input_keys('CUSTOM_CTRL_END') + expect.table_eq(cursor_change, {new=#text +1, old=1}) + expect.table_eq(text_change, {new=nil, old=nil}) + + screen:dismiss() +end + +function test.jump_to_text_begin() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_HOME') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_HOME') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + screen:dismiss() +end + +function test.jump_to_text_begin_callbacks() + local cursor_change = {old=nil, new=nil} + local text_change = {old=nil, new=nil} + local text_area, screen, window = arrange_textarea({ + w=65, + on_cursor_change=function (_cursor, _old_cursor) + cursor_change = {old=_old_cursor, new=_cursor} + end, + on_text_change=function (_text, _old_text) + text_change = {old=_old_text, new=_text} + end, + }) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + expect.table_eq(cursor_change, {new=#text + 1, old=#text}) + + text_change = {old=nil, new=nil} + + simulate_input_keys('CUSTOM_CTRL_HOME') + expect.table_eq(cursor_change, {new=1, old=#text + 1}) + expect.table_eq(text_change, {new=nil, old=nil}) + + screen:dismiss() +end + +function test.select_all() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_selected_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + screen:dismiss() +end + +function test.text_key_replace_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 9, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + expect.eq(read_selected_text(text_area), 'Lorem '); + + simulate_input_text('+') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: +_psum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + simulate_mouse_drag(text_area, 6, 1, 6, 2) + + simulate_input_text('!') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: +ipsum dolor sit amet, consectetur adipiscing elit.', + '112: S!_r mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + simulate_mouse_drag(text_area, 3, 1, 6, 2) + + simulate_input_text('@') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: +ipsum dolor sit amet, consectetur adipiscing elit.', + '112@_m ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + screen:dismiss() +end + +function test.arrows_reset_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + expect.eq(read_selected_text(text_area), '') + + simulate_input_keys('CUSTOM_CTRL_A') + + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + expect.eq(read_selected_text(text_area), '') + + simulate_input_keys('CUSTOM_CTRL_A') + + simulate_input_keys('KEYBOARD_CURSOR_UP') + expect.eq(read_selected_text(text_area), '') + + simulate_input_keys('CUSTOM_CTRL_A') + + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + expect.eq(read_selected_text(text_area), '') + + screen:dismiss() +end + +function test.click_reset_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + simulate_mouse_click(text_area, 4, 0) + expect.eq(read_selected_text(text_area), '') + + simulate_input_keys('CUSTOM_CTRL_A') + + simulate_mouse_click(text_area, 4, 8) + expect.eq(read_selected_text(text_area), '') + + screen:dismiss() +end + +function test.line_navigation_reset_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + simulate_input_keys('CUSTOM_HOME') + expect.eq(read_selected_text(text_area), '') + + simulate_input_keys('CUSTOM_END') + expect.eq(read_selected_text(text_area), '') + + screen:dismiss() +end + +function test.jump_begin_or_end_reset_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_HOME') + expect.eq(read_selected_text(text_area), '') + + simulate_input_keys('CUSTOM_CTRL_END') + expect.eq(read_selected_text(text_area), '') + + screen:dismiss() +end + +function test.new_line_override_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 29, 2) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum ero', + }, '\n')); + + simulate_input_keys('SELECT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: ', + '_ metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + screen:dismiss() +end + +function test.backspace_delete_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 29, 2) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum ero', + }, '\n')); + + simulate_input_keys('STRING_A000') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: _ metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + screen:dismiss() +end + +function test.delete_char_delete_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 29, 2) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum ero', + }, '\n')); + + simulate_input_keys('CUSTOM_DELETE') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: _ metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + screen:dismiss() +end + +function test.delete_line_delete_selection_lines() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 9, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + expect.eq(read_selected_text(text_area), 'Lorem '); + + simulate_input_keys('CUSTOM_CTRL_U') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_12: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + simulate_mouse_drag(text_area, 4, 1, 29, 2) + + simulate_input_keys('CUSTOM_CTRL_U') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_1: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + screen:dismiss() +end + +function test.delete_line_rest_delete_selection_lines() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 9, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + expect.eq(read_selected_text(text_area), 'Lorem '); + + simulate_input_keys('CUSTOM_CTRL_K') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: _', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + simulate_mouse_drag(text_area, 6, 1, 6, 2) + + simulate_input_keys('CUSTOM_CTRL_K') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: ', + '112: S_', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + simulate_mouse_drag(text_area, 3, 1, 6, 2) + + simulate_input_keys('CUSTOM_CTRL_K') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: ', + '112_', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + screen:dismiss() +end + +function test.delete_last_word_delete_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 9, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + expect.eq(read_selected_text(text_area), 'Lorem '); + + simulate_input_keys('CUSTOM_CTRL_W') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: _psum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + simulate_mouse_drag(text_area, 6, 1, 6, 2) + + simulate_input_keys('CUSTOM_CTRL_W') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: ipsum dolor sit amet, consectetur adipiscing elit.', + '112: S_r mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + simulate_mouse_drag(text_area, 3, 1, 6, 2) + + simulate_input_keys('CUSTOM_CTRL_W') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: ipsum dolor sit amet, consectetur adipiscing elit.', + '112_m ipsum dolor sit amet, consectetur adipiscing elit.', + '51: Sed consectetur, urna sit amet aliquet egestas.', + }, '\n')); + + screen:dismiss() +end + +function test.single_mouse_click_set_cursor() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_click(text_area, 4, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: _orem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 40, 2) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus ne_ libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 49, 2) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 60, 2) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 0, 10) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 21, 10) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor_sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 63, 10) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + screen:dismiss() +end + +function test.double_mouse_click_select_word() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + simulate_mouse_click(text_area, 0, 0) + simulate_mouse_click(text_area, 0, 0) + + expect.eq(read_selected_text(text_area), '60:') + + simulate_mouse_click(text_area, 4, 0) + simulate_mouse_click(text_area, 4, 0) + + expect.eq(read_selected_text(text_area), 'Lorem') + + simulate_mouse_click(text_area, 40, 2) + simulate_mouse_click(text_area, 40, 2) + + expect.eq(read_selected_text(text_area), 'nec') + + simulate_mouse_click(text_area, 58, 3) + simulate_mouse_click(text_area, 58, 3) + expect.eq(read_selected_text(text_area), 'elit') + + simulate_mouse_click(text_area, 60, 3) + simulate_mouse_click(text_area, 60, 3) + expect.eq(read_selected_text(text_area), '.') + + screen:dismiss() +end + +function test.double_mouse_click_select_white_spaces() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = 'Lorem ipsum dolor sit amet, consectetur elit.' + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_mouse_click(text_area, 29, 0) + simulate_mouse_click(text_area, 29, 0) + + expect.eq(read_selected_text(text_area), ' ') + + screen:dismiss() +end + +function test.triple_mouse_click_select_line() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + simulate_mouse_click(text_area, 0, 0) + simulate_mouse_click(text_area, 0, 0) + simulate_mouse_click(text_area, 0, 0) + + expect.eq( + read_selected_text(text_area), + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + ) + + simulate_mouse_click(text_area, 4, 0) + simulate_mouse_click(text_area, 4, 0) + simulate_mouse_click(text_area, 4, 0) + + expect.eq( + read_selected_text(text_area), + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + ) + + simulate_mouse_click(text_area, 40, 2) + simulate_mouse_click(text_area, 40, 2) + simulate_mouse_click(text_area, 40, 2) + + expect.eq(read_selected_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + simulate_mouse_click(text_area, 58, 3) + simulate_mouse_click(text_area, 58, 3) + simulate_mouse_click(text_area, 58, 3) + + expect.eq( + read_selected_text(text_area), + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + ) + + simulate_mouse_click(text_area, 60, 3) + simulate_mouse_click(text_area, 60, 3) + simulate_mouse_click(text_area, 60, 3) + + expect.eq( + read_selected_text(text_area), + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + ) + + screen:dismiss() +end + +function test.mouse_selection_control() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 29, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + expect.eq(read_selected_text(text_area), 'Lorem ipsum dolor sit amet') + + simulate_mouse_drag(text_area, 0, 0, 29, 0) + + expect.eq(read_selected_text(text_area), '60: Lorem ipsum dolor sit amet') + + simulate_mouse_drag(text_area, 32, 0, 32, 1) + + expect.eq(read_selected_text(text_area), table.concat({ + 'consectetur adipiscing elit.', + '112: Sed consectetur, urna sit am' + }, '\n')); + + simulate_mouse_drag(text_area, 32, 1, 48, 2) + + expect.eq(read_selected_text(text_area), table.concat({ + 'met aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + simulate_mouse_drag(text_area, 42, 2, 59, 3) + + expect.eq(read_selected_text(text_area), table.concat({ + 'libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, '\n')); + + simulate_mouse_drag(text_area, 42, 2, 65, 3) + + expect.eq(read_selected_text(text_area), table.concat({ + 'libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, '\n')); + + simulate_mouse_drag(text_area, 42, 2, 65, 6) + + expect.eq(read_selected_text(text_area), table.concat({ + 'libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + }, '\n')); + + simulate_mouse_drag(text_area, 42, 2, 42, 6) + + expect.eq(read_selected_text(text_area), table.concat({ + 'libero.', + '60: Lorem ipsum dolor sit amet, consectetur' + }, '\n')); + + screen:dismiss() +end + +function test.copy_and_paste_text_line() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_C') + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + simulate_mouse_click(text_area, 15, 3) + simulate_input_keys('CUSTOM_CTRL_C') + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '60: Lorem ipsum_dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 5, 0) + simulate_input_keys('CUSTOM_CTRL_C') + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '112: _ed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 6, 0) + simulate_input_keys('CUSTOM_CTRL_C') + simulate_mouse_click(text_area, 5, 6) + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: L_rem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + screen:dismiss() +end + +function test.copy_and_paste_selected_text() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 8, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + expect.eq(read_selected_text(text_area), 'Lorem') + + simulate_input_keys('CUSTOM_CTRL_C') + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem_ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 4, 2) + + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'portLorem_itor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 0, 0) + + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Lorem_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'portLoremtitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 60, 4) + + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Lorem60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'portLoremtitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem_', + }, '\n')); + + screen:dismiss() +end + +function test.cut_and_paste_text_line() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_X') + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '_', + }, '\n')); + + simulate_mouse_click(text_area, 0, 0) + simulate_input_keys('CUSTOM_CTRL_X') + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_click(text_area, 60, 2) + simulate_input_keys('CUSTOM_CTRL_X') + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '_', + }, '\n')); + + screen:dismiss() +end + +function test.cut_and_paste_selected_text() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + + simulate_mouse_drag(text_area, 4, 0, 8, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + expect.eq(read_selected_text(text_area), 'Lorem') + + simulate_input_keys('CUSTOM_CTRL_X') + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem_ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_drag(text_area, 4, 0, 8, 0) + simulate_input_keys('CUSTOM_CTRL_X') + + simulate_mouse_click(text_area, 4, 2) + + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'portLorem_itor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_drag(text_area, 5, 2, 8, 2) + simulate_input_keys('CUSTOM_CTRL_X') + + simulate_mouse_click(text_area, 0, 0) + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + 'orem_0: ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'portLtitor mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + simulate_mouse_drag(text_area, 5, 2, 8, 2) + simulate_input_keys('CUSTOM_CTRL_X') + + simulate_mouse_click(text_area, 60, 4) + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), table.concat({ + 'orem60: ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'portLr mi, vitae rutrum eros metus nec libero.', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.tito_', + }, '\n')); + + screen:dismiss() +end + +function test.scroll_long_text() + local text_area, screen, window, widget = arrange_textarea({w=100, h=10}) + local scrollbar = widget.scrollbar + + local text = table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Nulla ut lacus ut tortor semper consectetur.', + 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', + 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', + 'Sed in libero eget velit condimentum lacinia ut quis dui.', + 'Praesent sollicitudin dui ac mollis lacinia.', + 'Ut gravida tortor ac accumsan suscipit.', + '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', + 'Sed in libero eget velit condimentum lacinia ut quis dui.', + 'Praesent sollicitudin dui ac mollis lacinia.', + 'Ut gravida tortor ac accumsan suscipit.', + '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', + }, '\n')) + + simulate_mouse_click(scrollbar, 0, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', + 'Sed in libero eget velit condimentum lacinia ut quis dui.', + 'Praesent sollicitudin dui ac mollis lacinia.', + 'Ut gravida tortor ac accumsan suscipit.', + }, '\n')) + + simulate_mouse_click(scrollbar, 0, 0) + simulate_mouse_click(scrollbar, 0, 0) + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Sed euismod odio eu fringilla bibendum.', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', + 'Sed in libero eget velit condimentum lacinia ut quis dui.', + }, '\n')) + + simulate_mouse_click(scrollbar, 0, scrollbar.frame_body.height - 2) + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', + 'Sed in libero eget velit condimentum lacinia ut quis dui.', + 'Praesent sollicitudin dui ac mollis lacinia.', + 'Ut gravida tortor ac accumsan suscipit.', + '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', + }, '\n')) + + simulate_mouse_click(scrollbar, 0, 2) + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + }, '\n')) + + screen:dismiss() +end + +function test.scroll_follows_cursor() + local text_area, screen, window = arrange_textarea({w=100, h=10}) + local scrollbar = window.subviews.text_area_scrollbar + + local text = table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Nulla ut lacus ut tortor semper consectetur.', + 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', + 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', + 'Sed in libero eget velit condimentum lacinia ut quis dui.', + 'Praesent sollicitudin dui ac mollis lacinia.', + 'Ut gravida tortor ac accumsan suscipit.', + '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex.', + }, '\n') + + simulate_input_text(text) + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', + 'Sed in libero eget velit condimentum lacinia ut quis dui.', + 'Praesent sollicitudin dui ac mollis lacinia.', + 'Ut gravida tortor ac accumsan suscipit.', + '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', + }, '\n')) + + simulate_mouse_click(text_area, 0, 8) + simulate_input_keys('KEYBOARD_CURSOR_UP') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_nteger tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', + 'Sed in libero eget velit condimentum lacinia ut quis dui.', + 'Praesent sollicitudin dui ac mollis lacinia.', + 'Ut gravida tortor ac accumsan suscipit.', + }, '\n')) + + simulate_input_keys('CUSTOM_CTRL_HOME') + + simulate_mouse_click(text_area, 0, 9) + simulate_input_keys('KEYBOARD_CURSOR_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Nulla ut lacus ut tortor semper consectetur.', + 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', + 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '_onec quis lectus ac erat placerat eleifend.', + }, '\n')) + + simulate_mouse_click(text_area, 44, 10) + simulate_input_keys('KEYBOARD_CURSOR_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', + 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + '_enean non orci id erat malesuada pharetra.', + }, '\n')) + + simulate_mouse_click(text_area, 0, 2) + simulate_input_keys('KEYBOARD_CURSOR_LEFT') + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Nulla ut lacus ut tortor semper consectetur._', + 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', + 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + 'Donec quis lectus ac erat placerat eleifend.', + }, '\n')) + + screen:dismiss() +end + +function test.fast_rewind_words_right() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + text_area:setCursor(1) + + simulate_input_keys('CUSTOM_CTRL_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60:_Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem_ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,6 do + simulate_input_keys('CUSTOM_CTRL_RIGHT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing_', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit._', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112:_Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,17 do + simulate_input_keys('CUSTOM_CTRL_RIGHT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero._', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_RIGHT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero._', + }, '\n')); + + screen:dismiss() +end + +function test.fast_rewind_words_left() + local text_area, screen, window = arrange_textarea({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_LEFT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + '_ibero.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_LEFT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus _ec ', + 'libero.', + }, '\n')); + + for i=1,8 do + simulate_input_keys('CUSTOM_CTRL_LEFT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + '_nte nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_LEFT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet _gestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,16 do + simulate_input_keys('CUSTOM_CTRL_LEFT') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_LEFT') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + screen:dismiss() +end + +function test.fast_rewind_reset_selection() + local text_area, screen, window = arrange_textarea({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero._', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + simulate_input_keys('CUSTOM_CTRL_LEFT') + expect.eq(read_selected_text(text_area), '') + + simulate_input_keys('CUSTOM_CTRL_A') + + simulate_input_keys('CUSTOM_CTRL_RIGHT') + expect.eq(read_selected_text(text_area), '') + + screen:dismiss() +end + +function test.render_text_set_by_api() + local text_area, screen, window, widget = arrange_textarea({w=80}) + + local text = table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Pellentesque dignissim volutpat orci, sed molestie metus elementum vel.', + 'Donec sit amet mattis ligula, ac vestibulum lorem.', + }, '\n') + + widget:setText(text) + widget:setCursor(#text + 1) + + expect.eq(read_rendered_text(text_area), text .. '_') + + screen:dismiss() +end + +function test.undo_redo_keyboard_changes() + local text_area, screen, window, widget = arrange_textarea({w=80}) + + local text = table.concat({ + 'Lorem ipsum dolor sit amet. ', + }, '\n') + + function reset_text() + text_area:setText(text) + text_area:setCursor(#text + 1) + end + + reset_text() + + -- undo single char + simulate_input_text('A') + + expect.eq(read_rendered_text(text_area), text .. 'A_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), text .. 'A_') + + -- undo fast written text as group + reset_text() + simulate_input_text('123') + + expect.eq(read_rendered_text(text_area), text .. '123_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), text .. '123_') + + -- undo cut feature + reset_text() + simulate_input_text('123') + + expect.eq(read_rendered_text(text_area), text .. '123_') + + simulate_input_keys('CUSTOM_CTRL_A') + simulate_input_keys('CUSTOM_CTRL_X') + + expect.eq(read_rendered_text(text_area), '_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '123_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), '_') + + -- undo paste feature + reset_text() + + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), text .. text .. '123_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), text .. text .. '123_') + + -- undo enter + reset_text() + + simulate_input_keys('SELECT') + + expect.eq(read_rendered_text(text_area), text .. '\n_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), text .. '\n_') + + -- undo backspace + reset_text() + + simulate_input_keys('STRING_A000') + + expect.eq(read_rendered_text(text_area), text:sub(1, #text - 1) .. '_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), text:sub(1, #text - 1) .. '_') + + -- undo line delete + reset_text() + + simulate_input_keys('CUSTOM_CTRL_U') + + expect.eq(read_rendered_text(text_area), '_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), '_') + + -- undo delete rest of line + reset_text() + + text_area:setCursor(5) + local expected_text = text:sub(1, 4) .. '_' .. text:sub(6, #text) + expect.eq(read_rendered_text(text_area), expected_text) + + simulate_input_keys('CUSTOM_CTRL_K') + + expect.eq(read_rendered_text(text_area), text:sub(1, 4) .. '_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), expected_text) + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), text:sub(1, 4) .. '_') + + -- undo delete char + reset_text() + + text_area:setCursor(5) + expect.eq(read_rendered_text(text_area), expected_text) + + simulate_input_keys('CUSTOM_DELETE') + + expect.eq( + read_rendered_text(text_area), + text:sub(1, 4) .. '_' .. text:sub(7, #text) + ) + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), expected_text) + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq( + read_rendered_text(text_area), + text:sub(1, 4) .. '_' .. text:sub(7, #text) + ) + + -- undo delete last word + reset_text() + + simulate_input_keys('CUSTOM_CTRL_W') + expect.eq(read_rendered_text(text_area), 'Lorem ipsum dolor sit _') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), 'Lorem ipsum dolor sit _') + + -- undo API setText + reset_text() + + widget:clearHistory() + widget:setText('Random new text') + widget:setCursor(1) + expect.eq(read_rendered_text(text_area), '_andom new text') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '_') + + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq(read_rendered_text(text_area), '_andom new text') + + screen:dismiss() +end + +function test.clear_undo_redo_history() + local text_area, screen, window, widget = arrange_textarea({w=80}) + + local text = table.concat({ + 'Lorem ipsum dolor sit amet. ', + }, '\n') + + text_area:setText(text) + text_area:setCursor(#text + 1) + + simulate_input_text('A') + simulate_input_text(' ') + simulate_input_text('longer text') + + expect.eq(read_rendered_text(text_area), text .. 'A longer text_') + + widget:clearHistory() + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. 'A longer text_') + + screen:dismiss() +end