From 8bc9da2b72a830b94a1b6df527dff22206cf10e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 8 Oct 2024 19:02:43 +0200 Subject: [PATCH 01/10] Create TextArea widget, based on `gui/journal` widget --- library/lua/gui/widgets.lua | 1 + library/lua/gui/widgets/text_area.lua | 166 +++++ .../gui/widgets/text_area/history_store.lua | 81 +++ .../widgets/text_area/text_area_content.lua | 674 ++++++++++++++++++ .../gui/widgets/text_area/wrapped_text.lua | 70 ++ 5 files changed, 992 insertions(+) create mode 100644 library/lua/gui/widgets/text_area.lua create mode 100644 library/lua/gui/widgets/text_area/history_store.lua create mode 100644 library/lua/gui/widgets/text_area/text_area_content.lua create mode 100644 library/lua/gui/widgets/text_area/wrapped_text.lua 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..c6711f90fd --- /dev/null +++ b/library/lua/gui/widgets/text_area.lua @@ -0,0 +1,166 @@ +-- 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') + +TextArea = defclass(TextArea, Panel) + +TextArea.ATTRS{ + init_text = '', + init_cursor = DEFAULT_NIL, + text_pen = COLOR_LIGHTCYAN, + ignore_keys = {'STRING_A096'}, + 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:addviews{ + TextAreaContent{ + view_id='text_area', + 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 (val) + self:updateLayout() + if self.on_text_change then + self.on_text_change(val) + end + end, + on_cursor_change=self:callback('onCursorChange') + }, + Scrollbar{ + view_id='scrollbar', + frame={r=0,t=1}, + on_scroll=self:callback('onScrollbar'), + visible=not self.one_line_mode + } + } + self:setFocus(true) +end + +function TextArea:getText() + return self.subviews.text_area.text +end + +function TextArea:getCursor() + return self.subviews.text_area.cursor +end + +function TextArea:onCursorChange(cursor) + local x, y = self.subviews.text_area.wrapped_text:indexToCoords( + self.subviews.text_area.cursor + ) + + if y >= self.render_start_line_y + self.subviews.text_area.frame_body.height then + self:updateScrollbar( + y - self.subviews.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) + end +end + +function TextArea:scrollToCursor(cursor_offset) + if self.subviews.scrollbar.visible then + local _, cursor_liny_y = self.subviews.text_area.wrapped_text:indexToCoords( + cursor_offset + ) + self:updateScrollbar(cursor_liny_y) + end +end + +function TextArea:setCursor(cursor_offset) + return self.subviews.text_area:setCursor(cursor_offset) +end + +function TextArea:getPreferredFocusState() + return self.parent_view.focus +end + +function TextArea:postUpdateLayout() + self:updateScrollbar(self.render_start_line_y) + + if self.subviews.text_area.cursor == nil then + local cursor = self.init_cursor or #self.init_text + 1 + self.subviews.text_area:setCursor(cursor) + self:scrollToCursor(cursor) + end +end + +function TextArea:onScrollbar(scroll_spec) + local height = self.subviews.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.subviews.text_area.wrapped_text.lines + + local render_start_line_y = (math.min( + #self.subviews.text_area.wrapped_text.lines - self.subviews.text_area.frame_body.height + 1, + math.max(1, scrollbar_current_y) + )) + + self.subviews.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.subviews.text_area:setRenderStartLineY(self.render_start_line_y) +end + +function TextArea:renderSubviews(dc) + self.subviews.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) + + TextArea.super.renderSubviews(self, dc) +end + +function TextArea:onInput(keys) + if (self.subviews.scrollbar.is_dragging) then + return self.subviews.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..4537d9bf69 --- /dev/null +++ b/library/lua/gui/widgets/text_area/history_store.lua @@ -0,0 +1,81 @@ +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 + +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..fcca75ac91 --- /dev/null +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -0,0 +1,674 @@ +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 = {'STRING_A096'}, + 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 = 10, +} + +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:getPreferredFocusState() + return true +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) + 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 then + self.on_cursor_change(self.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 changed = self.text ~= text + self.text = self:normalizeText(text) + + self:recomputeLines() + + if changed and self.on_text_change then + self.on_text_change(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:cut() + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + return true + elseif keys.CUSTOM_CTRL_V then + self:paste() + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + 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(1, #self.text) + 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..d57a4d2fd6 --- /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 = DEFAULT_NIL, +} + +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 From 55ae723d300ff0152624294e8470c2f113b7ccbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 8 Oct 2024 20:25:49 +0200 Subject: [PATCH 02/10] Migrate journal text-area related test to core TestArea test module --- test/library/gui/widgets.TextArea.lua | 2602 +++++++++++++++++++++++++ 1 file changed, 2602 insertions(+) create mode 100644 test/library/gui/widgets.TextArea.lua diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua new file mode 100644 index 0000000000..15982f7ae6 --- /dev/null +++ b/test/library/gui/widgets.TextArea.lua @@ -0,0 +1,2602 @@ +local gui = require('gui') +local widgets = require('gui.widgets') + +config.target = 'core' + +local df_major_version = tonumber(dfhack.getCompiledDFVersion():match('%d+')) + +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_text_change=self:callback('onTextChange'), + -- on_cursor_change=self:callback('onCursorChange'), + } + } + } + }) + + local window = screen.subviews.window + local text_area = screen.subviews.text_area + text_area.enable_cursor_blink = false + + screen:show() + screen:onRender() + + return text_area, screen, window +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.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_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_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_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.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_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.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_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_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_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.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.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_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.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 = arrange_textarea({w=100, h=10}) + local scrollbar = window.subviews.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 + +if df_major_version < 51 then + -- temporary ignore test features that base on newest API of the DF game + return +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 From e2373879d039d47b148c2b24f64e1604564b898c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 9 Oct 2024 09:03:07 +0200 Subject: [PATCH 03/10] Add documentation for new TextArea widget --- docs/dev/Lua API.rst | 114 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 7da0e08856..b598445a04 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -5513,6 +5513,120 @@ 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. + +Attributes: + +* ``init_text``: + The initial text content for the text area. + +* ``init_cursor``: + The initial cursor position within the text content. + +* ``text_pen``: + Optional pen used to draw the text. + +* ``select_pen``: + Optional pen used for text selection. + +* ``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)``. + +* ``on_cursor_change``: + Callback function called whenever the cursor position changes. + The function signature should be ``on_cursor_change(new_cursor_pos)``. + +* ``one_line_mode``: + Boolean attribute that, when set to ``true``, disables multi-line + text features and restricts the text area to a single line. + +Functions: + +* ``textarea:getText()`` + + Returns the current text content of the ``TextArea`` widget as a string. + +* ``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 is useful for automatically scrolling when the user moves the cursor + beyond the visible region of the text area. + +Functionality: + +- Cursor Control: Navigate through text using arrow keys (Left, Right, Up, + and Down) for precise cursor placement. +- Fast Rewind: Use :kbd:`Ctrl` + :kbd:`Left` and :kbd:`Ctrl` + :kbd:`Right` to + move the cursor one word back or forward. +- Longest X Position Memory: The cursor remembers the longest x position when + moving up or down, making vertical navigation more intuitive. +- Mouse Control: Use the mouse to position the cursor within the text, + providing an alternative to keyboard navigation. +- 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. +- 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. +- Line Navigation: :kbd:`Home` moves the cursor to the beginning of the current + line, and :kbd:`End` moves it to the end. +- 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. +- Text Selection: Select text with the mouse, with support for replacing or + removing selected text. +- 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`. +- Select Word/Line: Use double click to select current word, or triple click to + select current line +- 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 +- Scrolling behaviour for long text build-in + Scrollbar class --------------- From 83a9a19f679ff5c2d6f878484c1d6c9a63997c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 9 Oct 2024 09:09:29 +0200 Subject: [PATCH 04/10] Add way to set text from the TextArea widget API --- library/lua/gui/widgets/text_area.lua | 4 ++++ test/library/gui/widgets.TextArea.lua | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index c6711f90fd..a23195b133 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -55,6 +55,10 @@ function TextArea:getText() return self.subviews.text_area.text end +function TextArea:setText(text) + return self.subviews.text_area:setText(text) +end + function TextArea:getCursor() return self.subviews.text_area.cursor end diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 15982f7ae6..6c300997a7 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -106,7 +106,7 @@ local function arrange_textarea(options) screen:show() screen:onRender() - return text_area, screen, window + return text_area, screen, window, screen.subviews.text_area_widget end local function read_rendered_text(text_area) @@ -2600,3 +2600,20 @@ function test.fast_rewind_reset_selection() 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 From 3ce4f1fbe58b76d2bf4d0850e06f9ed952ee3ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 9 Oct 2024 09:18:25 +0200 Subject: [PATCH 05/10] Abandon named subviews for TextArea to avoid collisions with user code --- library/lua/gui/widgets/text_area.lua | 91 ++++++++++++++------------- test/library/gui/widgets.TextArea.lua | 7 ++- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index a23195b133..52eac62d1f 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -21,56 +21,57 @@ TextArea.ATTRS{ 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 (val) + self:updateLayout() + if self.on_text_change then + self.on_text_change(val) + 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{ - TextAreaContent{ - view_id='text_area', - 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 (val) - self:updateLayout() - if self.on_text_change then - self.on_text_change(val) - end - end, - on_cursor_change=self:callback('onCursorChange') - }, - Scrollbar{ - view_id='scrollbar', - frame={r=0,t=1}, - on_scroll=self:callback('onScrollbar'), - visible=not self.one_line_mode - } + self.text_area, + self.scrollbar, } self:setFocus(true) end function TextArea:getText() - return self.subviews.text_area.text + return self.text_area.text end function TextArea:setText(text) - return self.subviews.text_area:setText(text) + return self.text_area:setText(text) end function TextArea:getCursor() - return self.subviews.text_area.cursor + return self.text_area.cursor end function TextArea:onCursorChange(cursor) - local x, y = self.subviews.text_area.wrapped_text:indexToCoords( - self.subviews.text_area.cursor + local x, y = self.text_area.wrapped_text:indexToCoords( + self.text_area.cursor ) - if y >= self.render_start_line_y + self.subviews.text_area.frame_body.height then + if y >= self.render_start_line_y + self.text_area.frame_body.height then self:updateScrollbar( - y - self.subviews.text_area.frame_body.height + 1 + y - self.text_area.frame_body.height + 1 ) elseif (y < self.render_start_line_y) then self:updateScrollbar(y) @@ -82,8 +83,8 @@ function TextArea:onCursorChange(cursor) end function TextArea:scrollToCursor(cursor_offset) - if self.subviews.scrollbar.visible then - local _, cursor_liny_y = self.subviews.text_area.wrapped_text:indexToCoords( + if self.scrollbar.visible then + local _, cursor_liny_y = self.text_area.wrapped_text:indexToCoords( cursor_offset ) self:updateScrollbar(cursor_liny_y) @@ -91,7 +92,7 @@ function TextArea:scrollToCursor(cursor_offset) end function TextArea:setCursor(cursor_offset) - return self.subviews.text_area:setCursor(cursor_offset) + return self.text_area:setCursor(cursor_offset) end function TextArea:getPreferredFocusState() @@ -101,15 +102,15 @@ end function TextArea:postUpdateLayout() self:updateScrollbar(self.render_start_line_y) - if self.subviews.text_area.cursor == nil then + if self.text_area.cursor == nil then local cursor = self.init_cursor or #self.init_text + 1 - self.subviews.text_area:setCursor(cursor) + self.text_area:setCursor(cursor) self:scrollToCursor(cursor) end end function TextArea:onScrollbar(scroll_spec) - local height = self.subviews.text_area.frame_body.height + local height = self.text_area.frame_body.height local render_start_line = self.render_start_line_y if scroll_spec == 'down_large' then @@ -128,14 +129,14 @@ function TextArea:onScrollbar(scroll_spec) end function TextArea:updateScrollbar(scrollbar_current_y) - local lines_count = #self.subviews.text_area.wrapped_text.lines + local lines_count = #self.text_area.wrapped_text.lines local render_start_line_y = (math.min( - #self.subviews.text_area.wrapped_text.lines - self.subviews.text_area.frame_body.height + 1, + #self.text_area.wrapped_text.lines - self.text_area.frame_body.height + 1, math.max(1, scrollbar_current_y) )) - self.subviews.scrollbar:update( + self.scrollbar:update( render_start_line_y, self.frame_body.height, lines_count @@ -146,18 +147,18 @@ function TextArea:updateScrollbar(scrollbar_current_y) end self.render_start_line_y = render_start_line_y - self.subviews.text_area:setRenderStartLineY(self.render_start_line_y) + self.text_area:setRenderStartLineY(self.render_start_line_y) end function TextArea:renderSubviews(dc) - self.subviews.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) + self.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) TextArea.super.renderSubviews(self, dc) end function TextArea:onInput(keys) - if (self.subviews.scrollbar.is_dragging) then - return self.subviews.scrollbar:onInput(keys) + if (self.scrollbar.is_dragging) then + return self.scrollbar:onInput(keys) end if keys._MOUSE_L and self:getMousePos() then diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 6c300997a7..8aa327c29c 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -92,6 +92,7 @@ local function arrange_textarea(options) init_text=options.text or '', init_cursor=options.cursor or 1, frame={l=0,r=0,t=0,b=0} + -- TODO: add tests for callbacks -- on_text_change=self:callback('onTextChange'), -- on_cursor_change=self:callback('onCursorChange'), } @@ -100,7 +101,7 @@ local function arrange_textarea(options) }) local window = screen.subviews.window - local text_area = screen.subviews.text_area + local text_area = screen.subviews.text_area_widget.text_area text_area.enable_cursor_blink = false screen:show() @@ -2184,8 +2185,8 @@ function test.cut_and_paste_selected_text() end function test.scroll_long_text() - local text_area, screen, window = arrange_textarea({w=100, h=10}) - local scrollbar = window.subviews.scrollbar + 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.', From fa68bbe2c1bf99bd9102d939217aa3262649fedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 9 Oct 2024 09:21:57 +0200 Subject: [PATCH 06/10] Improve TextArea docs --- docs/dev/Lua API.rst | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index b598445a04..a47474c932 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -5522,33 +5522,25 @@ and typical text editor shortcuts. Attributes: -* ``init_text``: - The initial text content for the text area. +* ``init_text``: The initial text content for the text area. -* ``init_cursor``: - The initial cursor position within the text content. +* ``init_cursor``: The initial cursor position within the text content. -* ``text_pen``: - Optional pen used to draw the text. +* ``text_pen``: Optional pen used to draw the text. -* ``select_pen``: - Optional pen used for text selection. +* ``select_pen``: Optional pen used for text selection. -* ``ignore_keys``: - List of input keys to ignore. +* ``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. +* ``on_text_change``: Callback function called whenever the text changes. The function signature should be ``on_text_change(new_text)``. -* ``on_cursor_change``: - Callback function called whenever the cursor position changes. - The function signature should be ``on_cursor_change(new_cursor_pos)``. +* ``on_cursor_change``: Callback function called whenever the cursor + position changes. The function signature should be ``on_cursor_change(new_cursor_pos)``. -* ``one_line_mode``: - Boolean attribute that, when set to ``true``, disables multi-line - text features and restricts the text area to a single line. +* ``one_line_mode``: Boolean attribute that, when set to ``true``, + disables multi-line text features and restricts the text area to a single line. Functions: From cb549ed13ced72a4c3644e103b8f9ccfdee0dd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 09:17:52 +0200 Subject: [PATCH 07/10] Add tests for TextArea undo feature --- test/library/gui/widgets.TextArea.lua | 125 ++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 8aa327c29c..303b78ba9a 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -2618,3 +2618,128 @@ function test.render_text_set_by_api() screen:dismiss() end + +function test.undo_keyboard_changes() + local text_area, screen, window, widget = arrange_textarea({w=80}) + + local text = table.concat({ + 'Lorem ipsum dolor sit amet. ', + }, '\n') + + simulate_input_text(text) + + 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 .. '_') + + -- undo fast written text as group + 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 .. '_') + + -- undo cut feature + 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_') + + -- undo paste feature + + simulate_input_keys('CUSTOM_CTRL_V') + + expect.eq(read_rendered_text(text_area), text .. '123' .. text .. '123_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '123_') + + -- undo enter + + simulate_input_keys('SELECT') + + expect.eq(read_rendered_text(text_area), text .. '123\n_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '123_') + + -- undo backspace + + simulate_input_keys('STRING_A000') + + expect.eq(read_rendered_text(text_area), text .. '12_') + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), text .. '123_') + + -- undo line delete + + 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 .. '123_') + + -- undo delete rest of line + + text_area:setCursor(5) + local expected_text = text:sub(1, 4) .. '_' .. text:sub(6, #text) .. '123' + 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) + + -- undo delete char + + 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) .. '123' + ) + + simulate_input_keys('CUSTOM_CTRL_Z') + + expect.eq(read_rendered_text(text_area), expected_text) + + -- undo delete last word + + text_area:setText(text) + text_area:setCursor(#text + 1) + + 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 .. '_') + + screen:dismiss() +end From ff41a5e1cea94dce02fc349c642066bf9188d804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 09:30:47 +0200 Subject: [PATCH 08/10] Add undo/redo textarea widget features tests --- test/library/gui/widgets.TextArea.lua | 82 ++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 303b78ba9a..2005c8e461 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -2619,15 +2619,21 @@ function test.render_text_set_by_api() screen:dismiss() end -function test.undo_keyboard_changes() +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') - simulate_input_text(text) + 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_') @@ -2636,7 +2642,12 @@ function test.undo_keyboard_changes() 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_') @@ -2645,7 +2656,12 @@ function test.undo_keyboard_changes() 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_') @@ -2659,37 +2675,57 @@ function test.undo_keyboard_changes() 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 .. '123' .. text .. '123_') + expect.eq(read_rendered_text(text_area), text .. text .. '123_') simulate_input_keys('CUSTOM_CTRL_Z') - expect.eq(read_rendered_text(text_area), text .. '123_') + 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 .. '123\n_') + expect.eq(read_rendered_text(text_area), text .. '\n_') simulate_input_keys('CUSTOM_CTRL_Z') - expect.eq(read_rendered_text(text_area), text .. '123_') + 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 .. '12_') + 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 .. '123_') + 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') @@ -2697,12 +2733,17 @@ function test.undo_keyboard_changes() simulate_input_keys('CUSTOM_CTRL_Z') - expect.eq(read_rendered_text(text_area), text .. '123_') + 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) .. '123' + 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') @@ -2713,7 +2754,12 @@ function test.undo_keyboard_changes() 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) @@ -2722,17 +2768,22 @@ function test.undo_keyboard_changes() expect.eq( read_rendered_text(text_area), - text:sub(1, 4) .. '_' .. text:sub(7, #text) .. '123' + text:sub(1, 4) .. '_' .. text:sub(7, #text) ) simulate_input_keys('CUSTOM_CTRL_Z') expect.eq(read_rendered_text(text_area), expected_text) - -- undo delete last word + simulate_input_keys('CUSTOM_CTRL_Y') + + expect.eq( + read_rendered_text(text_area), + text:sub(1, 4) .. '_' .. text:sub(7, #text) + ) - text_area:setText(text) - text_area:setCursor(#text + 1) + -- undo delete last word + reset_text() simulate_input_keys('CUSTOM_CTRL_W') expect.eq(read_rendered_text(text_area), 'Lorem ipsum dolor sit _') @@ -2741,5 +2792,8 @@ function test.undo_keyboard_changes() 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 _') screen:dismiss() end From 2ed6dcbe8502ff3d9f77adf6dadc0c9ad2670db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 09:33:03 +0200 Subject: [PATCH 09/10] Add clear history textarea feature --- docs/dev/Lua API.rst | 4 +++ library/lua/gui/widgets/text_area.lua | 12 ++++--- .../gui/widgets/text_area/history_store.lua | 5 +++ .../widgets/text_area/text_area_content.lua | 8 ++--- test/library/gui/widgets.TextArea.lua | 32 ++++++++++++++++--- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index a47474c932..ecf816c91f 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -5568,6 +5568,10 @@ Functions: This is useful for automatically scrolling when the user moves the cursor beyond the visible region of the text area. +* ``textarea:clearHistory()`` + + Clear undo/redo history of the widget. + Functionality: - Cursor Control: Navigate through text using arrow keys (Left, Right, Up, diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index 52eac62d1f..e4984e2e76 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -64,6 +64,14 @@ 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) local x, y = self.text_area.wrapped_text:indexToCoords( self.text_area.cursor @@ -91,10 +99,6 @@ function TextArea:scrollToCursor(cursor_offset) end end -function TextArea:setCursor(cursor_offset) - return self.text_area:setCursor(cursor_offset) -end - function TextArea:getPreferredFocusState() return self.parent_view.focus end diff --git a/library/lua/gui/widgets/text_area/history_store.lua b/library/lua/gui/widgets/text_area/history_store.lua index 4537d9bf69..abaf2de3ee 100644 --- a/library/lua/gui/widgets/text_area/history_store.lua +++ b/library/lua/gui/widgets/text_area/history_store.lua @@ -76,6 +76,11 @@ function HistoryStore:redo(curr_text, curr_cursor) 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 index fcca75ac91..5c88902b7d 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -234,7 +234,7 @@ function TextAreaContent:onRenderBody(dc) and gui.blink_visible(530) ) - if (show_focus) then + if show_focus then local x, y = self.wrapped_text:indexToCoords(self.cursor) dc:seek(x - 1, y - 1) :char('_') @@ -403,12 +403,12 @@ function TextAreaContent:onInput(keys) self:copy() return true elseif keys.CUSTOM_CTRL_X then - self:cut() self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + self:cut() return true elseif keys.CUSTOM_CTRL_V then - self:paste() self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + self:paste() return true else return TextAreaContent.super.onInput(self, keys) @@ -608,7 +608,7 @@ function TextAreaContent:onTextManipulationInput(keys) return true elseif keys.CUSTOM_CTRL_A then -- select all - self:setSelection(1, #self.text) + self:setSelection(#self.text + 1, 1) return true elseif keys.CUSTOM_CTRL_U then -- delete current line diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 2005c8e461..aac3da020f 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -1243,7 +1243,7 @@ function test.arrows_reset_selection() 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.', + 'porttitor mi, vitae rutrum eros metus nec libero._', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1288,7 +1288,7 @@ function test.click_reset_selection() 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.', + 'porttitor mi, vitae rutrum eros metus nec libero._', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1323,7 +1323,7 @@ function test.line_navigation_reset_selection() 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.', + 'porttitor mi, vitae rutrum eros metus nec libero._', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -1356,7 +1356,7 @@ function test.jump_begin_or_end_reset_selection() 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.', + 'porttitor mi, vitae rutrum eros metus nec libero._', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -2582,7 +2582,7 @@ function test.fast_rewind_reset_selection() 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.', + 'porttitor mi, vitae rutrum eros metus nec libero._', }, '\n')); expect.eq(read_selected_text(text_area), table.concat({ @@ -2797,3 +2797,25 @@ function test.undo_redo_keyboard_changes() expect.eq(read_rendered_text(text_area), 'Lorem ipsum dolor sit _') 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_') +end From 58c8d9b01f6d471129bb711a8bcbc6137d2b54ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 09:47:37 +0200 Subject: [PATCH 10/10] Add history entry (undo/redo) for TextArea API text set (:setText) --- library/lua/gui/widgets/text_area.lua | 9 +++++++++ test/library/gui/widgets.TextArea.lua | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index e4984e2e76..2101324f56 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -3,6 +3,9 @@ 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) @@ -57,6 +60,12 @@ function TextArea:getText() end function TextArea:setText(text) + self.text_area.history:store( + HISTORY_ENTRY.OTHER, + self:getText(), + self:getCursor() + ) + return self.text_area:setText(text) end diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index aac3da020f..7117d63dc2 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -2795,6 +2795,23 @@ function test.undo_redo_keyboard_changes() 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