-- text editor, particularly text drawing, horizontal wrap, vertical scrolling Text = {} function Text.draw_cursor(editor, x, y) -- blink every 0.5s if math.floor(Cursor_time*2)%2 == 0 then App.color(Cursor_color) love.graphics.rectangle('fill', x,y, 3,editor.line_height) end end function Text.text_input(editor, t) if love.mouse.isDown(1) then return end if any_modifier_down() then if Keys_down[t] then -- The modifiers didn't change the key. Handle it in keychord_press. return else -- Key mutated by the keyboard layout. Continue below. end end local before = snapshot(editor, editor.cursor.line) Text.insert_at_cursor(editor, t) maybe_snap_cursor_to_bottom_of_screen(editor) record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)}) end function Text.insert_at_cursor(editor, t) assert(editor.lines[editor.cursor.line].mode == 'text', 'line is not text') local byte_offset = Text.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos) editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_offset-1)..t..string.sub(editor.lines[editor.cursor.line].data, byte_offset) editor.cursor.pos = editor.cursor.pos+1 end -- Don't handle any keys here that would trigger text_input above. function Text.keychord_press(editor, chord) assert(editor.lines[editor.cursor.line].mode == editor.cursor.mode) --== shortcuts that mutate text (must schedule_save) if chord == 'return' then local before_line = editor.cursor.line local before = snapshot(editor, before_line) Text.insert_return(editor) maybe_snap_cursor_to_bottom_of_screen(editor) record_undo_event(editor, {before=before, after=snapshot(editor, before_line, editor.cursor.line)}) schedule_save(editor) elseif chord == 'tab' then if editor.cursor.mode == 'text' then local before = snapshot(editor, editor.cursor.line) Text.insert_at_cursor(editor, '\t') maybe_snap_cursor_to_bottom_of_screen(editor) record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)}) schedule_save(editor) end elseif chord == 'backspace' then if editor.cursor.mode == 'text' then if editor.selection1.line then Text.delete_selection_and_record_undo_event(editor) schedule_save(editor) return end local before if editor.cursor.pos > 1 then before = snapshot(editor, editor.cursor.line) local byte_start = utf8.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos-1) local byte_end = utf8.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos) if byte_start then if byte_end then editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_start-1)..string.sub(editor.lines[editor.cursor.line].data, byte_end) else editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_start-1) end editor.cursor.pos = editor.cursor.pos-1 end elseif editor.cursor.line > 1 then before = snapshot(editor, editor.cursor.line-1, editor.cursor.line) if editor.lines[editor.cursor.line-1].mode == 'drawing' then table.remove(editor.lines, editor.cursor.line-1) if editor.screen_top.line == editor.cursor.line-1 then editor.screen_top = {mode='text', line=editor.screen_top.line, pos=1} end else -- join lines editor.cursor.pos = utf8.len(editor.lines[editor.cursor.line-1].data)+1 editor.lines[editor.cursor.line-1].data = editor.lines[editor.cursor.line-1].data..editor.lines[editor.cursor.line].data table.remove(editor.lines, editor.cursor.line) end editor.cursor.line = editor.cursor.line-1 end if editor.screen_top.line > #editor.lines then editor.screen_top = vert(editor, editor.cursor, 0) elseif edit.lt(editor.cursor, editor.screen_top) then maybe_snap_cursor_to_top_of_screen(editor) end assert(edit.le(editor.screen_top, editor.cursor), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(editor.screen_top.line, editor.screen_top.pos or -1, editor.cursor.line, editor.cursor.pos or -1)) record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)}) schedule_save(editor) end elseif chord == 'delete' then if editor.cursor.mode == 'drawing' then assert(editor.cursor.line < #editor.lines) local before = snapshot(editor, editor.cursor.line, editor.cursor.line+1) table.remove(editor.lines, editor.cursor.line) assert(editor.cursor.line <= #editor.lines) if editor.lines[editor.cursor.line].mode == 'text' then editor.cursor = {mode='text', line=editor.cursor.line, pos=1} else editor.cursor = {mode='drawing', line=editor.cursor.line} end -- no need to scroll, but screen_top may have switched mode if editor.screen_top.line == editor.cursor.line then editor.screen_top = deepcopy(editor.cursor) end record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)}) else -- cursor in text line if editor.selection1.line then Text.delete_selection_and_record_undo_event(editor) schedule_save(editor) return end local before if editor.cursor.pos <= utf8.len(editor.lines[editor.cursor.line].data) then before = snapshot(editor, editor.cursor.line) else before = snapshot(editor, editor.cursor.line, editor.cursor.line+1) end if editor.cursor.pos <= utf8.len(editor.lines[editor.cursor.line].data) then local byte_start = utf8.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos) local byte_end = utf8.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos+1) if byte_start then if byte_end then editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_start-1)..string.sub(editor.lines[editor.cursor.line].data, byte_end) else editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_start-1) end -- no change to editor.cursor.pos end elseif editor.cursor.line < #editor.lines then if editor.lines[editor.cursor.line+1].mode == 'text' then -- join lines editor.lines[editor.cursor.line].data = editor.lines[editor.cursor.line].data..editor.lines[editor.cursor.line+1].data end table.remove(editor.lines, editor.cursor.line+1) end record_undo_event(editor, {before=before, after=snapshot(editor, editor.cursor.line)}) schedule_save(editor) end --== shortcuts that move the cursor elseif chord == 'left' then Text.left(editor) editor.selection1 = {} elseif chord == 'right' then Text.right(editor) editor.selection1 = {} elseif chord == 'S-left' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.left(editor) elseif chord == 'S-right' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.right(editor) -- C- hotkeys reserved for drawings, so we'll use M- elseif chord == 'M-left' then Text.word_left(editor) editor.selection1 = {} elseif chord == 'M-right' then Text.word_right(editor) editor.selection1 = {} elseif chord == 'M-S-left' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.word_left(editor) elseif chord == 'M-S-right' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.word_right(editor) elseif chord == 'home' then Text.start_of_line(editor) editor.selection1 = {} elseif chord == 'end' then Text.end_of_line(editor) editor.selection1 = {} elseif chord == 'S-home' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.start_of_line(editor) elseif chord == 'S-end' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.end_of_line(editor) elseif chord == 'up' then Text.up(editor) editor.selection1 = {} elseif chord == 'down' then Text.down(editor) editor.selection1 = {} elseif chord == 'S-up' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.up(editor) elseif chord == 'S-down' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.down(editor) elseif chord == 'pageup' then Text.pageup(editor) editor.selection1 = {} elseif chord == 'pagedown' then Text.pagedown(editor) editor.selection1 = {} elseif chord == 'S-pageup' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.pageup(editor) elseif chord == 'S-pagedown' then if editor.selection1.line == nil then editor.selection1 = deepcopy(editor.cursor) end Text.pagedown(editor) end end function Text.insert_return(editor) if editor.cursor.mode == 'drawing' then table.insert(editor.lines, editor.cursor.line+1, {mode='text', data=''}) editor.cursor = {mode='text', line=editor.cursor.line+1, pos=1} return end local byte_offset = Text.offset(editor.lines[editor.cursor.line].data, editor.cursor.pos) table.insert(editor.lines, editor.cursor.line+1, {mode='text', data=string.sub(editor.lines[editor.cursor.line].data, byte_offset)}) editor.lines[editor.cursor.line].data = string.sub(editor.lines[editor.cursor.line].data, 1, byte_offset-1) editor.cursor = {mode='text', line=editor.cursor.line+1, pos=1} maybe_snap_cursor_to_bottom_of_screen(editor) end function Text.offset(s, pos1) if pos1 == 1 then return 1 end local result = utf8.offset(s, pos1) if result == nil then assert(false, ('Text.offset(%d) called on a string of length %d (byte size %d); this is likely a failure to handle utf8\n\n^%s$\n'):format(pos1, utf8.len(s), #s, s)) end return result end