diff options
-rw-r--r-- | Manual_tests.md | 2 | ||||
-rw-r--r-- | README.md | 97 | ||||
-rw-r--r-- | edit.lua | 245 | ||||
-rw-r--r-- | file.lua | 134 | ||||
-rw-r--r-- | main.lua | 12 | ||||
-rw-r--r-- | run.lua | 9 | ||||
-rw-r--r-- | select.lua | 10 | ||||
-rw-r--r-- | source.lua | 4 | ||||
-rw-r--r-- | text.lua | 152 | ||||
-rw-r--r-- | text_tests.lua | 60 | ||||
-rw-r--r-- | undo.lua | 16 |
11 files changed, 143 insertions, 598 deletions
diff --git a/Manual_tests.md b/Manual_tests.md index cde027f..45154e3 100644 --- a/Manual_tests.md +++ b/Manual_tests.md @@ -9,7 +9,7 @@ Startup: Initializing settings: - delete app settings, start; window opens running the text editor - quit while running the text editor, restart; window opens running the text editor in same position+dimensions - - quit while editing source (color; no drawings; no selection), restart; window opens editing source in same position+dimensions + - quit while editing source (color; no selection), restart; window opens editing source in same position+dimensions - start out running the text editor, move window, press ctrl+e twice; window is running text editor in same position+dimensions - start out editing source, move window, press ctrl+e twice; window is editing source in same position+dimensions - no log file; switching to source works diff --git a/README.md b/README.md index 8b800b2..c5df570 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ -# Plain text with lines +# An editor for plain text. + +Not very useful by itself, but it's a fork of [lines.love](http://akkartik.name/lines.html) +that you can take in other directions besides line drawings, while easily +sharing patches between forks. -An editor for plain text where you can also seamlessly insert line drawings. Designed above all to be easy to modify and give you early warning if your modifications break something. -http://akkartik.name/lines.html - ## Invocation To run from the terminal, [pass this directory to LÖVE](https://love2d.org/wiki/Getting_Started#Running_Games), @@ -13,13 +14,13 @@ optionally with a file path to edit. Alternatively, turn it into a .love file you can double-click on: ``` -$ zip -r /tmp/lines.love *.lua +$ zip -r /tmp/text.love *.lua ``` -By default, lines.love reads/writes the file `lines.txt` in your default +By default, it reads/writes the file `lines.txt` in your default user/home directory (`https://love2d.org/wiki/love.filesystem.getUserDirectory`). -To open a different file, drop it on the lines.love window. +To open a different file, drop it on the app window. ## Keyboard shortcuts @@ -32,12 +33,7 @@ While editing text: * mouse drag or `shift` + movement to select text, `ctrl+a` to select all * `ctrl+e` to modify the sources -For shortcuts while editing drawings, consult the online help. Either: -* hover on a drawing and hit `ctrl+h`, or -* click on a drawing to start a stroke and then press and hold `h` to see your - options at any point during a stroke. - -lines.love has been exclusively tested so far with a US keyboard layout. If +Exclusively tested so far with a US keyboard layout. If you use a different layout, please let me know if things worked, or if you found anything amiss: http://akkartik.name/contact @@ -48,34 +44,15 @@ found anything amiss: http://akkartik.name/contact * No support yet for right-to-left languages. * Undo/redo may be sluggish in large files. Large files may grow sluggish in - other ways. lines.love works well in all circumstances with files under - 50KB. + other ways. Works well in all circumstances with files under 50KB. * If you kill the process, say by force-quitting because things things get sluggish, you can lose data. -* The text cursor will always stay on the screen. This can have some strange - implications: - - * A long series of drawings will get silently skipped when you hit - page-down, until a line of text can be showed on screen. - * If there's no line of text at the top of the file, you may not be able - to scroll back up to the top with page-up. - - So far this app isn't really designed for drawing-heavy files. For now I'm - targeting mostly-text files with a few drawings mixed in. - -* No clipping yet for drawings. In particular, circles/squares/rectangles and - point labels can overflow a drawing. - * Long wrapping lines can't yet distinguish between the cursor at end of one screen line and start of the next, so clicking the mouse to position the cursor can very occasionally do the wrong thing. -* Touchpads can drag the mouse pointer using a light touch or a heavy click. - On Linux, drags using the light touch get interrupted when a key is pressed. - You'll have to press down to drag. - * Can't scroll while selecting text with mouse. * No scrollbars yet. That stuff is hard. @@ -84,35 +61,31 @@ found anything amiss: http://akkartik.name/contact ## Mirrors and Forks -Updates to lines.love can be downloaded from the following mirrors in addition -to the website above: -* https://github.com/akkartik/lines.love -* https://repo.or.cz/lines.love.git -* https://codeberg.org/akkartik/lines.love -* https://tildegit.org/akkartik/lines.love -* https://git.tilde.institute/akkartik/lines.love -* https://git.sr.ht/~akkartik/lines.love -* https://notabug.org/akkartik/lines.love -* https://pagure.io/lines.love - -Forks of lines.love are encouraged. If you show me your fork, I'll link to it -here. - -* https://github.com/akkartik/lines-polygon-experiment -- an experiment that - uses separate shortcuts for regular polygons. `ctrl+3` for triangles, - `ctrl+4` for squares, etc. -* https://codeberg.org/akkartik/text.love -- a stripped down version without - drawings; useful starting point for some forks -* https://git.sr.ht/~akkartik/pensieve.love -- a note-taking app on an - infinite 2D surface. Still in development. -* https://git.sr.ht/~akkartik/capture.love -- a blank-slate mode for the - note-taking app, so all the stuff pensieve.love puts on screen doesn't cause - you to forget what you came to write down. - -## Associated tools - -* https://codeberg.org/akkartik/lines2md exports lines.love files to Markdown - and (non-editable) SVG. +This repo is a fork of lines.love at [http://akkartik.name/lines.html](http://akkartik.name/lines.html). +Updates to it can be downloaded from the following mirrors: + +* https://codeberg.org/akkartik/text.love +* https://repo.or.cz/text.love.git +* https://tildegit.org/akkartik/text.love +* https://git.tilde.institute/akkartik/text.love +* https://git.sr.ht/~akkartik/text.love +* https://notabug.org/akkartik/text.love +* https://github.com/akkartik/text.love +* https://pagure.io/text.love + +Further forks are encouraged. If you show me your fork, I'll link to it here. + +* https://codeberg.org/akkartik/view.love -- a stripped down version without + support for modifying files; useful starting point for some forks. +* https://codeberg.org/akkartik/pong.love -- a fairly minimal example app that + can edit and debug its own source code. +* https://codeberg.org/akkartik/template-live-editor.love -- a template for + building "free-wheeling" live programs (easy to fork, can be modified as + they run), with a text editor primitive. +* https://codeberg.org/akkartik/luaML.love -- a free-wheeling 'browser' for a + Lua-based markup language built as a live program. +* https://codeberg.org/akkartik/driver.love -- a programming environment for + modifying free-wheeling programs while they run. ## Feedback diff --git a/edit.lua b/edit.lua index 727f6e0..9c527b4 100644 --- a/edit.lua +++ b/edit.lua @@ -1,51 +1,19 @@ -- some constants people might like to tweak Text_color = {r=0, g=0, b=0} Cursor_color = {r=1, g=0, b=0} -Stroke_color = {r=0, g=0, b=0} -Current_stroke_color = {r=0.7, g=0.7, b=0.7} -- in process of being drawn -Current_name_background_color = {r=1, g=0, b=0, a=0.1} -- name currently being edited Focus_stroke_color = {r=1, g=0, b=0} -- what mouse is hovering over Highlight_color = {r=0.7, g=0.7, b=0.9} -- selected text -Icon_color = {r=0.7, g=0.7, b=0.7} -- color of current mode icon in drawings -Help_color = {r=0, g=0.5, b=0} -Help_background_color = {r=0, g=0.5, b=0, a=0.1} Margin_top = 15 Margin_left = 25 Margin_right = 25 -Drawing_padding_top = 10 -Drawing_padding_bottom = 10 -Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom - -Same_point_distance = 4 -- pixel distance at which two points are considered the same - edit = {} -- run in both tests and a real run function edit.initialize_state(top, left, right, font_height, line_height) -- currently always draws to bottom of screen local result = { - -- a line is either text or a drawing - -- a text is a table with: - -- mode = 'text', - -- string data, - -- a drawing is a table with: - -- mode = 'drawing' - -- a (y) coord in pixels (updated while painting screen), - -- a (h)eight, - -- an array of points, and - -- an array of shapes - -- a shape is a table containing: - -- a mode - -- an array points for mode 'freehand' (raw x,y coords; freehand drawings don't pollute the points array of a drawing) - -- an array vertices for mode 'polygon', 'rectangle', 'square' - -- p1, p2 for mode 'line' - -- center, radius for mode 'circle' - -- center, radius, start_angle, end_angle for mode 'arc' - -- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide - -- The field names are carefully chosen so that switching modes in midstream - -- remembers previously entered points where that makes sense. - lines = {{mode='text', data=''}}, -- array of lines + lines = {{data=''}}, -- array of strings -- Lines can be too long to fit on screen, in which case they _wrap_ into -- multiple _screen lines_. @@ -82,9 +50,6 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c cursor_x = 0, cursor_y = 0, - current_drawing_mode = 'line', - previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points - font_height = font_height, line_height = line_height, em = App.newText(love.graphics.getFont(), 'm'), -- widest possible character width @@ -109,17 +74,7 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c return result end -- App.initialize_state -function edit.fixup_cursor(State) - for i,line in ipairs(State.lines) do - if line.mode == 'text' then - State.cursor1.line = i - break - end - end -end - function edit.draw(State) - State.button_handlers = {} App.color(Text_color) assert(#State.lines == #State.line_cache) if not Text.le1(State.screen_top1, State.cursor1) then @@ -135,39 +90,14 @@ function edit.draw(State) --? print('draw:', y, line_index, line) if y + State.line_height > App.screen.height then break end State.screen_bottom1 = {line=line_index, pos=nil} - if line.mode == 'text' then ---? print('text.draw', y, line_index) - local startpos = 1 - if line_index == State.screen_top1.line then - startpos = State.screen_top1.pos - end - if line.data == '' then - -- button to insert new drawing - button(State, 'draw', {x=4,y=y+4, w=12,h=12, color={1,1,0}, - icon = icon.insert_drawing, - onpress1 = function() - Drawing.before = snapshot(State, line_index-1, line_index) - table.insert(State.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}}) - table.insert(State.line_cache, line_index, {}) - if State.cursor1.line >= line_index then - State.cursor1.line = State.cursor1.line+1 - end - schedule_save(State) - record_undo_event(State, {before=Drawing.before, after=snapshot(State, line_index-1, line_index+1)}) - end, - }) - end - y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos) - y = y + State.line_height ---? print('=> y', y) - elseif line.mode == 'drawing' then - y = y+Drawing_padding_top - Drawing.draw(State, line_index, y) - y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom - else - print(line.mode) - assert(false) +--? print('text.draw', y, line_index) + local startpos = 1 + if line_index == State.screen_top1.line then + startpos = State.screen_top1.pos end + y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos) + y = y + State.line_height +--? print('=> y', y) end if State.search_term then Text.draw_search_bar(State) @@ -175,7 +105,6 @@ function edit.draw(State) end function edit.update(State, dt) - Drawing.update(State, dt) if State.next_save and State.next_save < Current_time then save_to_disk(State) State.next_save = nil @@ -198,42 +127,26 @@ end function edit.mouse_press(State, x,y, mouse_button) if State.search_term then return end --? print('press', State.selection1.line, State.selection1.pos) - if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then - -- press on a button and it returned 'true' to short-circuit - return - end - for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then - -- delicate dance between cursor, selection and old cursor/selection - -- scenarios: - -- regular press+release: sets cursor, clears selection - -- shift press+release: - -- sets selection to old cursor if not set otherwise leaves it untouched - -- sets cursor - -- press and hold to start a selection: sets selection on press, cursor on release - -- press and hold, then press shift: ignore shift - -- i.e. mouse_release should never look at shift state - State.old_cursor1 = State.cursor1 - State.old_selection1 = State.selection1 - State.mousepress_shift = App.shift_down() - State.selection1 = { - line=line_index, - pos=Text.to_pos_on_line(State, line_index, x, y), - } ---? print('selection', State.selection1.line, State.selection1.pos) - break - end - elseif line.mode == 'drawing' then - local line_cache = State.line_cache[line_index] - if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) then - State.lines.current_drawing_index = line_index - State.lines.current_drawing = line - Drawing.before = snapshot(State, line_index) - Drawing.mouse_press(State, line_index, x,y, mouse_button) - break - end + if Text.in_line(State, line_index, x,y) then + -- delicate dance between cursor, selection and old cursor/selection + -- scenarios: + -- regular press+release: sets cursor, clears selection + -- shift press+release: + -- sets selection to old cursor if not set otherwise leaves it untouched + -- sets cursor + -- press and hold to start a selection: sets selection on press, cursor on release + -- press and hold, then press shift: ignore shift + -- i.e. mouse_released should never look at shift state + State.old_cursor1 = State.cursor1 + State.old_selection1 = State.selection1 + State.mousepress_shift = App.shift_down() + State.selection1 = { + line=line_index, + pos=Text.to_pos_on_line(State, line_index, x, y), + } +--? print('selection', State.selection1.line, State.selection1.pos) + break end end end @@ -241,40 +154,29 @@ end function edit.mouse_release(State, x,y, mouse_button) if State.search_term then return end --? print('release') - if State.lines.current_drawing then - Drawing.mouse_release(State, x,y, mouse_button) - schedule_save(State) - if Drawing.before then - record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)}) - Drawing.before = nil - end - else - for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then ---? print('reset selection') - State.cursor1 = { - line=line_index, - pos=Text.to_pos_on_line(State, line_index, x, y), - } ---? print('cursor', State.cursor1.line, State.cursor1.pos) - if State.mousepress_shift then - if State.old_selection1.line == nil then - State.selection1 = State.old_cursor1 - else - State.selection1 = State.old_selection1 - end - end - State.old_cursor1, State.old_selection1, State.mousepress_shift = nil - if eq(State.cursor1, State.selection1) then - State.selection1 = {} - end - break + for line_index,line in ipairs(State.lines) do + if Text.in_line(State, line_index, x,y) then +--? print('reset selection') + State.cursor1 = { + line=line_index, + pos=Text.to_pos_on_line(State, line_index, x, y), + } +--? print('cursor', State.cursor1.line, State.cursor1.pos) + if State.mousepress_shift then + if State.old_selection1.line == nil then + State.selection1 = State.old_cursor1 + else + State.selection1 = State.old_selection1 end end + State.old_cursor1, State.old_selection1, State.mousepress_shift = nil + if eq(State.cursor1, State.selection1) then + State.selection1 = {} + end + break end ---? print('selection:', State.selection1.line, State.selection1.pos) end +--? print('selection:', State.selection1.line, State.selection1.pos) end function edit.text_input(State, t) @@ -282,25 +184,15 @@ function edit.text_input(State, t) State.search_term = State.search_term..t State.search_text = nil Text.search_next(State) - elseif State.current_drawing_mode == 'name' then - local before = snapshot(State, State.lines.current_drawing_index) - local drawing = State.lines.current_drawing - local p = drawing.points[drawing.pending.target_point] - p.name = p.name..t - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) else - local drawing_index, drawing = Drawing.current_drawing(State) - if drawing_index == nil then - for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll - Text.text_input(State, t) - end + for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll + Text.text_input(State, t) end schedule_save(State) end function edit.keychord_press(State, chord, key) if State.selection1.line and - not State.lines.current_drawing and -- printable character created using shift key => delete selection -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys) (not App.shift_down() or utf8.len(key) == 1) and @@ -359,8 +251,6 @@ function edit.keychord_press(State, chord, key) State.selection1 = deepcopy(src.selection) patch(State.lines, event.after, event.before) patch_placeholders(State.line_cache, event.after, event.before) - -- invalidate various cached bits of lines - State.lines.current_drawing = nil -- if we're scrolling, reclaim all fragments to avoid memory leaks Text.redraw_all(State) schedule_save(State) @@ -374,8 +264,6 @@ function edit.keychord_press(State, chord, key) State.cursor1 = deepcopy(src.cursor) State.selection1 = deepcopy(src.selection) patch(State.lines, event.before, event.after) - -- invalidate various cached bits of lines - State.lines.current_drawing = nil -- if we're scrolling, reclaim all fragments to avoid memory leaks Text.redraw_all(State) schedule_save(State) @@ -416,42 +304,7 @@ function edit.keychord_press(State, chord, key) end schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) - -- dispatch to drawing or text - elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then - -- DON'T reset line_cache.starty here - local drawing_index, drawing = Drawing.current_drawing(State) - if drawing_index then - local before = snapshot(State, drawing_index) - Drawing.keychord_press(State, chord) - record_undo_event(State, {before=before, after=snapshot(State, drawing_index)}) - schedule_save(State) - end - elseif chord == 'escape' and not App.mouse_down(1) then - for _,line in ipairs(State.lines) do - if line.mode == 'drawing' then - line.show_help = false - end - end - elseif State.current_drawing_mode == 'name' then - if chord == 'return' then - State.current_drawing_mode = State.previous_drawing_mode - State.previous_drawing_mode = nil - else - local before = snapshot(State, State.lines.current_drawing_index) - local drawing = State.lines.current_drawing - local p = drawing.points[drawing.pending.target_point] - if chord == 'escape' then - p.name = nil - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) - elseif chord == 'backspace' then - local len = utf8.len(p.name) - local byte_offset = Text.offset(p.name, len-1) - if len == 1 then byte_offset = 0 end - p.name = string.sub(p.name, 1, byte_offset) - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) - end - end - schedule_save(State) + -- dispatch to text else for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll Text.keychord_press(State, chord) diff --git a/file.lua b/file.lua index 2140bb1..d7fcc4e 100644 --- a/file.lua +++ b/file.lua @@ -22,15 +22,11 @@ function load_from_file(infile) while true do local line = infile_next_line() if line == nil then break end - if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated - table.insert(result, load_drawing(infile_next_line)) - else - table.insert(result, {mode='text', data=line}) - end + table.insert(result, {data=line}) end end if #result == 0 then - table.insert(result, {mode='text', data=''}) + table.insert(result, {data=''}) end return result end @@ -41,82 +37,12 @@ function save_to_disk(State) error('failed to write to "'..State.filename..'"') end for _,line in ipairs(State.lines) do - if line.mode == 'drawing' then - store_drawing(outfile, line) - else - outfile:write(line.data) - outfile:write('\n') - end + outfile:write(line.data) + outfile:write('\n') end outfile:close() end -function load_drawing(infile_next_line) - local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}} - while true do - local line = infile_next_line() - assert(line) - if line == '```' then break end - local shape = json.decode(line) - if shape.mode == 'freehand' then - -- no changes needed - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local name = shape.p1.name - shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p2].name = name - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - for i,p in ipairs(shape.vertices) do - local name = p.name - shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.vertices[i]].name = name - end - elseif shape.mode == 'circle' or shape.mode == 'arc' then - local name = shape.center.name - shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.center].name = name - elseif shape.mode == 'deleted' then - -- ignore - else - print(shape.mode) - assert(false) - end - table.insert(drawing.shapes, shape) - end - return drawing -end - -function store_drawing(outfile, drawing) - outfile:write('```lines\n') - for _,shape in ipairs(drawing.shapes) do - if shape.mode == 'freehand' then - outfile:write(json.encode(shape), '\n') - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local line = json.encode({mode=shape.mode, p1=drawing.points[shape.p1], p2=drawing.points[shape.p2]}) - outfile:write(line, '\n') - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - local obj = {mode=shape.mode, vertices={}} - for _,p in ipairs(shape.vertices) do - table.insert(obj.vertices, drawing.points[p]) - end - local line = json.encode(obj) - outfile:write(line, '\n') - elseif shape.mode == 'circle' then - outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius}), '\n') - elseif shape.mode == 'arc' then - outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius, start_angle=shape.start_angle, end_angle=shape.end_angle}), '\n') - elseif shape.mode == 'deleted' then - -- ignore - else - print(shape.mode) - assert(false) - end - end - outfile:write('```\n') -end - -- for tests function load_array(a) local result = {} @@ -125,62 +51,14 @@ function load_array(a) while true do i,line = next_line(a, i) if i == nil then break end ---? print(line) - if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated ---? print('inserting drawing') - i, drawing = load_drawing_from_array(next_line, a, i) ---? print('i now', i) - table.insert(result, drawing) - else ---? print('inserting text') - table.insert(result, {mode='text', data=line}) - end + table.insert(result, {data=line}) end if #result == 0 then - table.insert(result, {mode='text', data=''}) + table.insert(result, {data=''}) end return result end -function load_drawing_from_array(iter, a, i) - local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}} - local line - while true do - i, line = iter(a, i) - assert(i) ---? print(i) - if line == '```' then break end - local shape = json.decode(line) - if shape.mode == 'freehand' then - -- no changes needed - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local name = shape.p1.name - shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p2].name = name - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - for i,p in ipairs(shape.vertices) do - local name = p.name - shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.vertices[i]].name = name - end - elseif shape.mode == 'circle' or shape.mode == 'arc' then - local name = shape.center.name - shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.center].name = name - elseif shape.mode == 'deleted' then - -- ignore - else - print(shape.mode) - assert(false) - end - table.insert(drawing.shapes, shape) - end - return i, drawing -end - function is_absolute_path(path) local os_path_separator = package.config:sub(1,1) if os_path_separator == '/' then diff --git a/main.lua b/main.lua index 8e61b5f..7c2e34c 100644 --- a/main.lua +++ b/main.lua @@ -24,13 +24,6 @@ load_file_from_source_or_save_directory('button.lua') -- both sides require (different parts of) the logging framework load_file_from_source_or_save_directory('log.lua') --- both sides use drawings -load_file_from_source_or_save_directory('icons.lua') -load_file_from_source_or_save_directory('drawing.lua') - load_file_from_source_or_save_directory('geom.lua') - load_file_from_source_or_save_directory('help.lua') -load_file_from_source_or_save_directory('drawing_tests.lua') - -- but some files we want to only load sometimes function App.load() log_new('session') @@ -65,6 +58,11 @@ function App.load() load_file_from_source_or_save_directory('source_undo.lua') load_file_from_source_or_save_directory('colorize.lua') load_file_from_source_or_save_directory('source_text_tests.lua') + load_file_from_source_or_save_directory('icons.lua') + load_file_from_source_or_save_directory('drawing.lua') + load_file_from_source_or_save_directory('geom.lua') + load_file_from_source_or_save_directory('help.lua') + load_file_from_source_or_save_directory('drawing_tests.lua') load_file_from_source_or_save_directory('source_tests.lua') else assert(false, 'unknown app "'..Current_app..'"') diff --git a/run.lua b/run.lua index 45df7c9..fb1f9c1 100644 --- a/run.lua +++ b/run.lua @@ -33,15 +33,11 @@ function run.initialize(arg) Text.redraw_all(Editor_state) Editor_state.screen_top1 = {line=1, pos=1} Editor_state.cursor1 = {line=1, pos=1} - edit.fixup_cursor(Editor_state) else load_from_disk(Editor_state) Text.redraw_all(Editor_state) - if Editor_state.cursor1.line > #Editor_state.lines or Editor_state.lines[Editor_state.cursor1.line].mode ~= 'text' then - edit.fixup_cursor(Editor_state) - end end - love.window.setTitle('lines.love - '..Editor_state.filename) + love.window.setTitle('text.love - '..Editor_state.filename) if #arg > 1 then print('ignoring commandline args after '..arg[1]) @@ -117,8 +113,7 @@ function run.file_drop(file) Editor_state.lines = load_from_file(file) file:close() Text.redraw_all(Editor_state) - edit.fixup_cursor(Editor_state) - love.window.setTitle('lines.love - '..Editor_state.filename) + love.window.setTitle('text.love - '..Editor_state.filename) end function run.draw() diff --git a/select.lua b/select.lua index bae9504..b7cf65f 100644 --- a/select.lua +++ b/select.lua @@ -89,10 +89,8 @@ end function Text.to_pos(State, x,y) for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then - return line_index, Text.to_pos_on_line(State, line_index, x,y) - end + if Text.in_line(State, line_index, x,y) then + return line_index, Text.to_pos_on_line(State, line_index, x,y) end end end @@ -172,9 +170,7 @@ function Text.selection(State) assert(minl < maxl) local result = {State.lines[minl].data:sub(min_offset)} for i=minl+1,maxl-1 do - if State.lines[i].mode == 'text' then - table.insert(result, State.lines[i].data) - end + table.insert(result, State.lines[i].data) end table.insert(result, State.lines[maxl].data:sub(1, max_offset-1)) return table.concat(result, '\n') diff --git a/source.lua b/source.lua index 44cf440..0d12415 100644 --- a/source.lua +++ b/source.lua @@ -78,7 +78,7 @@ function source.initialize() Menu_status_bar_height = 5 + Editor_state.line_height + 5 Editor_state.top = Editor_state.top + Menu_status_bar_height Log_browser_state.top = Log_browser_state.top + Menu_status_bar_height - love.window.setTitle('lines.love - source') + love.window.setTitle('text.love - source') end -- environment for a mutable file of bifolded text @@ -210,7 +210,7 @@ function source.file_drop(file) Text.redraw_all(Editor_state) Editor_state.screen_top1 = {line=1, pos=1} Editor_state.cursor1 = {line=1, pos=1} - love.window.setTitle('lines.love - source') + love.window.setTitle('text.love - source') end -- a copy of source.file_drop when given a filename diff --git a/text.lua b/text.lua index 386553f..961faba 100644 --- a/text.lua +++ b/text.lua @@ -77,7 +77,6 @@ end function Text.populate_screen_line_starting_pos(State, line_index) local line = State.lines[line_index] - if line.mode ~= 'text' then return end local line_cache = State.line_cache[line_index] if line_cache.screen_line_starting_pos then return @@ -104,7 +103,6 @@ end function Text.compute_fragments(State, line_index) --? print('compute_fragments', line_index, 'between', State.left, State.right) local line = State.lines[line_index] - if line.mode ~= 'text' then return end local line_cache = State.line_cache[line_index] if line_cache.fragments then return @@ -214,16 +212,11 @@ function Text.keychord_press(State, chord) end elseif State.cursor1.line > 1 then before = snapshot(State, State.cursor1.line-1, State.cursor1.line) - if State.lines[State.cursor1.line-1].mode == 'drawing' then - table.remove(State.lines, State.cursor1.line-1) - table.remove(State.line_cache, State.cursor1.line-1) - else - -- join lines - State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1 - State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data - table.remove(State.lines, State.cursor1.line) - table.remove(State.line_cache, State.cursor1.line) - end + -- join lines + State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1 + State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data + table.remove(State.lines, State.cursor1.line) + table.remove(State.line_cache, State.cursor1.line) State.cursor1.line = State.cursor1.line-1 end if State.screen_top1.line > #State.lines then @@ -264,10 +257,8 @@ function Text.keychord_press(State, chord) -- no change to State.cursor1.pos end elseif State.cursor1.line < #State.lines then - if State.lines[State.cursor1.line+1].mode == 'text' then - -- join lines - State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data - end + -- join lines + State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data table.remove(State.lines, State.cursor1.line+1) table.remove(State.line_cache, State.cursor1.line+1) end @@ -361,7 +352,7 @@ end function Text.insert_return(State) local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)}) + table.insert(State.lines, State.cursor1.line+1, {data=string.sub(State.lines[State.cursor1.line].data, byte_offset)}) table.insert(State.line_cache, State.cursor1.line+1, {}) State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1) Text.clear_screen_line_cache(State, State.cursor1.line) @@ -377,11 +368,7 @@ function Text.pageup(State) while y >= State.top do --? print(y, top2.line, top2.screen_line, top2.screen_pos) if State.screen_top1.line == 1 and State.screen_top1.pos == 1 then break end - if State.lines[State.screen_top1.line].mode == 'text' then - y = y - State.line_height - elseif State.lines[State.screen_top1.line].mode == 'drawing' then - y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width) - end + y = y - State.line_height top2 = Text.previous_screen_line(State, top2) end State.screen_top1 = Text.to1(State, top2) @@ -415,29 +402,24 @@ function Text.pagedown(State) end function Text.up(State) - assert(State.lines[State.cursor1.line].mode == 'text') --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos) local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1) if screen_line_starting_pos == 1 then --? print('cursor is at first screen line of its line') -- line is done; skip to previous text line - local new_cursor_line = State.cursor1.line - while new_cursor_line > 1 do - new_cursor_line = new_cursor_line-1 - if State.lines[new_cursor_line].mode == 'text' then ---? print('found previous text line') - State.cursor1 = {line=State.cursor1.line-1, pos=nil} - Text.populate_screen_line_starting_pos(State, State.cursor1.line) - -- previous text line found, pick its final screen line ---? print('has multiple screen lines') - local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos ---? print(#screen_line_starting_pos) - screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos] - local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos) - local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset) - State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 - break - end + if State.cursor1.line > 1 then + local new_cursor_line = State.cursor1.line-1 +--? print('found previous text line') + State.cursor1.line = new_cursor_line + Text.populate_screen_line_starting_pos(State, State.cursor1.line) + -- previous text line found, pick its final screen line +--? print('has multiple screen lines') + local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos +--? print(#screen_line_starting_pos) + screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos] + local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos) + local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset) + State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 end else -- move up one screen line in current line @@ -456,22 +438,15 @@ function Text.up(State) end function Text.down(State) - assert(State.lines[State.cursor1.line].mode == 'text') --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) if Text.cursor_at_final_screen_line(State) then -- line is done, skip to next text line --? print('cursor at final screen line of its line') - local new_cursor_line = State.cursor1.line - while new_cursor_line < #State.lines do - new_cursor_line = new_cursor_line+1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1 = { - line = new_cursor_line, - pos = Text.nearest_cursor_pos(State.lines[new_cursor_line].data, State.cursor_x, State.left), - } ---? print(State.cursor1.pos) - break - end + if State.cursor1.line < #State.lines then + local new_cursor_line = State.cursor1.line+1 + State.cursor1.line = new_cursor_line + State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line].data, State.cursor_x, State.left) +--? print(State.cursor1.pos) end if State.cursor1.line > State.screen_bottom1.line then --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos) @@ -573,21 +548,11 @@ function Text.match(s, pos, pat) end function Text.left(State) - assert(State.lines[State.cursor1.line].mode == 'text') if State.cursor1.pos > 1 then State.cursor1.pos = State.cursor1.pos-1 - else - local new_cursor_line = State.cursor1.line - while new_cursor_line > 1 do - new_cursor_line = new_cursor_line-1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1 = { - line = new_cursor_line, - pos = utf8.len(State.lines[new_cursor_line].data) + 1, - } - break - end - end + elseif State.cursor1.line > 1 then + State.cursor1.line = State.cursor1.line-1 + State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1 end if Text.lt1(State.cursor1, State.screen_top1) then local top2 = Text.to2(State, State.screen_top1) @@ -604,18 +569,11 @@ function Text.right(State) end function Text.right_without_scroll(State) - assert(State.lines[State.cursor1.line].mode == 'text') if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then State.cursor1.pos = State.cursor1.pos+1 - else - local new_cursor_line = State.cursor1.line - while new_cursor_line <= #State.lines-1 do - new_cursor_line = new_cursor_line+1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1 = {line=new_cursor_line, pos=1} - break - end - end + elseif State.cursor1.line <= #State.lines-1 then + State.cursor1.line = State.cursor1.line+1 + State.cursor1.pos = 1 end end @@ -639,23 +597,7 @@ function Text.cursor_at_final_screen_line(State) end function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) - local y = State.top - while State.cursor1.line <= #State.lines do - if State.lines[State.cursor1.line].mode == 'text' then - break - end ---? print('cursor skips', State.cursor1.line) - y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width) - State.cursor1.line = State.cursor1.line + 1 - end - -- hack: insert a text line at bottom of file if necessary - if State.cursor1.line > #State.lines then - assert(State.cursor1.line == #State.lines+1) - table.insert(State.lines, {mode='text', data=''}) - table.insert(State.line_cache, {}) - end ---? print(y, App.screen.height, App.screen.height-State.line_height) - if y > App.screen.height - State.line_height then + if State.top > App.screen.height - State.line_height then --? print('scroll up') Text.snap_cursor_to_bottom_of_screen(State) end @@ -675,24 +617,11 @@ function Text.snap_cursor_to_bottom_of_screen(State) while true do --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos) if top2.line == 1 and top2.screen_line == 1 then break end - if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then - local h = State.line_height - if y - h < State.top then - break - end - y = y - h - else - assert(top2.line > 1) - assert(State.lines[top2.line-1].mode == 'drawing') - -- We currently can't draw partial drawings, so either skip it entirely - -- or not at all. - local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width) - if y - h < State.top then - break - end ---? print('skipping drawing of height', h) - y = y - h + local h = State.line_height + if y - h < State.top then + break end + y = y - h top2 = Text.previous_screen_line(State, top2) end --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos) @@ -855,9 +784,6 @@ function Text.x(s, pos) end function Text.to2(State, loc1) - if State.lines[loc1.line].mode == 'drawing' then - return {line=loc1.line, screen_line=1, screen_pos=1} - end local result = {line=loc1.line} local line_cache = State.line_cache[loc1.line] Text.populate_screen_line_starting_pos(State, loc1.line) @@ -920,8 +846,6 @@ function Text.previous_screen_line(State, loc2) return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1} elseif loc2.line == 1 then return loc2 - elseif State.lines[loc2.line-1].mode == 'drawing' then - return {line=loc2.line-1, screen_line=1, screen_pos=1} else local l = State.lines[loc2.line-1] Text.populate_screen_line_starting_pos(State, loc2.line-1) diff --git a/text_tests.lua b/text_tests.lua index 1d5113f..d94b1ec 100644 --- a/text_tests.lua +++ b/text_tests.lua @@ -14,34 +14,6 @@ function test_initial_state() check_eq(Editor_state.screen_top1.pos, 1, 'F - test_initial_state/screen_top:pos') end -function test_click_to_create_drawing() - io.write('\ntest_click_to_create_drawing') - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1) - -- cursor skips drawing to always remain on text - check_eq(#Editor_state.lines, 2, 'F - test_click_to_create_drawing/#lines') - check_eq(Editor_state.cursor1.line, 2, 'F - test_click_to_create_drawing/cursor') -end - -function test_backspace_to_delete_drawing() - io.write('\ntest_backspace_to_delete_drawing') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - -- cursor is on text as always (outside tests this will get initialized correctly) - Editor_state.cursor1.line = 2 - -- backspacing deletes the drawing - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(#Editor_state.lines, 1, 'F - test_backspace_to_delete_drawing/#lines') - check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_to_delete_drawing/cursor') -end - function test_backspace_from_start_of_final_line() io.write('\ntest_backspace_from_start_of_final_line') -- display final line of text with cursor at start of it @@ -977,36 +949,6 @@ function test_pagedown() App.screen.check(y, 'ghi', 'F - test_pagedown/screen:2') end -function test_pagedown_skips_drawings() - io.write('\ntest_pagedown_skips_drawings') - -- some lines of text with a drawing intermixed - local drawing_width = 50 - App.screen.init{width=Editor_state.left+drawing_width, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', -- height 15 - '```lines', '```', -- height 25 - 'def', -- height 15 - 'ghi'} -- height 15 - Text.redraw_all(Editor_state) - check_eq(Editor_state.lines[2].mode, 'drawing', 'F - test_pagedown_skips_drawings/baseline/lines') - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - local drawing_height = Drawing_padding_height + drawing_width/2 -- default - -- initially the screen displays the first line and the drawing - -- 15px margin + 15px line1 + 10px margin + 25px drawing + 10px margin = 75px < screen height 80px - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_pagedown_skips_drawings/baseline/screen:1') - -- after pagedown the screen draws the drawing up top - -- 15px margin + 10px margin + 25px drawing + 10px margin + 15px line3 = 75px < screen height 80px - edit.run_after_keychord(Editor_state, 'pagedown') - check_eq(Editor_state.screen_top1.line, 2, 'F - test_pagedown_skips_drawings/screen_top') - check_eq(Editor_state.cursor1.line, 3, 'F - test_pagedown_skips_drawings/cursor') - y = Editor_state.top + drawing_height - App.screen.check(y, 'def', 'F - test_pagedown_skips_drawings/screen:1') -end - function test_pagedown_often_shows_start_of_wrapping_line() io.write('\ntest_pagedown_often_shows_start_of_wrapping_line') -- draw a few lines ending in part of a wrapping line @@ -2005,7 +1947,7 @@ function test_search() io.write('\ntest_search') App.screen.init{width=120, height=60} Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', 'def', 'ghi', 'deg'} + Editor_state.lines = load_array{'abc', 'def', 'ghi', 'deg'} Text.redraw_all(Editor_state) Editor_state.cursor1 = {line=1, pos=1} Editor_state.screen_top1 = {line=1, pos=1} diff --git a/undo.lua b/undo.lua index d699211..912c949 100644 --- a/undo.lua +++ b/undo.lua @@ -50,8 +50,6 @@ function snapshot(State, s,e) screen_top=deepcopy(State.screen_top1), selection=deepcopy(State.selection1), cursor=deepcopy(State.cursor1), - current_drawing_mode=Drawing_mode, - previous_drawing_mode=State.previous_drawing_mode, lines={}, start_line=s, end_line=e, @@ -60,19 +58,7 @@ function snapshot(State, s,e) -- deep copy lines without cached stuff like text fragments for i=s,e do local line = State.lines[i] - if line.mode == 'text' then - table.insert(event.lines, {mode='text', data=line.data}) - elseif line.mode == 'drawing' then - local points=deepcopy(line.points) ---? print('copying', line.points, 'with', #line.points, 'points into', points) - local shapes=deepcopy(line.shapes) ---? print('copying', line.shapes, 'with', #line.shapes, 'shapes into', shapes) - table.insert(event.lines, {mode='drawing', h=line.h, points=points, shapes=shapes, pending={}}) ---? table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}}) - else - print(line.mode) - assert(false) - end + table.insert(event.lines, {data=line.data}) end return event end |