diff options
-rw-r--r-- | README.md | 51 | ||||
-rw-r--r-- | drawing.lua | 750 | ||||
-rw-r--r-- | drawing_tests.lua | 785 | ||||
-rw-r--r-- | edit.lua | 234 | ||||
-rw-r--r-- | file.lua | 133 | ||||
-rw-r--r-- | geom.lua | 168 | ||||
-rw-r--r-- | help.lua | 156 | ||||
-rw-r--r-- | icons.lua | 58 | ||||
-rw-r--r-- | main.lua | 6 | ||||
-rw-r--r-- | main_tests.lua | 6 | ||||
-rw-r--r-- | search.lua | 16 | ||||
-rw-r--r-- | select.lua | 40 | ||||
-rw-r--r-- | text.lua | 228 | ||||
-rw-r--r-- | text_tests.lua | 96 | ||||
-rw-r--r-- | undo.lua | 14 |
15 files changed, 202 insertions, 2539 deletions
diff --git a/README.md b/README.md index 31c2658..7c49842 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 @@ -35,7 +36,7 @@ For shortcuts while editing drawings, consult the online help. Either: * 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 @@ -46,8 +47,7 @@ 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. @@ -80,28 +80,19 @@ 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. +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: -## Associated tools +* 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 -* https://codeberg.org/akkartik/lines2md exports lines.love files to Markdown - and (non-editable) SVG. +Further forks are encouraged. If you show me your fork, I'll link to it here. ## Feedback diff --git a/drawing.lua b/drawing.lua deleted file mode 100644 index 8263dd9..0000000 --- a/drawing.lua +++ /dev/null @@ -1,750 +0,0 @@ --- primitives for editing drawings -Drawing = {} -require 'drawing_tests' - --- All drawings span 100% of some conceptual 'page width' and divide it up --- into 256 parts. -function Drawing.draw(State, line_index, y) - local line = State.lines[line_index] - local line_cache = State.line_cache[line_index] - line_cache.starty = y - local pmx,pmy = App.mouse_x(), App.mouse_y() - if pmx < State.right and pmy > line_cache.starty and pmy < line_cache.starty+Drawing.pixels(line.h, State.width) then - App.color(Icon_color) - love.graphics.rectangle('line', State.left,line_cache.starty, State.width,Drawing.pixels(line.h, State.width)) - if icon[State.current_drawing_mode] then - icon[State.current_drawing_mode](State.right-22, line_cache.starty+4) - else - icon[State.previous_drawing_mode](State.right-22, line_cache.starty+4) - end - - if App.mouse_down(1) and love.keyboard.isDown('h') then - draw_help_with_mouse_pressed(State, line_index) - return - end - end - - if line.show_help then - draw_help_without_mouse_pressed(State, line_index) - return - end - - local mx = Drawing.coord(pmx-State.left, State.width) - local my = Drawing.coord(pmy-line_cache.starty, State.width) - - for _,shape in ipairs(line.shapes) do - assert(shape) - if geom.on_shape(mx,my, line, shape) then - App.color(Focus_stroke_color) - else - App.color(Stroke_color) - end - Drawing.draw_shape(line, shape, line_cache.starty, State.left,State.right) - end - - local function px(x) return Drawing.pixels(x, State.width)+State.left end - local function py(y) return Drawing.pixels(y, State.width)+line_cache.starty end - for i,p in ipairs(line.points) do - if p.deleted == nil then - if Drawing.near(p, mx,my, State.width) then - App.color(Focus_stroke_color) - love.graphics.circle('line', px(p.x),py(p.y), Same_point_distance) - else - App.color(Stroke_color) - love.graphics.circle('fill', px(p.x),py(p.y), 2) - end - if p.name then - -- TODO: clip - local x,y = px(p.x)+5, py(p.y)+5 - love.graphics.print(p.name, x,y) - if State.current_drawing_mode == 'name' and i == line.pending.target_point then - -- create a faint red box for the name - App.color(Current_name_background_color) - local name_text - -- TODO: avoid computing name width on every repaint - if p.name == '' then - name_text = State.em - else - name_text = App.newText(love.graphics.getFont(), p.name) - end - love.graphics.rectangle('fill', x,y, App.width(name_text), State.line_height) - end - end - end - end - App.color(Current_stroke_color) - Drawing.draw_pending_shape(line, line_cache.starty, State.left,State.right) -end - -function Drawing.draw_shape(drawing, shape, top, left,right) - local width = right-left - local function px(x) return Drawing.pixels(x, width)+left end - local function py(y) return Drawing.pixels(y, width)+top end - if shape.mode == 'freehand' then - local prev = nil - for _,point in ipairs(shape.points) do - if prev then - love.graphics.line(px(prev.x),py(prev.y), px(point.x),py(point.y)) - end - prev = point - end - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local p1 = drawing.points[shape.p1] - local p2 = drawing.points[shape.p2] - love.graphics.line(px(p1.x),py(p1.y), px(p2.x),py(p2.y)) - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - local prev = nil - for _,point in ipairs(shape.vertices) do - local curr = drawing.points[point] - if prev then - love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y)) - end - prev = curr - end - -- close the loop - local curr = drawing.points[shape.vertices[1]] - love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y)) - elseif shape.mode == 'circle' then - -- TODO: clip - local center = drawing.points[shape.center] - love.graphics.circle('line', px(center.x),py(center.y), Drawing.pixels(shape.radius, width)) - elseif shape.mode == 'arc' then - local center = drawing.points[shape.center] - love.graphics.arc('line', 'open', px(center.x),py(center.y), Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360) - elseif shape.mode == 'deleted' then - -- ignore - else - print(shape.mode) - assert(false) - end -end - -function Drawing.draw_pending_shape(drawing, top, left,right) - local width = right-left - local pmx,pmy = App.mouse_x(), App.mouse_y() - local function px(x) return Drawing.pixels(x, width)+left end - local function py(y) return Drawing.pixels(y, width)+top end - local mx = Drawing.coord(pmx-left, width) - local my = Drawing.coord(pmy-top, width) - -- recreate pixels from coords to precisely mimic how the drawing will look - -- after mouse_released - pmx,pmy = px(mx), py(my) - local shape = drawing.pending - if shape.mode == nil then - -- nothing pending - elseif shape.mode == 'freehand' then - local shape_copy = deepcopy(shape) - Drawing.smoothen(shape_copy) - Drawing.draw_shape(drawing, shape_copy, top, left,right) - elseif shape.mode == 'line' then - if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then - return - end - local p1 = drawing.points[shape.p1] - love.graphics.line(px(p1.x),py(p1.y), pmx,pmy) - elseif shape.mode == 'manhattan' then - if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then - return - end - local p1 = drawing.points[shape.p1] - if math.abs(mx-p1.x) > math.abs(my-p1.y) then - love.graphics.line(px(p1.x),py(p1.y), pmx, py(p1.y)) - else - love.graphics.line(px(p1.x),py(p1.y), px(p1.x),pmy) - end - elseif shape.mode == 'polygon' then - -- don't close the loop on a pending polygon - local prev = nil - for _,point in ipairs(shape.vertices) do - local curr = drawing.points[point] - if prev then - love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y)) - end - prev = curr - end - love.graphics.line(px(prev.x),py(prev.y), pmx,pmy) - elseif shape.mode == 'rectangle' then - local first = drawing.points[shape.vertices[1]] - if #shape.vertices == 1 then - love.graphics.line(px(first.x),py(first.y), pmx,pmy) - return - end - local second = drawing.points[shape.vertices[2]] - local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my) - love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y)) - love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy)) - love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy)) - love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y)) - elseif shape.mode == 'square' then - local first = drawing.points[shape.vertices[1]] - if #shape.vertices == 1 then - love.graphics.line(px(first.x),py(first.y), pmx,pmy) - return - end - local second = drawing.points[shape.vertices[2]] - local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my) - love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y)) - love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy)) - love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy)) - love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y)) - elseif shape.mode == 'circle' then - local center = drawing.points[shape.center] - if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then - return - end - local cx,cy = px(center.x), py(center.y) - love.graphics.circle('line', cx,cy, geom.dist(cx,cy, App.mouse_x(),App.mouse_y())) - elseif shape.mode == 'arc' then - local center = drawing.points[shape.center] - if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then - return - end - shape.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, shape.end_angle) - local cx,cy = px(center.x), py(center.y) - love.graphics.arc('line', 'open', cx,cy, Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360) - elseif shape.mode == 'move' then - -- nothing pending; changes are immediately committed - elseif shape.mode == 'name' then - -- nothing pending; changes are immediately committed - else - print(shape.mode) - assert(false) - end -end - -function Drawing.in_drawing(drawing, line_cache, x,y, left,right) - if line_cache.starty == nil then return false end -- outside current page - local width = right-left - return y >= line_cache.starty and y < line_cache.starty + Drawing.pixels(drawing.h, width) and x >= left and x < right -end - -function Drawing.mouse_pressed(State, drawing_index, x,y, button) - local drawing = State.lines[drawing_index] - local line_cache = State.line_cache[drawing_index] - local cx = Drawing.coord(x-State.left, State.width) - local cy = Drawing.coord(y-line_cache.starty, State.width) - if State.current_drawing_mode == 'freehand' then - drawing.pending = {mode=State.current_drawing_mode, points={{x=cx, y=cy}}} - elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width) - drawing.pending = {mode=State.current_drawing_mode, p1=j} - elseif State.current_drawing_mode == 'polygon' or State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width) - drawing.pending = {mode=State.current_drawing_mode, vertices={j}} - elseif State.current_drawing_mode == 'circle' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width) - drawing.pending = {mode=State.current_drawing_mode, center=j} - elseif State.current_drawing_mode == 'move' then - -- all the action is in mouse_released - elseif State.current_drawing_mode == 'name' then - -- nothing - else - print(State.current_drawing_mode) - assert(false) - end -end - --- a couple of operations on drawings need to constantly check the state of the mouse -function Drawing.update(State) - if State.lines.current_drawing == nil then return end - local drawing = State.lines.current_drawing - local line_cache = State.line_cache[State.lines.current_drawing_index] - assert(drawing.mode == 'drawing') - local pmx, pmy = App.mouse_x(), App.mouse_y() - local mx = Drawing.coord(pmx-State.left, State.width) - local my = Drawing.coord(pmy-line_cache.starty, State.width) - if App.mouse_down(1) then - if Drawing.in_drawing(drawing, line_cache, pmx,pmy, State.left,State.right) then - if drawing.pending.mode == 'freehand' then - table.insert(drawing.pending.points, {x=mx, y=my}) - elseif drawing.pending.mode == 'move' then - drawing.pending.target_point.x = mx - drawing.pending.target_point.y = my - Drawing.relax_constraints(drawing, drawing.pending.target_point_index) - end - end - elseif State.current_drawing_mode == 'move' then - if Drawing.in_drawing(drawing, line_cache, pmx, pmy, State.left,State.right) then - drawing.pending.target_point.x = mx - drawing.pending.target_point.y = my - Drawing.relax_constraints(drawing, drawing.pending.target_point_index) - end - else - -- do nothing - end -end - -function Drawing.relax_constraints(drawing, p) - for _,shape in ipairs(drawing.shapes) do - if shape.mode == 'manhattan' then - if shape.p1 == p then - shape.mode = 'line' - elseif shape.p2 == p then - shape.mode = 'line' - end - elseif shape.mode == 'rectangle' or shape.mode == 'square' then - for _,v in ipairs(shape.vertices) do - if v == p then - shape.mode = 'polygon' - end - end - end - end -end - -function Drawing.mouse_released(State, x,y, button) - if State.current_drawing_mode == 'move' then - State.current_drawing_mode = State.previous_drawing_mode - State.previous_drawing_mode = nil - if State.lines.current_drawing then - State.lines.current_drawing.pending = {} - State.lines.current_drawing = nil - end - elseif State.lines.current_drawing then - local drawing = State.lines.current_drawing - local line_cache = State.line_cache[State.lines.current_drawing_index] - if drawing.pending then - if drawing.pending.mode == nil then - -- nothing pending - elseif drawing.pending.mode == 'freehand' then - -- the last point added during update is good enough - Drawing.smoothen(drawing.pending) - table.insert(drawing.shapes, drawing.pending) - elseif drawing.pending.mode == 'line' then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx,my, State.width) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'manhattan' then - local p1 = drawing.points[drawing.pending.p1] - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - if math.abs(mx-p1.x) > math.abs(my-p1.y) then - drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx, p1.y, State.width) - else - drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, State.width) - end - local p2 = drawing.points[drawing.pending.p2] - App.mouse_move(State.left+Drawing.pixels(p2.x, State.width), line_cache.starty+Drawing.pixels(p2.y, State.width)) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'polygon' then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, mx,my, State.width)) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'rectangle' then - assert(#drawing.pending.vertices <= 2) - if #drawing.pending.vertices == 2 then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - local first = drawing.points[drawing.pending.vertices[1]] - local second = drawing.points[drawing.pending.vertices[2]] - local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width)) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width)) - table.insert(drawing.shapes, drawing.pending) - end - else - -- too few points; draw nothing - end - elseif drawing.pending.mode == 'square' then - assert(#drawing.pending.vertices <= 2) - if #drawing.pending.vertices == 2 then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - local first = drawing.points[drawing.pending.vertices[1]] - local second = drawing.points[drawing.pending.vertices[2]] - local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width)) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width)) - table.insert(drawing.shapes, drawing.pending) - end - end - elseif drawing.pending.mode == 'circle' then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - local center = drawing.points[drawing.pending.center] - drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my)) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'arc' then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - local center = drawing.points[drawing.pending.center] - drawing.pending.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, drawing.pending.end_angle) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'name' then - -- drop it - else - print(drawing.pending.mode) - assert(false) - end - State.lines.current_drawing.pending = {} - State.lines.current_drawing = nil - end - end -end - -function Drawing.keychord_pressed(State, chord) - if chord == 'C-p' and not App.mouse_down(1) then - State.current_drawing_mode = 'freehand' - elseif App.mouse_down(1) and chord == 'l' then - State.current_drawing_mode = 'line' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width) - elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then - drawing.pending.p1 = drawing.pending.vertices[1] - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.p1 = drawing.pending.center - end - drawing.pending.mode = 'line' - elseif chord == 'C-l' and not App.mouse_down(1) then - State.current_drawing_mode = 'line' - elseif App.mouse_down(1) and chord == 'm' then - State.current_drawing_mode = 'manhattan' - local drawing = Drawing.select_drawing_at_mouse(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width) - elseif drawing.pending.mode == 'line' then - -- do nothing - elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then - drawing.pending.p1 = drawing.pending.vertices[1] - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.p1 = drawing.pending.center - end - drawing.pending.mode = 'manhattan' - elseif chord == 'C-m' and not App.mouse_down(1) then - State.current_drawing_mode = 'manhattan' - elseif chord == 'C-g' and not App.mouse_down(1) then - State.current_drawing_mode = 'polygon' - elseif App.mouse_down(1) and chord == 'g' then - State.current_drawing_mode = 'polygon' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)} - elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then - if drawing.pending.vertices == nil then - drawing.pending.vertices = {drawing.pending.p1} - end - elseif drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then - -- reuse existing vertices - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.vertices = {drawing.pending.center} - end - drawing.pending.mode = 'polygon' - elseif chord == 'C-r' and not App.mouse_down(1) then - State.current_drawing_mode = 'rectangle' - elseif App.mouse_down(1) and chord == 'r' then - State.current_drawing_mode = 'rectangle' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)} - elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then - if drawing.pending.vertices == nil then - drawing.pending.vertices = {drawing.pending.p1} - end - elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'square' then - -- reuse existing (1-2) vertices - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.vertices = {drawing.pending.center} - end - drawing.pending.mode = 'rectangle' - elseif chord == 'C-s' and not App.mouse_down(1) then - State.current_drawing_mode = 'square' - elseif App.mouse_down(1) and chord == 's' then - State.current_drawing_mode = 'square' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)} - elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then - if drawing.pending.vertices == nil then - drawing.pending.vertices = {drawing.pending.p1} - end - elseif drawing.pending.mode == 'polygon' then - while #drawing.pending.vertices > 2 do - table.remove(drawing.pending.vertices) - end - elseif drawing.pending.mode == 'rectangle' then - -- reuse existing (1-2) vertices - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.vertices = {drawing.pending.center} - end - drawing.pending.mode = 'square' - elseif App.mouse_down(1) and chord == 'p' and State.current_drawing_mode == 'polygon' then - local _,drawing,line_cache = Drawing.current_drawing(State) - local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width) - local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width) - table.insert(drawing.pending.vertices, j) - elseif App.mouse_down(1) and chord == 'p' and (State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square') then - local _,drawing,line_cache = Drawing.current_drawing(State) - local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width) - local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width) - while #drawing.pending.vertices >= 2 do - table.remove(drawing.pending.vertices) - end - table.insert(drawing.pending.vertices, j) - elseif chord == 'C-o' and not App.mouse_down(1) then - State.current_drawing_mode = 'circle' - elseif App.mouse_down(1) and chord == 'a' and State.current_drawing_mode == 'circle' then - local _,drawing,line_cache = Drawing.current_drawing(State) - drawing.pending.mode = 'arc' - local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width) - local center = drawing.points[drawing.pending.center] - drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my)) - drawing.pending.start_angle = geom.angle(center.x,center.y, mx,my) - elseif App.mouse_down(1) and chord == 'o' then - State.current_drawing_mode = 'circle' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.center = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width) - elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then - drawing.pending.center = drawing.pending.p1 - elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then - drawing.pending.center = drawing.pending.vertices[1] - end - drawing.pending.mode = 'circle' - elseif chord == 'C-u' and not App.mouse_down(1) then - local drawing_index,drawing,line_cache,i,p = Drawing.select_point_at_mouse(State) - if drawing then - if State.previous_drawing_mode == nil then - State.previous_drawing_mode = State.current_drawing_mode - end - State.current_drawing_mode = 'move' - drawing.pending = {mode=State.current_drawing_mode, target_point=p, target_point_index=i} - State.lines.current_drawing_index = drawing_index - State.lines.current_drawing = drawing - end - elseif chord == 'C-n' and not App.mouse_down(1) then - local drawing_index,drawing,line_cache,point_index,p = Drawing.select_point_at_mouse(State) - if drawing then - if State.previous_drawing_mode == nil then - -- don't clobber - State.previous_drawing_mode = State.current_drawing_mode - end - State.current_drawing_mode = 'name' - p.name = '' - drawing.pending = {mode=State.current_drawing_mode, target_point=point_index} - State.lines.current_drawing_index = drawing_index - State.lines.current_drawing = drawing - end - elseif chord == 'C-d' and not App.mouse_down(1) then - local _,drawing,_,i,p = Drawing.select_point_at_mouse(State) - if drawing then - for _,shape in ipairs(drawing.shapes) do - if Drawing.contains_point(shape, i) then - if shape.mode == 'polygon' then - local idx = table.find(shape.vertices, i) - assert(idx) - table.remove(shape.vertices, idx) - if #shape.vertices < 3 then - shape.mode = 'deleted' - end - else - shape.mode = 'deleted' - end - end - end - drawing.points[i].deleted = true - end - local drawing,_,_,shape = Drawing.select_shape_at_mouse(State) - if drawing then - shape.mode = 'deleted' - end - elseif chord == 'C-h' and not App.mouse_down(1) then - local drawing = Drawing.select_drawing_at_mouse(State) - if drawing then - drawing.show_help = true - end - elseif chord == 'escape' and App.mouse_down(1) then - local _,drawing = Drawing.current_drawing(State) - drawing.pending = {} - end -end - -function Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y) - if firstx == secondx then - return x,secondy, x,firsty - end - if firsty == secondy then - return secondx,y, firstx,y - end - local first_slope = (secondy-firsty)/(secondx-firstx) - -- slope of second edge: - -- -1/first_slope - -- equation of line containing the second edge: - -- y-secondy = -1/first_slope*(x-secondx) - -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0 - -- now we want to find the point on this line that's closest to the mouse pointer. - -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation - local a = 1/first_slope - local c = -secondy - secondx/first_slope - local thirdx = round(((x-a*y) - a*c) / (a*a + 1)) - local thirdy = round((a*(-x + a*y) - c) / (a*a + 1)) - -- slope of third edge = first_slope - -- equation of line containing third edge: - -- y - thirdy = first_slope*(x-thirdx) - -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0 - -- now we want to find the point on this line that's closest to the first point - local a = -first_slope - local c = -thirdy + thirdx*first_slope - local fourthx = round(((firstx-a*firsty) - a*c) / (a*a + 1)) - local fourthy = round((a*(-firstx + a*firsty) - c) / (a*a + 1)) - return thirdx,thirdy, fourthx,fourthy -end - -function Drawing.complete_square(firstx,firsty, secondx,secondy, x,y) - -- use x,y only to decide which side of the first edge to complete the square on - local deltax = secondx-firstx - local deltay = secondy-firsty - local thirdx = secondx+deltay - local thirdy = secondy-deltax - if not geom.same_side(firstx,firsty, secondx,secondy, thirdx,thirdy, x,y) then - deltax = -deltax - deltay = -deltay - thirdx = secondx+deltay - thirdy = secondy-deltax - end - local fourthx = firstx+deltay - local fourthy = firsty-deltax - return thirdx,thirdy, fourthx,fourthy -end - -function Drawing.current_drawing(State) - local x, y = App.mouse_x(), App.mouse_y() - for drawing_index,drawing in ipairs(State.lines) do - if drawing.mode == 'drawing' then - local line_cache = State.line_cache[drawing_index] - if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then - return drawing_index,drawing,line_cache - end - end - end - return nil -end - -function Drawing.select_shape_at_mouse(State) - for drawing_index,drawing in ipairs(State.lines) do - if drawing.mode == 'drawing' then - local x, y = App.mouse_x(), App.mouse_y() - local line_cache = State.line_cache[drawing_index] - if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - for i,shape in ipairs(drawing.shapes) do - assert(shape) - if geom.on_shape(mx,my, drawing, shape) then - return drawing,line_cache,i,shape - end - end - end - end - end -end - -function Drawing.select_point_at_mouse(State) - for drawing_index,drawing in ipairs(State.lines) do - if drawing.mode == 'drawing' then - local x, y = App.mouse_x(), App.mouse_y() - local line_cache = State.line_cache[drawing_index] - if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - for i,point in ipairs(drawing.points) do - assert(point) - if Drawing.near(point, mx,my, State.width) then - return drawing_index,drawing,line_cache,i,point - end - end - end - end - end -end - -function Drawing.select_drawing_at_mouse(State) - for drawing_index,drawing in ipairs(State.lines) do - if drawing.mode == 'drawing' then - local x, y = App.mouse_x(), App.mouse_y() - local line_cache = State.line_cache[drawing_index] - if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then - return drawing - end - end - end -end - -function Drawing.contains_point(shape, p) - if shape.mode == 'freehand' then - -- not supported - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - return shape.p1 == p or shape.p2 == p - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - return table.find(shape.vertices, p) - elseif shape.mode == 'circle' then - return shape.center == p - elseif shape.mode == 'arc' then - return shape.center == p - -- ugh, how to support angles - elseif shape.mode == 'deleted' then - -- already done - else - print(shape.mode) - assert(false) - end -end - -function Drawing.smoothen(shape) - assert(shape.mode == 'freehand') - for _=1,7 do - for i=2,#shape.points-1 do - local a = shape.points[i-1] - local b = shape.points[i] - local c = shape.points[i+1] - b.x = round((a.x + b.x + c.x)/3) - b.y = round((a.y + b.y + c.y)/3) - end - end -end - -function round(num) - return math.floor(num+.5) -end - -function Drawing.insert_point(points, x,y) - table.insert(points, {x=x, y=y}) - return #points -end - -function Drawing.find_or_insert_point(points, x,y, width) - -- check if UI would snap the two points together - for i,point in ipairs(points) do - if Drawing.near(point, x,y, width) then - return i - end - end - table.insert(points, {x=x, y=y}) - return #points -end - -function Drawing.near(point, x,y, width) - local px,py = Drawing.pixels(x, width),Drawing.pixels(y, width) - local cx,cy = Drawing.pixels(point.x, width), Drawing.pixels(point.y, width) - return (cx-px)*(cx-px) + (cy-py)*(cy-py) < Same_point_distance*Same_point_distance -end - -function Drawing.pixels(n, width) -- parts to pixels - return math.floor(n*width/256) -end -function Drawing.coord(n, width) -- pixels to parts - return math.floor(n*256/width) -end - -function table.find(h, x) - for k,v in pairs(h) do - if v == x then - return k - end - end -end diff --git a/drawing_tests.lua b/drawing_tests.lua deleted file mode 100644 index f1e39a6..0000000 --- a/drawing_tests.lua +++ /dev/null @@ -1,785 +0,0 @@ --- major tests for drawings --- We minimize assumptions about specific pixels, and try to test at the level --- of specific shapes. In particular, no tests of freehand drawings. - -function test_creating_drawing_saves() - io.write('\ntest_creating_drawing_saves') - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - -- click on button to create drawing - edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1) - -- file not immediately saved - edit.update(Editor_state, 0.01) - check_nil(App.filesystem['foo'], 'F - test_creating_drawing_saves/early') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- filesystem contains drawing and an empty line of text - check_eq(App.filesystem['foo'], '```lines\n```\n\n', 'F - test_creating_drawing_saves') -end - -function test_draw_line() - io.write('\ntest_draw_line') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_line/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_line/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_line/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_line/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_line/baseline/#shapes') - -- draw a line - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_line/#shapes') - check_eq(#drawing.points, 2, 'F - test_draw_line/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_draw_line/p1:x') - check_eq(p1.y, 6, 'F - test_draw_line/p1:y') - check_eq(p2.x, 35, 'F - test_draw_line/p2:x') - check_eq(p2.y, 36, 'F - test_draw_line/p2:y') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- The format on disk isn't perfectly stable. Table fields can be reordered. - -- So just reload from disk to verify. - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_line/save/#shapes') - check_eq(#drawing.points, 2, 'F - test_draw_line/save/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/save/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_draw_line/save/p1:x') - check_eq(p1.y, 6, 'F - test_draw_line/save/p1:y') - check_eq(p2.x, 35, 'F - test_draw_line/save/p2:x') - check_eq(p2.y, 36, 'F - test_draw_line/save/p2:y') -end - -function test_draw_horizontal_line() - io.write('\ntest_draw_horizontal_line') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'manhattan' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_horizontal_line/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_horizontal_line/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_horizontal_line/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_horizontal_line/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_horizontal_line/baseline/#shapes') - -- draw a line that is more horizontal than vertical - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_horizontal_line/#shapes') - check_eq(#drawing.points, 2, 'F - test_draw_horizontal_line/#points') - check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_draw_horizontal_line/shape_mode') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_draw_horizontal_line/p1:x') - check_eq(p1.y, 6, 'F - test_draw_horizontal_line/p1:y') - check_eq(p2.x, 35, 'F - test_draw_horizontal_line/p2:x') - check_eq(p2.y, p1.y, 'F - test_draw_horizontal_line/p2:y') -end - -function test_draw_circle() - io.write('\ntest_draw_circle') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_circle/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle/baseline/#shapes') - -- draw a circle - App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing - edit.run_after_keychord(Editor_state, 'C-o') - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_circle/#shapes') - check_eq(#drawing.points, 1, 'F - test_draw_circle/#points') - check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode') - check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle/radius') - local center = drawing.points[drawing.shapes[1].center] - check_eq(center.x, 35, 'F - test_draw_circle/center:x') - check_eq(center.y, 36, 'F - test_draw_circle/center:y') -end - -function test_cancel_stroke() - io.write('\ntest_cancel_stroke') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_cancel_stroke/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_cancel_stroke/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_cancel_stroke/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_cancel_stroke/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_cancel_stroke/baseline/#shapes') - -- start drawing a line - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - -- cancel - edit.run_after_keychord(Editor_state, 'escape') - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 0, 'F - test_cancel_stroke/#shapes') -end - -function test_keys_do_not_affect_shape_when_mouse_up() - io.write('\ntest_keys_do_not_affect_shape_when_mouse_up') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- hover over drawing and press 'o' without holding mouse - App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing - edit.run_after_keychord(Editor_state, 'o') - -- no change to drawing mode - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_keys_do_not_affect_shape_when_mouse_up/drawing_mode') - -- no change to text either because we didn't run the textinput event -end - -function test_draw_circle_mid_stroke() - io.write('\ntest_draw_circle_mid_stroke') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_circle_mid_stroke/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle_mid_stroke/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle_mid_stroke/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle_mid_stroke/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle_mid_stroke/baseline/#shapes') - -- draw a circle - App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_keychord(Editor_state, 'o') - edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_circle_mid_stroke/#shapes') - check_eq(#drawing.points, 1, 'F - test_draw_circle_mid_stroke/#points') - check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode') - check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle_mid_stroke/radius') - local center = drawing.points[drawing.shapes[1].center] - check_eq(center.x, 35, 'F - test_draw_circle_mid_stroke/center:x') - check_eq(center.y, 36, 'F - test_draw_circle_mid_stroke/center:y') -end - -function test_draw_arc() - io.write('\ntest_draw_arc') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'circle' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_arc/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_arc/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_arc/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_arc/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_arc/baseline/#shapes') - -- draw an arc - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - App.mouse_move(Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'a') -- arc mode - edit.run_after_mouse_release(Editor_state, Editor_state.left+35+50, Editor_state.top+Drawing_padding_top+36+50, 1) -- 45° - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_arc/#shapes') - check_eq(#drawing.points, 1, 'F - test_draw_arc/#points') - check_eq(drawing.shapes[1].mode, 'arc', 'F - test_draw_horizontal_line/shape_mode') - local arc = drawing.shapes[1] - check_eq(arc.radius, 30, 'F - test_draw_arc/radius') - local center = drawing.points[arc.center] - check_eq(center.x, 35, 'F - test_draw_arc/center:x') - check_eq(center.y, 36, 'F - test_draw_arc/center:y') - check_eq(arc.start_angle, 0, 'F - test_draw_arc/start:angle') - check_eq(arc.end_angle, math.pi/4, 'F - test_draw_arc/end:angle') -end - -function test_draw_polygon() - io.write('\ntest_draw_polygon') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_polygon/baseline/drawing_mode') - check_eq(#Editor_state.lines, 2, 'F - test_draw_polygon/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_polygon/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_polygon/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_polygon/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_polygon/baseline/#shapes') - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_keychord(Editor_state, 'g') -- polygon mode - -- second point - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'p') -- add point - -- final point - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_polygon/#shapes') - check_eq(#drawing.points, 3, 'F - test_draw_polygon/vertices') - local shape = drawing.shapes[1] - check_eq(shape.mode, 'polygon', 'F - test_draw_polygon/shape_mode') - check_eq(#shape.vertices, 3, 'F - test_draw_polygon/vertices') - local p = drawing.points[shape.vertices[1]] - check_eq(p.x, 5, 'F - test_draw_polygon/p1:x') - check_eq(p.y, 6, 'F - test_draw_polygon/p1:y') - local p = drawing.points[shape.vertices[2]] - check_eq(p.x, 65, 'F - test_draw_polygon/p2:x') - check_eq(p.y, 36, 'F - test_draw_polygon/p2:y') - local p = drawing.points[shape.vertices[3]] - check_eq(p.x, 35, 'F - test_draw_polygon/p3:x') - check_eq(p.y, 26, 'F - test_draw_polygon/p3:y') -end - -function test_draw_rectangle() - io.write('\ntest_draw_rectangle') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle/baseline/drawing_mode') - check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle/baseline/#shapes') - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_keychord(Editor_state, 'r') -- rectangle mode - -- second point/first edge - App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45) - edit.run_after_keychord(Editor_state, 'p') - -- override second point/first edge - App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76) - edit.run_after_keychord(Editor_state, 'p') - -- release (decides 'thickness' of rectangle perpendicular to first edge) - edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_rectangle/#shapes') - check_eq(#drawing.points, 5, 'F - test_draw_rectangle/#points') -- currently includes every point added - local shape = drawing.shapes[1] - check_eq(shape.mode, 'rectangle', 'F - test_draw_rectangle/shape_mode') - check_eq(#shape.vertices, 4, 'F - test_draw_rectangle/vertices') - local p = drawing.points[shape.vertices[1]] - check_eq(p.x, 35, 'F - test_draw_rectangle/p1:x') - check_eq(p.y, 36, 'F - test_draw_rectangle/p1:y') - local p = drawing.points[shape.vertices[2]] - check_eq(p.x, 75, 'F - test_draw_rectangle/p2:x') - check_eq(p.y, 76, 'F - test_draw_rectangle/p2:y') - local p = drawing.points[shape.vertices[3]] - check_eq(p.x, 70, 'F - test_draw_rectangle/p3:x') - check_eq(p.y, 81, 'F - test_draw_rectangle/p3:y') - local p = drawing.points[shape.vertices[4]] - check_eq(p.x, 30, 'F - test_draw_rectangle/p4:x') - check_eq(p.y, 41, 'F - test_draw_rectangle/p4:y') -end - -function test_draw_rectangle_intermediate() - io.write('\ntest_draw_rectangle_intermediate') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle_intermediate/baseline/drawing_mode') - check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle_intermediate/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle_intermediate/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle_intermediate/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle_intermediate/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle_intermediate/baseline/#shapes') - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_keychord(Editor_state, 'r') -- rectangle mode - -- second point/first edge - App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45) - edit.run_after_keychord(Editor_state, 'p') - -- override second point/first edge - App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76) - edit.run_after_keychord(Editor_state, 'p') - local drawing = Editor_state.lines[1] - check_eq(#drawing.points, 3, 'F - test_draw_rectangle_intermediate/#points') -- currently includes every point added - local pending = drawing.pending - check_eq(pending.mode, 'rectangle', 'F - test_draw_rectangle_intermediate/shape_mode') - check_eq(#pending.vertices, 2, 'F - test_draw_rectangle_intermediate/vertices') - local p = drawing.points[pending.vertices[1]] - check_eq(p.x, 35, 'F - test_draw_rectangle_intermediate/p1:x') - check_eq(p.y, 36, 'F - test_draw_rectangle_intermediate/p1:y') - local p = drawing.points[pending.vertices[2]] - check_eq(p.x, 75, 'F - test_draw_rectangle_intermediate/p2:x') - check_eq(p.y, 76, 'F - test_draw_rectangle_intermediate/p2:y') - -- outline of rectangle is drawn based on where the mouse is, but we can't check that so far -end - -function test_draw_square() - io.write('\ntest_draw_square') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_square/baseline/drawing_mode') - check_eq(#Editor_state.lines, 2, 'F - test_draw_square/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_square/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_square/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_square/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_square/baseline/#shapes') - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_keychord(Editor_state, 's') -- square mode - -- second point/first edge - App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45) - edit.run_after_keychord(Editor_state, 'p') - -- override second point/first edge - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+66) - edit.run_after_keychord(Editor_state, 'p') - -- release (decides which side of first edge to draw square on) - edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_square/#shapes') - check_eq(#drawing.points, 5, 'F - test_draw_square/#points') -- currently includes every point added - check_eq(drawing.shapes[1].mode, 'square', 'F - test_draw_square/shape_mode') - check_eq(#drawing.shapes[1].vertices, 4, 'F - test_draw_square/vertices') - local p = drawing.points[drawing.shapes[1].vertices[1]] - check_eq(p.x, 35, 'F - test_draw_square/p1:x') - check_eq(p.y, 36, 'F - test_draw_square/p1:y') - local p = drawing.points[drawing.shapes[1].vertices[2]] - check_eq(p.x, 65, 'F - test_draw_square/p2:x') - check_eq(p.y, 66, 'F - test_draw_square/p2:y') - local p = drawing.points[drawing.shapes[1].vertices[3]] - check_eq(p.x, 35, 'F - test_draw_square/p3:x') - check_eq(p.y, 96, 'F - test_draw_square/p3:y') - local p = drawing.points[drawing.shapes[1].vertices[4]] - check_eq(p.x, 5, 'F - test_draw_square/p4:x') - check_eq(p.y, 66, 'F - test_draw_square/p4:y') -end - -function test_name_point() - io.write('\ntest_name_point') - -- create a drawing with a line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- draw a line - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_name_point/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_name_point/baseline/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_name_point/baseline/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_name_point/baseline/p1:x') - check_eq(p1.y, 6, 'F - test_name_point/baseline/p1:y') - check_eq(p2.x, 35, 'F - test_name_point/baseline/p2:x') - check_eq(p2.y, 36, 'F - test_name_point/baseline/p2:y') - check_nil(p2.name, 'F - test_name_point/baseline/p2:name') - -- enter 'name' mode without moving the mouse - edit.run_after_keychord(Editor_state, 'C-n') - check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:1') - edit.run_after_textinput(Editor_state, 'A') - check_eq(p2.name, 'A', 'F - test_name_point') - -- still in 'name' mode - check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:2') - -- exit 'name' mode - edit.run_after_keychord(Editor_state, 'return') - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_name_point/mode:3') - check_eq(p2.name, 'A', 'F - test_name_point') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- change is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.name, 'A', 'F - test_name_point/save') -end - -function test_move_point() - io.write('\ntest_move_point') - -- create a drawing with a line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_move_point/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_move_point/baseline/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point/baseline/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_move_point/baseline/p1:x') - check_eq(p1.y, 6, 'F - test_move_point/baseline/p1:y') - check_eq(p2.x, 35, 'F - test_move_point/baseline/p2:x') - check_eq(p2.y, 36, 'F - test_move_point/baseline/p2:y') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- line is saved to disk - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local drawing = Editor_state.lines[1] - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.x, 35, 'F - test_move_point/save/x') - check_eq(p2.y, 36, 'F - test_move_point/save/y') - edit.draw(Editor_state) - -- enter 'move' mode without moving the mouse - edit.run_after_keychord(Editor_state, 'C-u') - check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point/mode:1') - -- point is lifted - check_eq(drawing.pending.mode, 'move', 'F - test_move_point/mode:2') - check_eq(drawing.pending.target_point, p2, 'F - test_move_point/target') - -- move point - App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44) - edit.update(Editor_state, 0.05) - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p2.x, 26, 'F - test_move_point/x') - check_eq(p2.y, 44, 'F - test_move_point/y') - -- exit 'move' mode - edit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_move_point/mode:3') - check_eq(drawing.pending, {}, 'F - test_move_point/pending') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- change is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.x, 26, 'F - test_move_point/save/x') - check_eq(p2.y, 44, 'F - test_move_point/save/y') -end - -function test_move_point_on_manhattan_line() - io.write('\ntest_move_point_on_manhattan_line') - -- create a drawing with a manhattan line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'manhattan' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+46, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_move_point_on_manhattan_line/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_move_point_on_manhattan_line/baseline/#points') - check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_move_point_on_manhattan_line/baseline/shape:1') - edit.draw(Editor_state) - -- enter 'move' mode - edit.run_after_keychord(Editor_state, 'C-u') - check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point_on_manhattan_line/mode:1') - -- move point - App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44) - edit.update(Editor_state, 0.05) - -- line is no longer manhattan - check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point_on_manhattan_line/baseline/shape:1') -end - -function test_delete_lines_at_point() - io.write('\ntest_delete_lines_at_point') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 2, 'F - test_delete_lines_at_point/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:2') - -- hover on the common point and delete - App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'C-d') - check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_lines_at_point/shape:1') - check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_delete_lines_at_point/shape:2') - -- wait for some time - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- deleted points disappear after file is reloaded - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_delete_lines_at_point/save') -end - -function test_delete_line_under_mouse_pointer() - io.write('\ntest_delete_line_under_mouse_pointer') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 2, 'F - test_delete_line_under_mouse_pointer/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:2') - -- hover on one of the lines and delete - App.mouse_move(Editor_state.left+25, Editor_state.top+Drawing_padding_top+26) - edit.run_after_keychord(Editor_state, 'C-d') - -- only that line is deleted - check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_line_under_mouse_pointer/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/shape:2') -end - -function test_delete_point_from_polygon() - io.write('\ntest_delete_point_from_polygon') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_keychord(Editor_state, 'g') -- polygon mode - -- second point - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'p') -- add point - -- third point - App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26) - edit.run_after_keychord(Editor_state, 'p') -- add point - -- fourth point - edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode') - check_eq(#drawing.shapes[1].vertices, 4, 'F - test_delete_point_from_polygon/baseline/vertices') - -- hover on a point and delete - App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26) - edit.run_after_keychord(Editor_state, 'C-d') - -- just the one point is deleted - check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/shape') - check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/vertices') -end - -function test_delete_point_from_polygon() - io.write('\ntest_delete_point_from_polygon') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_keychord(Editor_state, 'g') -- polygon mode - -- second point - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'p') -- add point - -- third point - edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode') - check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/baseline/vertices') - -- hover on a point and delete - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'C-d') - -- there's < 3 points left, so the whole polygon is deleted - check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_point_from_polygon') -end - -function test_undo_name_point() - io.write('\ntest_undo_name_point') - -- create a drawing with a line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- draw a line - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_undo_name_point/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_undo_name_point/baseline/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_name_point/baseline/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_undo_name_point/baseline/p1:x') - check_eq(p1.y, 6, 'F - test_undo_name_point/baseline/p1:y') - check_eq(p2.x, 35, 'F - test_undo_name_point/baseline/p2:x') - check_eq(p2.y, 36, 'F - test_undo_name_point/baseline/p2:y') - check_nil(p2.name, 'F - test_undo_name_point/baseline/p2:name') - check_eq(#Editor_state.history, 1, 'F - test_undo_name_point/baseline/history:1') ---? print('a', Editor_state.lines.current_drawing) - -- enter 'name' mode without moving the mouse - edit.run_after_keychord(Editor_state, 'C-n') - edit.run_after_textinput(Editor_state, 'A') - edit.run_after_keychord(Editor_state, 'return') - check_eq(p2.name, 'A', 'F - test_undo_name_point/baseline') - check_eq(#Editor_state.history, 3, 'F - test_undo_name_point/baseline/history:2') - check_eq(Editor_state.next_history, 4, 'F - test_undo_name_point/baseline/next_history') ---? print('b', Editor_state.lines.current_drawing) - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - local drawing = Editor_state.lines[1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(Editor_state.next_history, 3, 'F - test_undo_name_point/next_history') - check_eq(p2.name, '', 'F - test_undo_name_point') -- not quite what it was before, but close enough - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- undo is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.name, '', 'F - test_undo_name_point/save') -end - -function test_undo_move_point() - io.write('\ntest_undo_move_point') - -- create a drawing with a line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_undo_move_point/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_undo_move_point/baseline/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_move_point/baseline/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_undo_move_point/baseline/p1:x') - check_eq(p1.y, 6, 'F - test_undo_move_point/baseline/p1:y') - check_eq(p2.x, 35, 'F - test_undo_move_point/baseline/p2:x') - check_eq(p2.y, 36, 'F - test_undo_move_point/baseline/p2:y') - check_nil(p2.name, 'F - test_undo_move_point/baseline/p2:name') - -- move p2 - edit.run_after_keychord(Editor_state, 'C-u') - App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44) - edit.update(Editor_state, 0.05) - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p2.x, 26, 'F - test_undo_move_point/x') - check_eq(p2.y, 44, 'F - test_undo_move_point/y') - -- exit 'move' mode - edit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1) - check_eq(Editor_state.next_history, 4, 'F - test_undo_move_point/next_history') - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - edit.run_after_keychord(Editor_state, 'C-z') -- bug: need to undo twice - local drawing = Editor_state.lines[1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(Editor_state.next_history, 2, 'F - test_undo_move_point/next_history') - check_eq(p2.x, 35, 'F - test_undo_move_point/x') - check_eq(p2.y, 36, 'F - test_undo_move_point/y') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- undo is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.x, 35, 'F - test_undo_move_point/save/x') - check_eq(p2.y, 36, 'F - test_undo_move_point/save/y') -end - -function test_undo_delete_point() - io.write('\ntest_undo_delete_point') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 2, 'F - test_undo_delete_point/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/baseline/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/baseline/shape:2') - -- hover on the common point and delete - App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'C-d') - check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_undo_delete_point/shape:1') - check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_undo_delete_point/shape:2') - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - local drawing = Editor_state.lines[1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(Editor_state.next_history, 3, 'F - test_undo_move_point/next_history') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/shape:2') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- undo is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - check_eq(#Editor_state.lines[1].shapes, 2, 'F - test_undo_delete_point/save') -end diff --git a/edit.lua b/edit.lua index ccf675b..bc34158 100644 --- a/edit.lua +++ b/edit.lua @@ -1,60 +1,24 @@ -- 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 - utf8 = require 'utf8' require 'file' require 'text' -require 'drawing' -require 'geom' -require 'help' -require 'icons' 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 = {''}, -- array of strings -- Lines can be too long to fit on screen, in which case they _wrap_ into -- multiple _screen lines_. @@ -91,9 +55,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 @@ -118,15 +79,6 @@ 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) App.color(Text_color) assert(#State.lines == #State.line_cache) @@ -142,51 +94,25 @@ 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 - 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('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.cursor_y == -1 then State.cursor_y = App.screen.height end ---? print('screen bottom: '..tostring(State.screen_bottom1.pos)..' in '..tostring(State.lines[State.screen_bottom1.line].data)) +--? print('screen bottom: '..tostring(State.screen_bottom1.pos)..' in '..tostring(State.lines[State.screen_bottom1.line])) if State.search_term then Text.draw_search_bar(State) end end function edit.update(State, dt) - Drawing.update(State, dt) if State.next_save and State.next_save < App.getTime() then save_to_disk(State) State.next_save = nil @@ -212,36 +138,25 @@ function edit.mouse_pressed(State, x,y, mouse_button) propagate_to_button_handlers(x,y, mouse_button) 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_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 - 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_pressed(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 @@ -249,40 +164,29 @@ end function edit.mouse_released(State, x,y, mouse_button) if State.search_term then return end --? print('release') - if State.lines.current_drawing then - Drawing.mouse_released(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.textinput(State, t) @@ -291,12 +195,6 @@ function edit.textinput(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 Text.textinput(State, t) end @@ -305,7 +203,6 @@ end function edit.keychord_pressed(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 @@ -413,42 +310,7 @@ function edit.keychord_pressed(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_pressed(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_pressed(State, chord) diff --git a/file.lua b/file.lua index 9440ae8..84cc74c 100644 --- a/file.lua +++ b/file.lua @@ -12,15 +12,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, line) end end if #result == 0 then - table.insert(result, {mode='text', data=''}) + table.insert(result, '') end return result end @@ -31,82 +27,11 @@ 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, '\n') - end + outfile:write(line, '\n') end outfile:close() end -json = require 'json' -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.insert_point(drawing.points, shape.p1.x, shape.p1.y) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.insert_point(drawing.points, shape.p2.x, shape.p2.y) - 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.insert_point(drawing.points, p.x,p.y) - 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.insert_point(drawing.points, shape.center.x,shape.center.y) - 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 = {} @@ -115,58 +40,10 @@ 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, line) end if #result == 0 then - table.insert(result, {mode='text', data=''}) + table.insert(result, '') 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.insert_point(drawing.points, shape.p1.x, shape.p1.y) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.insert_point(drawing.points, shape.p2.x, shape.p2.y) - 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.insert_point(drawing.points, p.x,p.y) - 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.insert_point(drawing.points, shape.center.x,shape.center.y) - 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 diff --git a/geom.lua b/geom.lua deleted file mode 100644 index 891e98d..0000000 --- a/geom.lua +++ /dev/null @@ -1,168 +0,0 @@ -geom = {} - -function geom.on_shape(x,y, drawing, shape) - if shape.mode == 'freehand' then - return geom.on_freehand(x,y, drawing, shape) - elseif shape.mode == 'line' then - return geom.on_line(x,y, drawing, shape) - elseif shape.mode == 'manhattan' then - local p1 = drawing.points[shape.p1] - local p2 = drawing.points[shape.p2] - if p1.x == p2.x then - if x ~= p1.x then return false end - local y1,y2 = p1.y, p2.y - if y1 > y2 then - y1,y2 = y2,y1 - end - return y >= y1-2 and y <= y2+2 - elseif p1.y == p2.y then - if y ~= p1.y then return false end - local x1,x2 = p1.x, p2.x - if x1 > x2 then - x1,x2 = x2,x1 - end - return x >= x1-2 and x <= x2+2 - end - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - return geom.on_polygon(x,y, drawing, shape) - elseif shape.mode == 'circle' then - local center = drawing.points[shape.center] - local dist = geom.dist(center.x,center.y, x,y) - return dist > shape.radius*0.95 and dist < shape.radius*1.05 - elseif shape.mode == 'arc' then - local center = drawing.points[shape.center] - local dist = geom.dist(center.x,center.y, x,y) - if dist < shape.radius*0.95 or dist > shape.radius*1.05 then - return false - end - return geom.angle_between(center.x,center.y, x,y, shape.start_angle,shape.end_angle) - elseif shape.mode == 'deleted' then - else - print(shape.mode) - assert(false) - end -end - -function geom.on_freehand(x,y, drawing, shape) - local prev - for _,p in ipairs(shape.points) do - if prev then - if geom.on_line(x,y, drawing, {p1=prev, p2=p}) then - return true - end - end - prev = p - end - return false -end - -function geom.on_line(x,y, drawing, shape) - local p1,p2 - if type(shape.p1) == 'number' then - p1 = drawing.points[shape.p1] - p2 = drawing.points[shape.p2] - else - p1 = shape.p1 - p2 = shape.p2 - end - if p1.x == p2.x then - if math.abs(p1.x-x) > 2 then - return false - end - local y1,y2 = p1.y,p2.y - if y1 > y2 then - y1,y2 = y2,y1 - end - return y >= y1-2 and y <= y2+2 - end - -- has the right slope and intercept - local m = (p2.y - p1.y) / (p2.x - p1.x) - local yp = p1.y + m*(x-p1.x) - if yp < y-2 or yp > y+2 then - return false - end - -- between endpoints - local k = (x-p1.x) / (p2.x-p1.x) - return k > -0.005 and k < 1.005 -end - -function geom.on_polygon(x,y, drawing, shape) - local prev - for _,p in ipairs(shape.vertices) do - if prev then - if geom.on_line(x,y, drawing, {p1=prev, p2=p}) then - return true - end - end - prev = p - end - return geom.on_line(x,y, drawing, {p1=shape.vertices[1], p2=shape.vertices[#shape.vertices]}) -end - --- are (x3,y3) and (x4,y4) on the same side of the line between (x1,y1) and (x2,y2) -function geom.same_side(x1,y1, x2,y2, x3,y3, x4,y4) - if x1 == x2 then - return math.sign(x3-x1) == math.sign(x4-x1) - end - if y1 == y2 then - return math.sign(y3-y1) == math.sign(y4-y1) - end - local m = (y2-y1)/(x2-x1) - return math.sign(m*(x3-x1) + y1-y3) == math.sign(m*(x4-x1) + y1-y4) -end - -function math.sign(x) - if x > 0 then - return 1 - elseif x == 0 then - return 0 - elseif x < 0 then - return -1 - end -end - -function geom.angle_with_hint(x1, y1, x2, y2, hint) - local result = geom.angle(x1,y1, x2,y2) - if hint then - -- Smooth the discontinuity where angle goes from positive to negative. - -- The hint is a memory of which way we drew it last time. - while result > hint+math.pi/10 do - result = result-math.pi*2 - end - while result < hint-math.pi/10 do - result = result+math.pi*2 - end - end - return result -end - --- result is from -π/2 to 3π/2, approximately adding math.atan2 from Lua 5.3 --- (LÖVE is Lua 5.1) -function geom.angle(x1,y1, x2,y2) - local result = math.atan((y2-y1)/(x2-x1)) - if x2 < x1 then - result = result+math.pi - end - return result -end - --- is the line between x,y and cx,cy at an angle between s and e? -function geom.angle_between(ox,oy, x,y, s,e) - local angle = geom.angle(ox,oy, x,y) - if s > e then - s,e = e,s - end - -- I'm not sure this is right or ideal.. - angle = angle-math.pi*2 - if s <= angle and angle <= e then - return true - end - angle = angle+math.pi*2 - if s <= angle and angle <= e then - return true - end - angle = angle+math.pi*2 - return s <= angle and angle <= e -end - -function geom.dist(x1,y1, x2,y2) return ((x2-x1)^2+(y2-y1)^2)^0.5 end diff --git a/help.lua b/help.lua deleted file mode 100644 index 2abeb00..0000000 --- a/help.lua +++ /dev/null @@ -1,156 +0,0 @@ -function draw_help_without_mouse_pressed(State, drawing_index) - local drawing = State.lines[drawing_index] - local line_cache = State.line_cache[drawing_index] - App.color(Help_color) - local y = line_cache.starty+10 - love.graphics.print("Things you can do:", State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press the mouse button to start drawing a "..current_shape(State), State.left+30,y) - y = y + State.line_height - love.graphics.print("* Hover on a point and press 'ctrl+u' to pick it up and start moving it,", State.left+30,y) - y = y + State.line_height - love.graphics.print("then press the mouse button to drop it", State.left+30+bullet_indent(),y) - y = y + State.line_height - love.graphics.print("* Hover on a point and press 'ctrl+n', type a name, then press 'enter'", State.left+30,y) - y = y + State.line_height - love.graphics.print("* Hover on a point or shape and press 'ctrl+d' to delete it", State.left+30,y) - y = y + State.line_height - if State.current_drawing_mode ~= 'freehand' then - love.graphics.print("* Press 'ctrl+p' to switch to drawing freehand strokes", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'line' then - love.graphics.print("* Press 'ctrl+l' to switch to drawing lines", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'manhattan' then - love.graphics.print("* Press 'ctrl+m' to switch to drawing horizontal/vertical lines", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'circle' then - love.graphics.print("* Press 'ctrl+o' to switch to drawing circles/arcs", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'polygon' then - love.graphics.print("* Press 'ctrl+g' to switch to drawing polygons", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'rectangle' then - love.graphics.print("* Press 'ctrl+r' to switch to drawing rectangles", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'square' then - love.graphics.print("* Press 'ctrl+s' to switch to drawing squares", State.left+30,y) - y = y + State.line_height - end - love.graphics.print("* Press 'ctrl+=' or 'ctrl+-' to zoom in or out, ctrl+0 to reset zoom", State.left+30,y) - y = y + State.line_height - love.graphics.print("Press 'esc' now to hide this message", State.left+30,y) - y = y + State.line_height - App.color(Help_background_color) - love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty)) -end - -function draw_help_with_mouse_pressed(State, drawing_index) - local drawing = State.lines[drawing_index] - local line_cache = State.line_cache[drawing_index] - App.color(Help_color) - local y = line_cache.starty+10 - love.graphics.print("You're currently drawing a "..current_shape(State, drawing.pending), State.left+30,y) - y = y + State.line_height - love.graphics.print('Things you can do now:', State.left+30,y) - y = y + State.line_height - if State.current_drawing_mode == 'freehand' then - love.graphics.print('* Release the mouse button to finish drawing the stroke', State.left+30,y) - y = y + State.line_height - elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then - love.graphics.print('* Release the mouse button to finish drawing the line', State.left+30,y) - y = y + State.line_height - elseif State.current_drawing_mode == 'circle' then - if drawing.pending.mode == 'circle' then - love.graphics.print('* Release the mouse button to finish drawing the circle', State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press 'a' to draw just an arc of a circle", State.left+30,y) - else - love.graphics.print('* Release the mouse button to finish drawing the arc', State.left+30,y) - end - y = y + State.line_height - elseif State.current_drawing_mode == 'polygon' then - love.graphics.print('* Release the mouse button to finish drawing the polygon', State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press 'p' to add a vertex to the polygon", State.left+30,y) - y = y + State.line_height - elseif State.current_drawing_mode == 'rectangle' then - if #drawing.pending.vertices < 2 then - love.graphics.print("* Press 'p' to add a vertex to the rectangle", State.left+30,y) - y = y + State.line_height - else - love.graphics.print('* Release the mouse button to finish drawing the rectangle', State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press 'p' to replace the second vertex of the rectangle", State.left+30,y) - y = y + State.line_height - end - elseif State.current_drawing_mode == 'square' then - if #drawing.pending.vertices < 2 then - love.graphics.print("* Press 'p' to add a vertex to the square", State.left+30,y) - y = y + State.line_height - else - love.graphics.print('* Release the mouse button to finish drawing the square', State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press 'p' to replace the second vertex of the square", State.left+30,y) - y = y + State.line_height - end - end - love.graphics.print("* Press 'esc' then release the mouse button to cancel the current shape", State.left+30,y) - y = y + State.line_height - y = y + State.line_height - if State.current_drawing_mode ~= 'line' then - love.graphics.print("* Press 'l' to switch to drawing lines", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'manhattan' then - love.graphics.print("* Press 'm' to switch to drawing horizontal/vertical lines", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'circle' then - love.graphics.print("* Press 'o' to switch to drawing circles/arcs", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'polygon' then - love.graphics.print("* Press 'g' to switch to drawing polygons", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'rectangle' then - love.graphics.print("* Press 'r' to switch to drawing rectangles", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'square' then - love.graphics.print("* Press 's' to switch to drawing squares", State.left+30,y) - y = y + State.line_height - end - App.color(Help_background_color) - love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty)) -end - -function current_shape(State, shape) - if State.current_drawing_mode == 'freehand' then - return 'freehand stroke' - elseif State.current_drawing_mode == 'line' then - return 'straight line' - elseif State.current_drawing_mode == 'manhattan' then - return 'horizontal/vertical line' - elseif State.current_drawing_mode == 'circle' and shape and shape.start_angle then - return 'arc' - else - return State.current_drawing_mode - end -end - -_bullet_indent = nil -function bullet_indent() - if _bullet_indent == nil then - local text = love.graphics.newText(love.graphics.getFont(), '* ') - _bullet_indent = text:getWidth() - end - return _bullet_indent -end diff --git a/icons.lua b/icons.lua deleted file mode 100644 index 19939cf..0000000 --- a/icons.lua +++ /dev/null @@ -1,58 +0,0 @@ -icon = {} - -function icon.insert_drawing(x, y) - App.color(Icon_color) - love.graphics.rectangle('line', x,y, 12,12) - love.graphics.line(4,y+6, 16,y+6) - love.graphics.line(10,y, 10,y+12) -end - -function icon.freehand(x, y) - love.graphics.line(x+4,y+7,x+5,y+5) - love.graphics.line(x+5,y+5,x+7,y+4) - love.graphics.line(x+7,y+4,x+9,y+3) - love.graphics.line(x+9,y+3,x+10,y+5) - love.graphics.line(x+10,y+5,x+12,y+6) - love.graphics.line(x+12,y+6,x+13,y+8) - love.graphics.line(x+13,y+8,x+13,y+10) - love.graphics.line(x+13,y+10,x+14,y+12) - love.graphics.line(x+14,y+12,x+15,y+14) - love.graphics.line(x+15,y+14,x+15,y+16) -end - -function icon.line(x, y) - love.graphics.line(x+4,y+2, x+16,y+18) -end - -function icon.manhattan(x, y) - love.graphics.line(x+4,y+20, x+4,y+2) - love.graphics.line(x+4,y+2, x+10,y+2) - love.graphics.line(x+10,y+2, x+10,y+10) - love.graphics.line(x+10,y+10, x+18,y+10) -end - -function icon.polygon(x, y) - love.graphics.line(x+8,y+2, x+14,y+2) - love.graphics.line(x+14,y+2, x+18,y+10) - love.graphics.line(x+18,y+10, x+10,y+18) - love.graphics.line(x+10,y+18, x+4,y+12) - love.graphics.line(x+4,y+12, x+8,y+2) -end - -function icon.rectangle(x, y) - love.graphics.line(x+4,y+8, x+4,y+16) - love.graphics.line(x+4,y+16, x+16,y+16) - love.graphics.line(x+16,y+16, x+16,y+8) - love.graphics.line(x+16,y+8, x+4,y+8) -end - -function icon.square(x, y) - love.graphics.line(x+6,y+6, x+6,y+16) - love.graphics.line(x+6,y+16, x+16,y+16) - love.graphics.line(x+16,y+16, x+16,y+6) - love.graphics.line(x+16,y+6, x+6,y+6) -end - -function icon.circle(x, y) - love.graphics.circle('line', x+10,y+10, 8) -end diff --git a/main.lua b/main.lua index de75045..6873352 100644 --- a/main.lua +++ b/main.lua @@ -1,4 +1,5 @@ utf8 = require 'utf8' +json = require 'json' require 'app' require 'test' @@ -43,13 +44,9 @@ function App.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) @@ -127,7 +124,6 @@ function App.filedropped(file) file:open('r') Editor_state.lines = load_from_file(file) file:close() - edit.fixup_cursor(Editor_state) love.window.setTitle('lines.love - '..Editor_state.filename) end diff --git a/main_tests.lua b/main_tests.lua index b89c634..9993d99 100644 --- a/main_tests.lua +++ b/main_tests.lua @@ -38,9 +38,9 @@ function test_drop_file() } App.filedropped(fake_dropped_file) check_eq(#Editor_state.lines, 3, 'F - test_drop_file/#lines') - check_eq(Editor_state.lines[1].data, 'abc', 'F - test_drop_file/lines:1') - check_eq(Editor_state.lines[2].data, 'def', 'F - test_drop_file/lines:2') - check_eq(Editor_state.lines[3].data, 'ghi', 'F - test_drop_file/lines:3') + check_eq(Editor_state.lines[1], 'abc', 'F - test_drop_file/lines:1') + check_eq(Editor_state.lines[2], 'def', 'F - test_drop_file/lines:2') + check_eq(Editor_state.lines[3], 'ghi', 'F - test_drop_file/lines:3') end function test_drop_file_saves_previous() diff --git a/search.lua b/search.lua index 1872b9e..ce089d3 100644 --- a/search.lua +++ b/search.lua @@ -21,14 +21,14 @@ end function Text.search_next(State) -- search current line from cursor - local pos = find(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos) + local pos = find(State.lines[State.cursor1.line], State.search_term, State.cursor1.pos) if pos then State.cursor1.pos = pos end if pos == nil then -- search lines below cursor for i=State.cursor1.line+1,#State.lines do - pos = find(State.lines[i].data, State.search_term) + pos = find(State.lines[i], State.search_term) if pos then State.cursor1.line = i State.cursor1.pos = pos @@ -39,7 +39,7 @@ function Text.search_next(State) if pos == nil then -- wrap around for i=1,State.cursor1.line-1 do - pos = find(State.lines[i].data, State.search_term) + pos = find(State.lines[i], State.search_term) if pos then State.cursor1.line = i State.cursor1.pos = pos @@ -49,7 +49,7 @@ function Text.search_next(State) end if pos == nil then -- search current line until cursor - pos = find(State.lines[State.cursor1.line].data, State.search_term) + pos = find(State.lines[State.cursor1.line], State.search_term) if pos and pos < State.cursor1.pos then State.cursor1.pos = pos end @@ -69,14 +69,14 @@ end function Text.search_previous(State) -- search current line before cursor - local pos = rfind(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos-1) + local pos = rfind(State.lines[State.cursor1.line], State.search_term, State.cursor1.pos-1) if pos then State.cursor1.pos = pos end if pos == nil then -- search lines above cursor for i=State.cursor1.line-1,1,-1 do - pos = rfind(State.lines[i].data, State.search_term) + pos = rfind(State.lines[i], State.search_term) if pos then State.cursor1.line = i State.cursor1.pos = pos @@ -87,7 +87,7 @@ function Text.search_previous(State) if pos == nil then -- wrap around for i=#State.lines,State.cursor1.line+1,-1 do - pos = rfind(State.lines[i].data, State.search_term) + pos = rfind(State.lines[i], State.search_term) if pos then State.cursor1.line = i State.cursor1.pos = pos @@ -97,7 +97,7 @@ function Text.search_previous(State) end if pos == nil then -- search current line after cursor - pos = rfind(State.lines[State.cursor1.line].data, State.search_term) + pos = rfind(State.lines[State.cursor1.line], State.search_term) if pos and pos > State.cursor1.pos then State.cursor1.pos = pos end diff --git a/select.lua b/select.lua index 0b77e9c..b89ea33 100644 --- a/select.lua +++ b/select.lua @@ -53,19 +53,19 @@ end -- Returns some intermediate computation useful elsewhere. function Text.draw_highlight(State, line, x,y, pos, lo,hi) if lo then - local lo_offset = Text.offset(line.data, lo) - local hi_offset = Text.offset(line.data, hi) - local pos_offset = Text.offset(line.data, pos) + local lo_offset = Text.offset(line, lo) + local hi_offset = Text.offset(line, hi) + local pos_offset = Text.offset(line, pos) local lo_px if pos == lo then lo_px = 0 else - local before = line.data:sub(pos_offset, lo_offset-1) + local before = line:sub(pos_offset, lo_offset-1) local before_text = App.newText(love.graphics.getFont(), before) lo_px = App.width(before_text) end --? print(lo,pos,hi, '--', lo_offset,pos_offset,hi_offset, '--', lo_px) - local s = line.data:sub(lo_offset, hi_offset-1) + local s = line:sub(lo_offset, hi_offset-1) local text = App.newText(love.graphics.getFont(), s) local text_width = App.width(text) App.color(Highlight_color) @@ -92,10 +92,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 @@ -138,20 +136,20 @@ function Text.delete_selection_without_undo(State) State.selection1 = {} -- delete everything between min (inclusive) and max (exclusive) Text.clear_screen_line_cache(State, minl) - local min_offset = Text.offset(State.lines[minl].data, minp) - local max_offset = Text.offset(State.lines[maxl].data, maxp) + local min_offset = Text.offset(State.lines[minl], minp) + local max_offset = Text.offset(State.lines[maxl], maxp) if minl == maxl then --? print('minl == maxl') - State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset) + State.lines[minl] = State.lines[minl]:sub(1, min_offset-1)..State.lines[minl]:sub(max_offset) return end assert(minl < maxl) - local rhs = State.lines[maxl].data:sub(max_offset) + local rhs = State.lines[maxl]:sub(max_offset) for i=maxl,minl+1,-1 do table.remove(State.lines, i) table.remove(State.line_cache, i) end - State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhs + State.lines[minl] = State.lines[minl]:sub(1, min_offset-1)..rhs end function Text.selection(State) @@ -167,18 +165,16 @@ function Text.selection(State) minp,maxp = maxp,minp end end - local min_offset = Text.offset(State.lines[minl].data, minp) - local max_offset = Text.offset(State.lines[maxl].data, maxp) + local min_offset = Text.offset(State.lines[minl], minp) + local max_offset = Text.offset(State.lines[maxl], maxp) if minl == maxl then - return State.lines[minl].data:sub(min_offset, max_offset-1) + return State.lines[minl]:sub(min_offset, max_offset-1) end assert(minl < maxl) - local result = {State.lines[minl].data:sub(min_offset)} + local result = {State.lines[minl]: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]) end - table.insert(result, State.lines[maxl].data:sub(1, max_offset-1)) + table.insert(result, State.lines[maxl]:sub(1, max_offset-1)) return table.concat(result, '\n') end diff --git a/text.lua b/text.lua index 3d70f93..014f77c 100644 --- a/text.lua +++ b/text.lua @@ -50,7 +50,7 @@ function Text.draw(State, line_index, y, startpos) if line_index == State.cursor1.line then if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then if State.search_term then - if State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then + if State.lines[State.cursor1.line]:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then local lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)) App.color(Text_color) love.graphics.print(State.search_term, x+lo_px,y) @@ -92,7 +92,7 @@ function Text.compute_fragments(State, line_index) line_cache.fragments = {} local x = State.left -- try to wrap at word boundaries - for frag in line.data:gmatch('%S*%s*') do + for frag in line:gmatch('%S*%s*') do local frag_text = App.newText(love.graphics.getFont(), frag) local frag_width = App.width(frag_text) --? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go') @@ -142,8 +142,8 @@ function Text.textinput(State, t) end function Text.insert_at_cursor(State, t) - local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset) + local byte_offset = Text.offset(State.lines[State.cursor1.line], State.cursor1.pos) + State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line], byte_offset) Text.clear_screen_line_cache(State, State.cursor1.line) State.cursor1.pos = State.cursor1.pos+1 end @@ -182,28 +182,23 @@ function Text.keychord_pressed(State, chord) local before if State.cursor1.pos > 1 then before = snapshot(State, State.cursor1.line) - local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1) - local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) + local byte_start = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos-1) + local byte_end = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos) if byte_start then if byte_end then - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end) + State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1)..string.sub(State.lines[State.cursor1.line], byte_end) else - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1) + State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1) end State.cursor1.pos = State.cursor1.pos-1 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])+1 + State.lines[State.cursor1.line-1] = State.lines[State.cursor1.line-1]..State.lines[State.cursor1.line] + 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 @@ -227,27 +222,25 @@ function Text.keychord_pressed(State, chord) return end local before - if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then + if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) then before = snapshot(State, State.cursor1.line) else before = snapshot(State, State.cursor1.line, State.cursor1.line+1) end - if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then - local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1) + if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) then + local byte_start = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos) + local byte_end = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos+1) if byte_start then if byte_end then - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end) + State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1)..string.sub(State.lines[State.cursor1.line], byte_end) else - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1) + State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1) end -- 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] = State.lines[State.cursor1.line]..State.lines[State.cursor1.line+1] table.remove(State.lines, State.cursor1.line+1) table.remove(State.line_cache, State.cursor1.line+1) end @@ -340,10 +333,10 @@ function Text.keychord_pressed(State, chord) 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)}) + local byte_offset = Text.offset(State.lines[State.cursor1.line], State.cursor1.pos) + table.insert(State.lines, State.cursor1.line+1, string.sub(State.lines[State.cursor1.line], 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) + State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_offset-1) Text.clear_screen_line_cache(State, State.cursor1.line) State.cursor1.line = State.cursor1.line+1 State.cursor1.pos = 1 @@ -358,11 +351,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) @@ -399,35 +388,30 @@ 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_index,screen_line_starting_pos = Text.pos_at_start_of_cursor_screen_line(State) 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 = 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] ---? print('previous screen line starts at pos '..tostring(screen_line_starting_pos)..' of its line') - if State.screen_top1.line > State.cursor1.line then - State.screen_top1.line = State.cursor1.line - State.screen_top1.pos = screen_line_starting_pos ---? print('pos of top of screen is also '..tostring(State.screen_top1.pos)..' of the same line') - end - 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 + 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] +--? print('previous screen line starts at pos '..tostring(screen_line_starting_pos)..' of its line') + if State.screen_top1.line > State.cursor1.line then + State.screen_top1.line = State.cursor1.line + State.screen_top1.pos = screen_line_starting_pos +--? print('pos of top of screen is also '..tostring(State.screen_top1.pos)..' of the same line') end + local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line], screen_line_starting_pos) + local s = string.sub(State.lines[State.cursor1.line], screen_line_starting_byte_offset) + State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 end if State.cursor1.line < State.screen_top1.line then State.screen_top1.line = State.cursor1.line @@ -442,28 +426,23 @@ function Text.up(State) State.screen_top1.pos = new_screen_line_starting_pos --? print('also setting pos of top of screen to '..tostring(State.screen_top1.pos)) end - local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos) - local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset) + local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line], new_screen_line_starting_pos) + local s = string.sub(State.lines[State.cursor1.line], new_screen_line_starting_byte_offset) State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 --? print('cursor pos is now '..tostring(State.cursor1.pos)) end 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 - State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.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], 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) @@ -481,8 +460,8 @@ function Text.down(State) local screen_line_index, screen_line_starting_pos = Text.pos_at_start_of_cursor_screen_line(State) new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index+1] --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos)) - local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos) - local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset) + local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line], new_screen_line_starting_pos) + local s = string.sub(State.lines[State.cursor1.line], new_screen_line_starting_byte_offset) State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos) if scroll_down then @@ -502,7 +481,7 @@ function Text.start_of_line(State) end function Text.end_of_line(State) - State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1 + State.cursor1.pos = utf8.len(State.lines[State.cursor1.line]) + 1 local _,botpos = Text.pos_at_start_of_cursor_screen_line(State) local botline1 = {line=State.cursor1.line, pos=botpos} if Text.cursor_past_screen_bottom(State) then @@ -516,7 +495,7 @@ function Text.word_left(State) if State.cursor1.pos == 1 then break end - if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then + if Text.match(State.lines[State.cursor1.line], State.cursor1.pos-1, '%S') then break end Text.left(State) @@ -528,7 +507,7 @@ function Text.word_left(State) break end assert(State.cursor1.pos > 1) - if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then + if Text.match(State.lines[State.cursor1.line], State.cursor1.pos-1, '%s') then break end end @@ -537,20 +516,20 @@ end function Text.word_right(State) -- skip some whitespace while true do - if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then + if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line]) then break end - if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then + if Text.match(State.lines[State.cursor1.line], State.cursor1.pos, '%S') then break end Text.right_without_scroll(State) end while true do Text.right_without_scroll(State) - if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then + if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line]) then break end - if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then + if Text.match(State.lines[State.cursor1.line], State.cursor1.pos, '%s') then break end end @@ -569,19 +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 - State.cursor1.pos = utf8.len(State.lines[State.cursor1.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]) + 1 end if Text.lt1(State.cursor1, State.screen_top1) then local top2 = Text.to2(State, State.screen_top1) @@ -598,19 +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 + if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) 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 - State.cursor1.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 @@ -633,23 +596,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 @@ -665,24 +612,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) @@ -713,8 +647,8 @@ function Text.to_pos_on_line(State, line_index, mx, my) local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index] - local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos) ---? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset)) + local screen_line_starting_byte_offset = Text.offset(line, screen_line_starting_pos) +--? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line, screen_line_starting_byte_offset)) local nexty = y + State.line_height if my < nexty then -- On all wrapped screen lines but the final one, clicks past end of @@ -724,7 +658,7 @@ function Text.to_pos_on_line(State, line_index, mx, my) --? print('past end of non-final line; return') return line_cache.screen_line_starting_pos[screen_line_index+1]-1 end - local s = string.sub(line.data, screen_line_starting_byte_offset) + local s = string.sub(line, screen_line_starting_byte_offset) --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1) return screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1 end @@ -737,14 +671,14 @@ function Text.screen_line_width(State, line_index, i) local line = State.lines[line_index] local line_cache = State.line_cache[line_index] local start_pos = line_cache.screen_line_starting_pos[i] - local start_offset = Text.offset(line.data, start_pos) + local start_offset = Text.offset(line, start_pos) local screen_line if i < #line_cache.screen_line_starting_pos then local past_end_pos = line_cache.screen_line_starting_pos[i+1] - local past_end_offset = Text.offset(line.data, past_end_pos) - screen_line = string.sub(line.data, start_offset, past_end_offset-1) + local past_end_offset = Text.offset(line, past_end_pos) + screen_line = string.sub(line, start_offset, past_end_offset-1) else - screen_line = string.sub(line.data, start_pos) + screen_line = string.sub(line, start_pos) end local screen_line_text = App.newText(love.graphics.getFont(), screen_line) return App.width(screen_line_text) @@ -847,9 +781,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, screen_line=1} Text.populate_screen_line_starting_pos(State, loc1.line) for i=#State.line_cache[loc1.line].screen_line_starting_pos,1,-1 do @@ -911,8 +842,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) @@ -922,7 +851,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 diff --git a/text_tests.lua b/text_tests.lua index fa4c173..5de9ea8 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 @@ -629,7 +601,7 @@ function test_cursor_movement_without_shift_resets_selection() edit.run_after_keychord(Editor_state, 'right') -- no change to data, selection is reset check_nil(Editor_state.selection1.line, 'F - test_cursor_movement_without_shift_resets_selection') - check_eq(Editor_state.lines[1].data, 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data') + check_eq(Editor_state.lines[1], 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data') end function test_edit_deletes_selection() @@ -647,7 +619,7 @@ function test_edit_deletes_selection() -- press a key edit.run_after_textinput(Editor_state, 'x') -- selected text is deleted and replaced with the key - check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_edit_deletes_selection') + check_eq(Editor_state.lines[1], 'xbc', 'F - test_edit_deletes_selection') end function test_edit_with_shift_key_deletes_selection() @@ -670,7 +642,7 @@ function test_edit_with_shift_key_deletes_selection() App.fake_key_release('lshift') -- selected text is deleted and replaced with the key check_nil(Editor_state.selection1.line, 'F - test_edit_with_shift_key_deletes_selection') - check_eq(Editor_state.lines[1].data, 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data') + check_eq(Editor_state.lines[1], 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data') end function test_copy_does_not_reset_selection() @@ -708,7 +680,7 @@ function test_cut() edit.run_after_keychord(Editor_state, 'C-x') check_eq(App.clipboard, 'a', 'F - test_cut/clipboard') -- selected text is deleted - check_eq(Editor_state.lines[1].data, 'bc', 'F - test_cut/data') + check_eq(Editor_state.lines[1], 'bc', 'F - test_cut/data') end function test_paste_replaces_selection() @@ -729,7 +701,7 @@ function test_paste_replaces_selection() edit.run_after_keychord(Editor_state, 'C-v') -- selection is reset since shift key is not pressed -- selection includes the newline, so it's also deleted - check_eq(Editor_state.lines[1].data, 'xyzdef', 'F - test_paste_replaces_selection') + check_eq(Editor_state.lines[1], 'xyzdef', 'F - test_paste_replaces_selection') end function test_deleting_selection_may_scroll() @@ -755,7 +727,7 @@ function test_deleting_selection_may_scroll() edit.run_after_keychord(Editor_state, 'backspace') -- page scrolls up check_eq(Editor_state.screen_top1.line, 1, 'F - test_deleting_selection_may_scroll') - check_eq(Editor_state.lines[1].data, 'ahi', 'F - test_deleting_selection_may_scroll/data') + check_eq(Editor_state.lines[1], 'ahi', 'F - test_deleting_selection_may_scroll/data') end function test_edit_wrapping_text() @@ -821,8 +793,8 @@ function test_insert_newline_at_start_of_line() edit.run_after_keychord(Editor_state, 'return') check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline_at_start_of_line/cursor:line') check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline_at_start_of_line/cursor:pos') - check_eq(Editor_state.lines[1].data, '', 'F - test_insert_newline_at_start_of_line/data:1') - check_eq(Editor_state.lines[2].data, 'abc', 'F - test_insert_newline_at_start_of_line/data:2') + check_eq(Editor_state.lines[1], '', 'F - test_insert_newline_at_start_of_line/data:1') + check_eq(Editor_state.lines[2], 'abc', 'F - test_insert_newline_at_start_of_line/data:2') end function test_insert_from_clipboard() @@ -994,36 +966,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 @@ -1816,7 +1758,7 @@ function test_backspace_past_line_boundary() Editor_state.cursor1 = {line=2, pos=1} -- backspace joins with previous line edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary") + check_eq(Editor_state.lines[1], 'abcdef', "F - test_backspace_past_line_boundary") end -- some tests for operating over selections created using Shift- chords @@ -1833,7 +1775,7 @@ function test_backspace_over_selection() Editor_state.selection1 = {line=1, pos=2} -- backspace deletes the selected character, even though it's after the cursor edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection/data") + check_eq(Editor_state.lines[1], 'bc', "F - test_backspace_over_selection/data") -- cursor (remains) at start of selection check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection/cursor:line") check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection/cursor:pos") @@ -1852,7 +1794,7 @@ function test_backspace_over_selection_reverse() Editor_state.selection1 = {line=1, pos=1} -- backspace deletes the selected character edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection_reverse/data") + check_eq(Editor_state.lines[1], 'bc', "F - test_backspace_over_selection_reverse/data") -- cursor moves to start of selection check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection_reverse/cursor:line") check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection_reverse/cursor:pos") @@ -1871,8 +1813,8 @@ function test_backspace_over_multiple_lines() Editor_state.selection1 = {line=4, pos=2} -- backspace deletes the region and joins the remaining portions of lines on either side edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'akl', "F - test_backspace_over_multiple_lines/data:1") - check_eq(Editor_state.lines[2].data, 'mno', "F - test_backspace_over_multiple_lines/data:2") + check_eq(Editor_state.lines[1], 'akl', "F - test_backspace_over_multiple_lines/data:1") + check_eq(Editor_state.lines[2], 'mno', "F - test_backspace_over_multiple_lines/data:2") -- cursor remains at start of selection check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_multiple_lines/cursor:line") check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_over_multiple_lines/cursor:pos") @@ -1891,8 +1833,8 @@ function test_backspace_to_end_of_line() Editor_state.selection1 = {line=1, pos=4} -- backspace deletes rest of line without joining to any other line edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'a', "F - test_backspace_to_start_of_line/data:1") - check_eq(Editor_state.lines[2].data, 'def', "F - test_backspace_to_start_of_line/data:2") + check_eq(Editor_state.lines[1], 'a', "F - test_backspace_to_start_of_line/data:1") + check_eq(Editor_state.lines[2], 'def', "F - test_backspace_to_start_of_line/data:2") -- cursor remains at start of selection check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_to_start_of_line/cursor:line") check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_to_start_of_line/cursor:pos") @@ -1911,8 +1853,8 @@ function test_backspace_to_start_of_line() Editor_state.selection1 = {line=2, pos=3} -- backspace deletes beginning of line without joining to any other line edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'abc', "F - test_backspace_to_start_of_line/data:1") - check_eq(Editor_state.lines[2].data, 'f', "F - test_backspace_to_start_of_line/data:2") + check_eq(Editor_state.lines[1], 'abc', "F - test_backspace_to_start_of_line/data:1") + check_eq(Editor_state.lines[2], 'f', "F - test_backspace_to_start_of_line/data:2") -- cursor remains at start of selection check_eq(Editor_state.cursor1.line, 2, "F - test_backspace_to_start_of_line/cursor:line") check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_to_start_of_line/cursor:pos") @@ -2008,7 +1950,7 @@ function test_undo_restores_selection() edit.draw(Editor_state) -- delete selected text edit.run_after_textinput(Editor_state, 'x') - check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_undo_restores_selection/baseline') + check_eq(Editor_state.lines[1], 'xbc', 'F - test_undo_restores_selection/baseline') check_nil(Editor_state.selection1.line, 'F - test_undo_restores_selection/baseline:selection') -- undo edit.run_after_keychord(Editor_state, 'C-z') @@ -2022,7 +1964,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..b3b9a00 100644 --- a/undo.lua +++ b/undo.lua @@ -60,19 +60,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, State.lines[i]) end return event end |