diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 8457b88f51..203a2aa78a 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -2,9 +2,61 @@ local gui = require('gui') local utils = require('utils') local Widget = require('gui.widgets.widget') local HotkeyLabel = require('gui.widgets.labels.hotkey_label') +local TextArea = require('gui.widgets.text_area') +local WrappedText = require('gui.widgets.text_area.wrapped_text') local getval = utils.getval +OneLineWrappedText = defclass(OneLineWrappedText, WrappedText) + +function OneLineWrappedText:update(text) + self.lines = {text} +end + +TextFieldArea = defclass(TextFieldArea, TextArea) +TextFieldArea.ATTRS{ + on_char = DEFAULT_NIL, + key = DEFAULT_NIL, + on_submit = DEFAULT_NIL, + on_submit2 = DEFAULT_NIL, + modal = false, +} + +function TextFieldArea:onInput(keys) + if self.on_char and keys._STRING and keys._STRING ~= 0 then + if not self.on_char(string.char(keys._STRING), self:getText()) then + return self.modal + end + end + + if keys.SELECT or keys.SELECT_ALL then + if self.key then + self:setFocus(false) + end + + if keys.SELECT_ALL then + if self.on_submit2 then + self.on_submit2(self:getText()) + return true + end + else + if self.on_submit then + self.on_submit(self:getText()) + return true + end + end + + return not not self.key + end + + return TextFieldArea.super.onInput(self, keys) +end + +function TextFieldArea:getPreferredFocusState() + -- allow EditField to manage focus + return false +end + ---------------- -- Edit field -- ---------------- @@ -52,18 +104,50 @@ end function EditField:init() local function on_activate() - self.saved_text = self.text self:setFocus(true) end self.start_pos = 1 self.cursor = #self.text + 1 + self.ignore_keys = self.ignore_keys or {} + + self.label = HotkeyLabel{ + frame={t=0,l=0}, + key=self.key, + key_sep=self.key_sep, + label=self.label_text, + on_activate=self.key and on_activate or nil + } + self.text_area = TextFieldArea{ + one_line_mode=true, + frame={t=0,r=0}, + init_text=self.text or '', + text_pen=self.text_pen or COLOR_LIGHTCYAN, + modal=self.modal, + on_char=self.on_char, + key = self.key, + on_submit = self.on_submit, + on_submit2 = self.on_submit2, + on_focus = self:callback('onFocus'), + on_unfocus = self.on_unfocus, + ignore_keys={ + table.unpack(self.ignore_keys) + }, + on_text_change=self:callback('onTextAreaTextChange'), + on_cursor_change=function(cursor) self.cursor = cursor end + } + + self:addviews{self.label, self.text_area} + + self.text_area.frame.l = self.label:getTextWidth() +end - self:addviews{HotkeyLabel{frame={t=0,l=0}, - key=self.key, - key_sep=self.key_sep, - label=self.label_text, - on_activate=self.key and on_activate or nil}} +function EditField:onFocus() + self.saved_text = self.text + + if self.on_focus then + self:on_focus() + end end function EditField:getPreferredFocusState() @@ -71,160 +155,63 @@ function EditField:getPreferredFocusState() end function EditField:setCursor(cursor) - if not cursor or cursor > #self.text then - self.cursor = #self.text + 1 - return + if not cursor then + cursor = #self.text + 1 end - self.cursor = math.max(1, cursor) + self.cursor = cursor + + self.text_area:setCursor(cursor) + self.cursor = self.text_area:getCursor() end function EditField:setText(text, cursor) + text = text or '' + local old = self.text self.text = text + self.text_area:setText(text) + self:setCursor(cursor) if self.on_change and text ~= old then self.on_change(self.text, old) end end -function EditField:postUpdateLayout() - self.text_offset = self.subviews[1]:getTextWidth() -end - ----@param dc gui.Painter -function EditField:onRenderBody(dc) - dc:pen(self.text_pen or COLOR_LIGHTCYAN) - - local cursor_char = '_' - if not getval(self.active) or not self.focus or gui.blink_visible(300) then - cursor_char = (self.cursor > #self.text) and ' ' or - self.text:sub(self.cursor, self.cursor) - end - local txt = self.text:sub(1, self.cursor - 1) .. cursor_char .. - self.text:sub(self.cursor + 1) - local max_width = dc.width - self.text_offset - self.start_pos = 1 - if #txt > max_width then - -- get the substring in the vicinity of the cursor - max_width = max_width - 2 - local half_width = math.floor(max_width/2) - local start_pos = math.max(1, self.cursor-half_width) - local end_pos = math.min(#txt, self.cursor+half_width-1) - if self.cursor + half_width > #txt then - start_pos = #txt - (max_width - 1) - end - if self.cursor - half_width <= 1 then - end_pos = max_width + 1 +function EditField:onTextAreaTextChange(text) + if self.text ~= text then + local old = self.text + self.text = text + if self.on_change then + self.on_change(self.text, old) end - self.start_pos = start_pos > 1 and start_pos - 1 or start_pos - txt = ('%s%s%s'):format(start_pos == 1 and '' or string.char(27), - txt:sub(start_pos, end_pos), - end_pos == #txt and '' or string.char(26)) end - dc:advance(self.text_offset):string(txt) +end + +function EditField:setFocus(focus) + self.text_area:setFocus(focus) end function EditField:insert(text) local old = self.text - self:setText(old:sub(1,self.cursor-1)..text..old:sub(self.cursor), - self.cursor + #text) + self:setText( + old:sub(1,self.cursor-1)..text..old:sub(self.cursor), + self.cursor + #text + ) end function EditField:onInput(keys) - if not self.focus then - -- only react to our hotkey + if not self.text_area.focus then return self:inputToSubviews(keys) end - if self.ignore_keys then - for _,ignore_key in ipairs(self.ignore_keys) do - if keys[ignore_key] then return false end - end - end - if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then self:setText(self.saved_text) self:setFocus(false) return true end - if keys.SELECT or keys.SELECT_ALL then - if self.key then - self:setFocus(false) - end - if keys.SELECT_ALL then - if self.on_submit2 then - self.on_submit2(self.text) - return true - end - else - if self.on_submit then - self.on_submit(self.text) - return true - end - end - return not not self.key - elseif keys.CUSTOM_DELETE then - local old = self.text - local del_pos = self.cursor - if del_pos <= #old then - self:setText(old:sub(1, del_pos-1) .. old:sub(del_pos+1), del_pos) - end - return true - elseif keys._STRING then - local old = self.text - if keys._STRING == 0 then - -- handle backspace - local del_pos = self.cursor - 1 - if del_pos > 0 then - self:setText(old:sub(1, del_pos-1) .. old:sub(del_pos+1), del_pos) - end - else - local cv = string.char(keys._STRING) - if not self.on_char or self.on_char(cv, old) then - self:insert(cv) - elseif self.on_char then - return self.modal - end - end + if self.text_area:onInput(keys) then return true - elseif keys.KEYBOARD_CURSOR_LEFT then - self:setCursor(self.cursor - 1) - return true - elseif keys.CUSTOM_CTRL_LEFT then -- back one word - local _, prev_word_end = self.text:sub(1, self.cursor-1): - find('.*[%w_%-][^%w_%-]') - self:setCursor(prev_word_end or 1) - return true - elseif keys.CUSTOM_HOME then - self:setCursor(1) - return true - elseif keys.KEYBOARD_CURSOR_RIGHT then - self:setCursor(self.cursor + 1) - return true - elseif keys.CUSTOM_CTRL_RIGHT then -- forward one word - local _,next_word_start = self.text:find('[^%w_%-][%w_%-]', self.cursor) - self:setCursor(next_word_start) - return true - elseif keys.CUSTOM_END then - self:setCursor() - return true - elseif keys.CUSTOM_CTRL_C then - dfhack.internal.setClipboardTextCp437(self.text) - return true - elseif keys.CUSTOM_CTRL_X then - dfhack.internal.setClipboardTextCp437(self.text) - self:setText('') - return true - elseif keys.CUSTOM_CTRL_V then - self:insert(dfhack.internal.getClipboardTextCp437()) - return true - elseif keys._MOUSE_L_DOWN then - local mouse_x = self:getMousePos() - if mouse_x then - self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) - return true - end end -- if we're modal, then unconditionally eat all the input diff --git a/library/lua/gui/widgets/text_area.lua b/library/lua/gui/widgets/text_area.lua index c76a7d03e8..75d83fef43 100644 --- a/library/lua/gui/widgets/text_area.lua +++ b/library/lua/gui/widgets/text_area.lua @@ -23,9 +23,10 @@ TextArea.ATTRS{ function TextArea:init() self.render_start_line_y = 1 + self.render_start_x = 1 self.text_area = TextAreaContent{ - frame={l=0,r=3,t=0}, + frame={l=0,r=self.one_line_mode and 0 or 3,t=0}, text=self.init_text, text_pen=self.text_pen, @@ -35,7 +36,10 @@ function TextArea:init() one_line_mode=self.one_line_mode, on_text_change=function (text, old_text) - self:updateLayout() + if self.frame_body then + self:updateLayout() + end + if self.on_text_change then self.on_text_change(text, old_text) end @@ -85,12 +89,25 @@ function TextArea:onCursorChange(cursor, old_cursor) self.text_area.cursor ) - if y >= self.render_start_line_y + self.text_area.frame_body.height then - self:updateScrollbar( - y - self.text_area.frame_body.height + 1 - ) - elseif (y < self.render_start_line_y) then - self:updateScrollbar(y) + if self.text_area.frame_body then + if y >= self.render_start_line_y + self.text_area.frame_body.height then + self:updateScrollbar( + y - self.text_area.frame_body.height + 1 + ) + elseif (y < self.render_start_line_y) then + self:updateScrollbar(y) + end + + if self.one_line_mode then + local x_screen_offset_right = math.max(0, x - self.render_start_x - self.text_area.frame_body.width + 1) + local x_screen_offset_left = math.max(0, self.render_start_x - x) + + if x_screen_offset_right > 0 then + self.render_start_x = self.render_start_x + x_screen_offset_right + elseif x_screen_offset_left > 0 then + self.render_start_x = self.render_start_x - x_screen_offset_left + end + end end if self.on_cursor_change then @@ -164,6 +181,9 @@ end function TextArea:renderSubviews(dc) self.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) + if self.one_line_mode then + self.text_area.frame_body.x1 = self.frame_body.x1-(self.render_start_x - 1) + end -- only visible lines of text_area will be rendered TextArea.super.renderSubviews(self, dc) end @@ -177,6 +197,10 @@ function TextArea:onInput(keys) self:setFocus(true) end + if not self.focus then + return false + end + return TextArea.super.onInput(self, keys) end 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 48877ccaf7..0791f200d9 100644 --- a/library/lua/gui/widgets/text_area/text_area_content.lua +++ b/library/lua/gui/widgets/text_area/text_area_content.lua @@ -7,6 +7,12 @@ local HistoryStore = require('gui.widgets.text_area.history_store') local CLIPBOARD_MODE = {LOCAL = 1, LINE = 2} local HISTORY_ENTRY = HistoryStore.HISTORY_ENTRY +OneLineWrappedText = defclass(OneLineWrappedText, WrappedText) + +function OneLineWrappedText:update(text) + self.lines = {text} +end + TextAreaContent = defclass(TextAreaContent, Widget) TextAreaContent.ATTRS{ @@ -41,9 +47,8 @@ function TextAreaContent:init() bold=true }) - self.text = self:normalizeText(self.text) - - self.wrapped_text = WrappedText{ + local TextWrapper = self.one_line_mode and OneLineWrappedText or WrappedText + self.wrapped_text = TextWrapper{ text=self.text, wrap_width=256 } @@ -51,14 +56,6 @@ function TextAreaContent:init() 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 @@ -68,6 +65,11 @@ function TextAreaContent:postComputeFrame() end function TextAreaContent:recomputeLines() + if not self.frame_body then + -- called before first layout compute + return + end + self.wrapped_text:update( self.text, -- something cursor '_' need to be add at the end of a line @@ -96,7 +98,7 @@ function TextAreaContent:setCursor(cursor_offset) end function TextAreaContent:setSelection(from_offset, to_offset) - -- text selection is always start on self.cursor and on self.sel_end + -- text selection is always start on self.cursor and end on self.sel_end self:setCursor(from_offset) self.sel_end = to_offset @@ -187,7 +189,7 @@ end function TextAreaContent:setText(text) local old_text = self.text - self.text = self:normalizeText(text) + self.text = text self:recomputeLines() @@ -207,11 +209,19 @@ function TextAreaContent:insert(text) self:setCursor(self.cursor + #text) end +function TextAreaContent:normalizeLine(text_line) + if self.one_line_mode or self.debug then + return text_line + else + -- do not render new lines symbol in multiline mode + return text_line:gsub(NEWLINE, '') + end +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, @@ -220,8 +230,7 @@ function TextAreaContent:onRenderBody(dc) 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) + local line = self:normalizeLine(self.wrapped_text.lines[i]) dc:string(line) dc:newline() end @@ -240,7 +249,6 @@ function TextAreaContent:onRenderBody(dc) 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 @@ -249,24 +257,26 @@ function TextAreaContent:onRenderBody(dc) 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) + local line = self:normalizeLine( + self.wrapped_text.lines[from_y]:sub( + from_x, to_y == from_y and to_x or nil + ) + ) 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) + line = self:normalizeLine(self.wrapped_text.lines[y]) 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) + local line = self:normalizeLine( + self.wrapped_text.lines[to_y]:sub(1, to_x) + ) dc:seek(0, to_y - 1) :string(line) end @@ -478,6 +488,7 @@ function TextAreaContent:onMouseInput(keys) 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 @@ -501,12 +512,22 @@ end function TextAreaContent:onCursorInput(keys) if keys.KEYBOARD_CURSOR_LEFT then - self:setCursor(self.cursor - 1) + if self:hasSelection() then + self:setCursor(math.min(self.cursor, self.sel_end)) + else + self:setCursor(self.cursor - 1) + end + return true elseif keys.KEYBOARD_CURSOR_RIGHT then - self:setCursor(self.cursor + 1) + if self:hasSelection() then + self:setCursor(math.max(self.cursor, self.sel_end)) + else + self:setCursor(self.cursor + 1) + end + return true - elseif keys.KEYBOARD_CURSOR_UP then + elseif keys.KEYBOARD_CURSOR_UP and not self.one_line_mode 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 @@ -515,7 +536,7 @@ function TextAreaContent:onCursorInput(keys) self:setCursor(offset) self.last_cursor_x = last_cursor_x return true - elseif keys.KEYBOARD_CURSOR_DOWN then + elseif keys.KEYBOARD_CURSOR_DOWN and not self.one_line_mode 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 @@ -559,12 +580,13 @@ end function TextAreaContent:onTextManipulationInput(keys) if keys.SELECT then -- handle enter + self.history:store( + HISTORY_ENTRY.WHITESPACE_BLOCK, + self.text, + self.cursor + ) + if not self.one_line_mode then - self.history:store( - HISTORY_ENTRY.WHITESPACE_BLOCK, - self.text, - self.cursor - ) self:insert(NEWLINE) end diff --git a/test/library/gui/widgets.EditField.lua b/test/library/gui/widgets.EditField.lua index 755c3c54de..432b65a163 100644 --- a/test/library/gui/widgets.EditField.lua +++ b/test/library/gui/widgets.EditField.lua @@ -4,6 +4,9 @@ local widgets = require('gui.widgets') function test.editfield_cursor() local e = widgets.EditField{} + + -- cursor is normally set in `postUpdateLayout`, hard to test in unit tests + e:setCursor(1) e:setFocus(true) expect.eq(1, e.cursor, 'cursor should be after the empty string') @@ -29,11 +32,11 @@ function test.editfield_cursor() e:onInput{CUSTOM_HOME=true} expect.eq(1, e.cursor, 'cursor should be at beginning of string') e:onInput{CUSTOM_CTRL_RIGHT=true} - expect.eq(6, e.cursor, 'goto beginning of next word') + expect.eq(5, e.cursor, 'goto end of current word') e:onInput{CUSTOM_END=true} expect.eq(16, e.cursor, 'cursor should be at end of string') e:onInput{CUSTOM_CTRL_LEFT=true} - expect.eq(9, e.cursor, 'goto end of previous word') + expect.eq(10, e.cursor, 'goto beginning of current word') end function test.editfield_click() @@ -41,24 +44,31 @@ function test.editfield_click() e:setFocus(true) expect.eq(5, e.cursor) - mock.patch(e, 'getMousePos', mock.func(0), function() - e:onInput{_MOUSE_L_DOWN=true} - expect.eq(1, e.cursor) - end) + local text_area_content = e.text_area.text_area + + mock.patch(text_area_content, 'getMousePos', mock.func(0, 0), function() + e:onInput{_MOUSE_L_DOWN=true, _MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} + expect.eq(1, e.cursor) + end) - mock.patch(e, 'getMousePos', mock.func(20), function() - e:onInput{_MOUSE_L_DOWN=true} - expect.eq(5, e.cursor, 'should only seek to end of text') - end) + mock.patch(text_area_content, 'getMousePos', mock.func(20, 0), function() + e:onInput{_MOUSE_L_DOWN=true, _MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} + expect.eq(5, e.cursor, 'should only seek to end of text') + end) - mock.patch(e, 'getMousePos', mock.func(2), function() - e:onInput{_MOUSE_L_DOWN=true} - expect.eq(3, e.cursor) - end) + mock.patch(text_area_content, 'getMousePos', mock.func(2, 0), function() + e:onInput{_MOUSE_L_DOWN=true, _MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} + expect.eq(3, e.cursor) + end) end function test.editfield_ignore_keys() local e = widgets.EditField{ignore_keys={'CUSTOM_B', 'CUSTOM_C'}} + -- cursor is normally set in `postUpdateLayout`, hard to test in unit tests + e:setCursor(1) e:setFocus(true) e:onInput{_STRING=string.byte('a'), CUSTOM_A=true} diff --git a/test/library/gui/widgets.TextArea.lua b/test/library/gui/widgets.TextArea.lua index 0223b05282..8212763237 100644 --- a/test/library/gui/widgets.TextArea.lua +++ b/test/library/gui/widgets.TextArea.lua @@ -3,6 +3,8 @@ local widgets = require('gui.widgets') config.target = 'core' +local CP437_NEW_LINE = '◙' + local function simulate_input_keys(...) local keys = {...} for _,key in ipairs(keys) do @@ -66,8 +68,8 @@ local function arrange_textarea(options) if options.w then local border_width = 2 - local scrollbar_width = 3 - local cursor_buffor = 1 + local scrollbar_width = options.one_line_mode and 0 or 3 + local cursor_buffor = options.one_line_mode and 0 or 1 window_width = options.w + border_width + scrollbar_width + cursor_buffor end @@ -90,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}, + one_line_mode=options.one_line_mode, on_cursor_change=options.on_cursor_change, on_text_change=options.on_text_change, } @@ -123,7 +126,7 @@ local function read_rendered_text(text_area) if pen == nil or pen.ch == nil or pen.ch == 0 or pen.fg == 0 then break else - text = text .. string.char(pen.ch) + text = text .. (pen.ch == 10 and CP437_NEW_LINE or string.char(pen.ch)) end end @@ -1715,32 +1718,27 @@ function test.arrows_reset_selection() 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.', + '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_rendered_text(text_area), text .. '_'); - 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')); + expect.eq(read_selected_text(text_area), text); simulate_input_keys('KEYBOARD_CURSOR_LEFT') expect.eq(read_selected_text(text_area), '') + expect.eq(read_rendered_text(text_area), '_' .. text:sub(2)) simulate_input_keys('CUSTOM_CTRL_A') simulate_input_keys('KEYBOARD_CURSOR_RIGHT') expect.eq(read_selected_text(text_area), '') + expect.eq(read_rendered_text(text_area), text:sub(1, #text) .. '_') simulate_input_keys('CUSTOM_CTRL_A') @@ -3315,3 +3313,81 @@ function test.clear_undo_redo_history() screen:dismiss() end + +function test.render_new_lines_in_one_line_mode() + local text_area, screen, window, widget = arrange_textarea({ + w=80, + one_line_mode=true + }) + + local text_table = { + 'Lorem ipsum dolor sit amet, ', + 'consectetur adipiscing elit.', + } + + widget:setText(table.concat(text_table, '\n')) + + widget:setCursor(1) + + expect.eq(read_rendered_text(text_area), '_' .. table.concat(text_table, CP437_NEW_LINE):sub(2)) + + widget:setText('') + simulate_input_text(' test') + simulate_input_text('\n') + simulate_input_text(' test') + + expect.eq( + read_rendered_text(text_area), + ' test' .. CP437_NEW_LINE .. ' test' .. '_' + ) + + screen:dismiss() +end + +function test.should_ignore_submit_in_one_line_mode() + local text_area, screen, window, widget = arrange_textarea({ + w=80, + one_line_mode=true + }) + + local text = 'Lorem ipsum dolor sit amet' + + widget:setText(text) + + widget:setCursor(1) + + simulate_input_keys('SELECT') + + expect.eq(read_rendered_text(text_area), '_' .. text:sub(2)) + + screen:dismiss() +end + +function test.should_scroll_horizontally_in_one_line_mode() + local text_area, screen, window, widget = arrange_textarea({ + w=80, + one_line_mode=true + }) + + local text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque dignissim volutpat orci, sed' + + widget:setText(text) + + widget:setCursor(1) + + expect.eq(read_rendered_text(text_area), '_' .. text:sub(2, 80)) + + widget:setCursor(81) + + expect.eq(read_rendered_text(text_area), text:sub(2, 80) .. '_') + + widget:setCursor(90) + + expect.eq(read_rendered_text(text_area), text:sub(11, 89) .. '_') + + widget:setCursor(2) + + expect.eq(read_rendered_text(text_area), '_' .. text:sub(3, 81)) + + screen:dismiss() +end