new fork: rip out drawing support

This commit is contained in:
Kartik K. Agaram 2022-08-14 09:17:53 -07:00
parent 974d17ffc0
commit 9d792a203b
15 changed files with 202 additions and 2539 deletions

View File

@ -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 Designed above all to be easy to modify and give you early warning if your
modifications break something. modifications break something.
http://akkartik.name/lines.html
## Invocation ## Invocation
To run from the terminal, [pass this directory to LÖVE](https://love2d.org/wiki/Getting_Started#Running_Games), 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: 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`). 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 ## 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 * click on a drawing to start a stroke and then press and hold `h` to see your
options at any point during a stroke. 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 you use a different layout, please let me know if things worked, or if you
found anything amiss: http://akkartik.name/contact 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. * No support yet for right-to-left languages.
* Undo/redo may be sluggish in large files. Large files may grow sluggish in * 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 other ways. Works well in all circumstances with files under 50KB.
50KB.
* If you kill the process, say by force-quitting because things things get * If you kill the process, say by force-quitting because things things get
sluggish, you can lose data. sluggish, you can lose data.
@ -80,28 +80,19 @@ found anything amiss: http://akkartik.name/contact
## Mirrors and Forks ## Mirrors and Forks
Updates to lines.love can be downloaded from the following mirrors in addition This repo is a fork of lines.love at [http://akkartik.name/lines.html](http://akkartik.name/lines.html).
to the website above: Updates to it can be downloaded from the following mirrors:
* 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 * https://codeberg.org/akkartik/text.love
here. * 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://github.com/akkartik/lines-polygon-experiment -- an experiment that Further forks are encouraged. If you show me your fork, I'll link to it here.
uses separate shortcuts for regular polygons. `ctrl+3` for triangles,
`ctrl+4` for squares, etc.
## Associated tools
* https://codeberg.org/akkartik/lines2md exports lines.love files to Markdown
and (non-editable) SVG.
## Feedback ## Feedback

View File

@ -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

View File

@ -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

234
edit.lua
View File

@ -1,60 +1,24 @@
-- some constants people might like to tweak -- some constants people might like to tweak
Text_color = {r=0, g=0, b=0} Text_color = {r=0, g=0, b=0}
Cursor_color = {r=1, 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 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 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_top = 15
Margin_left = 25 Margin_left = 25
Margin_right = 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' utf8 = require 'utf8'
require 'file' require 'file'
require 'text' require 'text'
require 'drawing'
require 'geom'
require 'help'
require 'icons'
edit = {} edit = {}
-- run in both tests and a real run -- 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 function edit.initialize_state(top, left, right, font_height, line_height) -- currently always draws to bottom of screen
local result = { local result = {
-- a line is either text or a drawing lines = {''}, -- array of strings
-- 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 can be too long to fit on screen, in which case they _wrap_ into -- Lines can be too long to fit on screen, in which case they _wrap_ into
-- multiple _screen lines_. -- multiple _screen lines_.
@ -91,9 +55,6 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c
cursor_x = 0, cursor_x = 0,
cursor_y = 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, font_height = font_height,
line_height = line_height, line_height = line_height,
em = App.newText(love.graphics.getFont(), 'm'), -- widest possible character width 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 return result
end -- App.initialize_state 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) function edit.draw(State)
App.color(Text_color) App.color(Text_color)
assert(#State.lines == #State.line_cache) assert(#State.lines == #State.line_cache)
@ -142,51 +94,25 @@ function edit.draw(State)
--? print('draw:', y, line_index, line) --? print('draw:', y, line_index, line)
if y + State.line_height > App.screen.height then break end if y + State.line_height > App.screen.height then break end
State.screen_bottom1.line = line_index State.screen_bottom1.line = line_index
if line.mode == 'text' then --? print('text.draw', y, line_index)
--? print('text.draw', y, line_index) local startpos = 1
local startpos = 1 if line_index == State.screen_top1.line then
if line_index == State.screen_top1.line then startpos = State.screen_top1.pos
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)
end end
y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
y = y + State.line_height
--? print('=> y', y)
end end
if State.cursor_y == -1 then if State.cursor_y == -1 then
State.cursor_y = App.screen.height State.cursor_y = App.screen.height
end 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 if State.search_term then
Text.draw_search_bar(State) Text.draw_search_bar(State)
end end
end end
function edit.update(State, dt) function edit.update(State, dt)
Drawing.update(State, dt)
if State.next_save and State.next_save < App.getTime() then if State.next_save and State.next_save < App.getTime() then
save_to_disk(State) save_to_disk(State)
State.next_save = nil 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) propagate_to_button_handlers(x,y, mouse_button)
for line_index,line in ipairs(State.lines) do for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then if Text.in_line(State, line_index, x,y) then
if Text.in_line(State, line_index, x,y) then -- delicate dance between cursor, selection and old cursor/selection
-- delicate dance between cursor, selection and old cursor/selection -- scenarios:
-- scenarios: -- regular press+release: sets cursor, clears selection
-- regular press+release: sets cursor, clears selection -- shift press+release:
-- shift press+release: -- sets selection to old cursor if not set otherwise leaves it untouched
-- sets selection to old cursor if not set otherwise leaves it untouched -- sets cursor
-- sets cursor -- press and hold to start a selection: sets selection on press, cursor on release
-- press and hold to start a selection: sets selection on press, cursor on release -- press and hold, then press shift: ignore shift
-- press and hold, then press shift: ignore shift -- i.e. mouse_released should never look at shift state
-- i.e. mouse_released should never look at shift state State.old_cursor1 = State.cursor1
State.old_cursor1 = State.cursor1 State.old_selection1 = State.selection1
State.old_selection1 = State.selection1 State.mousepress_shift = App.shift_down()
State.mousepress_shift = App.shift_down() State.selection1 = {
State.selection1 = { line=line_index,
line=line_index, pos=Text.to_pos_on_line(State, line_index, x, y),
pos=Text.to_pos_on_line(State, line_index, x, y), }
} --? print('selection', State.selection1.line, State.selection1.pos)
--? print('selection', State.selection1.line, State.selection1.pos) break
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
end end
end end
end end
@ -249,40 +164,29 @@ end
function edit.mouse_released(State, x,y, mouse_button) function edit.mouse_released(State, x,y, mouse_button)
if State.search_term then return end if State.search_term then return end
--? print('release') --? print('release')
if State.lines.current_drawing then for line_index,line in ipairs(State.lines) do
Drawing.mouse_released(State, x,y, mouse_button) if Text.in_line(State, line_index, x,y) then
schedule_save(State) --? print('reset selection')
if Drawing.before then State.cursor1 = {
record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)}) line=line_index,
Drawing.before = nil pos=Text.to_pos_on_line(State, line_index, x, y),
end }
else --? print('cursor', State.cursor1.line, State.cursor1.pos)
for line_index,line in ipairs(State.lines) do if State.mousepress_shift then
if line.mode == 'text' then if State.old_selection1.line == nil then
if Text.in_line(State, line_index, x,y) then State.selection1 = State.old_cursor1
--? print('reset selection') else
State.cursor1 = { State.selection1 = State.old_selection1
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 end
end end
State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
if eq(State.cursor1, State.selection1) then
State.selection1 = {}
end
break
end end
--? print('selection:', State.selection1.line, State.selection1.pos)
end end
--? print('selection:', State.selection1.line, State.selection1.pos)
end end
function edit.textinput(State, t) function edit.textinput(State, t)
@ -291,12 +195,6 @@ function edit.textinput(State, t)
State.search_term = State.search_term..t State.search_term = State.search_term..t
State.search_text = nil State.search_text = nil
Text.search_next(State) 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 else
Text.textinput(State, t) Text.textinput(State, t)
end end
@ -305,7 +203,6 @@ end
function edit.keychord_pressed(State, chord, key) function edit.keychord_pressed(State, chord, key)
if State.selection1.line and if State.selection1.line and
not State.lines.current_drawing and
-- printable character created using shift key => delete selection -- printable character created using shift key => delete selection
-- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys) -- (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 (not App.shift_down() or utf8.len(key) == 1) and
@ -413,42 +310,7 @@ function edit.keychord_pressed(State, chord, key)
end end
schedule_save(State) schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
-- dispatch to drawing or text -- dispatch to 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)
else else
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
Text.keychord_pressed(State, chord) Text.keychord_pressed(State, chord)

133
file.lua
View File

@ -12,15 +12,11 @@ function load_from_file(infile)
while true do while true do
local line = infile_next_line() local line = infile_next_line()
if line == nil then break end if line == nil then break end
if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated table.insert(result, line)
table.insert(result, load_drawing(infile_next_line))
else
table.insert(result, {mode='text', data=line})
end
end end
end end
if #result == 0 then if #result == 0 then
table.insert(result, {mode='text', data=''}) table.insert(result, '')
end end
return result return result
end end
@ -31,82 +27,11 @@ function save_to_disk(State)
error('failed to write to "'..State.filename..'"') error('failed to write to "'..State.filename..'"')
end end
for _,line in ipairs(State.lines) do for _,line in ipairs(State.lines) do
if line.mode == 'drawing' then outfile:write(line, '\n')
store_drawing(outfile, line)
else
outfile:write(line.data, '\n')
end
end end
outfile:close() outfile:close()
end 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 -- for tests
function load_array(a) function load_array(a)
local result = {} local result = {}
@ -115,58 +40,10 @@ function load_array(a)
while true do while true do
i,line = next_line(a, i) i,line = next_line(a, i)
if i == nil then break end if i == nil then break end
--? print(line) table.insert(result, 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
end end
if #result == 0 then if #result == 0 then
table.insert(result, {mode='text', data=''}) table.insert(result, '')
end end
return result return result
end 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

168
geom.lua
View File

@ -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

156
help.lua
View File

@ -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

View File

@ -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

View File

@ -1,4 +1,5 @@
utf8 = require 'utf8' utf8 = require 'utf8'
json = require 'json'
require 'app' require 'app'
require 'test' require 'test'
@ -43,13 +44,9 @@ function App.initialize(arg)
Text.redraw_all(Editor_state) Text.redraw_all(Editor_state)
Editor_state.screen_top1 = {line=1, pos=1} Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.cursor1 = {line=1, pos=1} Editor_state.cursor1 = {line=1, pos=1}
edit.fixup_cursor(Editor_state)
else else
load_from_disk(Editor_state) load_from_disk(Editor_state)
Text.redraw_all(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 end
love.window.setTitle('lines.love - '..Editor_state.filename) love.window.setTitle('lines.love - '..Editor_state.filename)
@ -127,7 +124,6 @@ function App.filedropped(file)
file:open('r') file:open('r')
Editor_state.lines = load_from_file(file) Editor_state.lines = load_from_file(file)
file:close() file:close()
edit.fixup_cursor(Editor_state)
love.window.setTitle('lines.love - '..Editor_state.filename) love.window.setTitle('lines.love - '..Editor_state.filename)
end end

View File

@ -38,9 +38,9 @@ function test_drop_file()
} }
App.filedropped(fake_dropped_file) App.filedropped(fake_dropped_file)
check_eq(#Editor_state.lines, 3, 'F - test_drop_file/#lines') 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[1], '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[2], '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[3], 'ghi', 'F - test_drop_file/lines:3')
end end
function test_drop_file_saves_previous() function test_drop_file_saves_previous()

View File

@ -21,14 +21,14 @@ end
function Text.search_next(State) function Text.search_next(State)
-- search current line from cursor -- 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 if pos then
State.cursor1.pos = pos State.cursor1.pos = pos
end end
if pos == nil then if pos == nil then
-- search lines below cursor -- search lines below cursor
for i=State.cursor1.line+1,#State.lines do 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 if pos then
State.cursor1.line = i State.cursor1.line = i
State.cursor1.pos = pos State.cursor1.pos = pos
@ -39,7 +39,7 @@ function Text.search_next(State)
if pos == nil then if pos == nil then
-- wrap around -- wrap around
for i=1,State.cursor1.line-1 do 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 if pos then
State.cursor1.line = i State.cursor1.line = i
State.cursor1.pos = pos State.cursor1.pos = pos
@ -49,7 +49,7 @@ function Text.search_next(State)
end end
if pos == nil then if pos == nil then
-- search current line until cursor -- 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 if pos and pos < State.cursor1.pos then
State.cursor1.pos = pos State.cursor1.pos = pos
end end
@ -69,14 +69,14 @@ end
function Text.search_previous(State) function Text.search_previous(State)
-- search current line before cursor -- 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 if pos then
State.cursor1.pos = pos State.cursor1.pos = pos
end end
if pos == nil then if pos == nil then
-- search lines above cursor -- search lines above cursor
for i=State.cursor1.line-1,1,-1 do 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 if pos then
State.cursor1.line = i State.cursor1.line = i
State.cursor1.pos = pos State.cursor1.pos = pos
@ -87,7 +87,7 @@ function Text.search_previous(State)
if pos == nil then if pos == nil then
-- wrap around -- wrap around
for i=#State.lines,State.cursor1.line+1,-1 do 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 if pos then
State.cursor1.line = i State.cursor1.line = i
State.cursor1.pos = pos State.cursor1.pos = pos
@ -97,7 +97,7 @@ function Text.search_previous(State)
end end
if pos == nil then if pos == nil then
-- search current line after cursor -- 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 if pos and pos > State.cursor1.pos then
State.cursor1.pos = pos State.cursor1.pos = pos
end end

View File

@ -53,19 +53,19 @@ end
-- Returns some intermediate computation useful elsewhere. -- Returns some intermediate computation useful elsewhere.
function Text.draw_highlight(State, line, x,y, pos, lo,hi) function Text.draw_highlight(State, line, x,y, pos, lo,hi)
if lo then if lo then
local lo_offset = Text.offset(line.data, lo) local lo_offset = Text.offset(line, lo)
local hi_offset = Text.offset(line.data, hi) local hi_offset = Text.offset(line, hi)
local pos_offset = Text.offset(line.data, pos) local pos_offset = Text.offset(line, pos)
local lo_px local lo_px
if pos == lo then if pos == lo then
lo_px = 0 lo_px = 0
else 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) local before_text = App.newText(love.graphics.getFont(), before)
lo_px = App.width(before_text) lo_px = App.width(before_text)
end end
--? print(lo,pos,hi, '--', lo_offset,pos_offset,hi_offset, '--', lo_px) --? 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 = App.newText(love.graphics.getFont(), s)
local text_width = App.width(text) local text_width = App.width(text)
App.color(Highlight_color) App.color(Highlight_color)
@ -92,10 +92,8 @@ end
function Text.to_pos(State, x,y) function Text.to_pos(State, x,y)
for line_index,line in ipairs(State.lines) do for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then if Text.in_line(State, line_index, x,y) then
if Text.in_line(State, line_index, x,y) then return line_index, Text.to_pos_on_line(State, line_index, x,y)
return line_index, Text.to_pos_on_line(State, line_index, x,y)
end
end end
end end
end end
@ -138,20 +136,20 @@ function Text.delete_selection_without_undo(State)
State.selection1 = {} State.selection1 = {}
-- delete everything between min (inclusive) and max (exclusive) -- delete everything between min (inclusive) and max (exclusive)
Text.clear_screen_line_cache(State, minl) Text.clear_screen_line_cache(State, minl)
local min_offset = Text.offset(State.lines[minl].data, minp) local min_offset = Text.offset(State.lines[minl], minp)
local max_offset = Text.offset(State.lines[maxl].data, maxp) local max_offset = Text.offset(State.lines[maxl], maxp)
if minl == maxl then if minl == maxl then
--? print('minl == maxl') --? 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 return
end end
assert(minl < maxl) 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 for i=maxl,minl+1,-1 do
table.remove(State.lines, i) table.remove(State.lines, i)
table.remove(State.line_cache, i) table.remove(State.line_cache, i)
end 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 end
function Text.selection(State) function Text.selection(State)
@ -167,18 +165,16 @@ function Text.selection(State)
minp,maxp = maxp,minp minp,maxp = maxp,minp
end end
end end
local min_offset = Text.offset(State.lines[minl].data, minp) local min_offset = Text.offset(State.lines[minl], minp)
local max_offset = Text.offset(State.lines[maxl].data, maxp) local max_offset = Text.offset(State.lines[maxl], maxp)
if minl == maxl then 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 end
assert(minl < maxl) 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 for i=minl+1,maxl-1 do
if State.lines[i].mode == 'text' then table.insert(result, State.lines[i])
table.insert(result, State.lines[i].data)
end
end 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') return table.concat(result, '\n')
end end

228
text.lua
View File

@ -50,7 +50,7 @@ function Text.draw(State, line_index, y, startpos)
if line_index == State.cursor1.line then if line_index == State.cursor1.line then
if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then
if State.search_term 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)) 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) App.color(Text_color)
love.graphics.print(State.search_term, x+lo_px,y) love.graphics.print(State.search_term, x+lo_px,y)
@ -92,7 +92,7 @@ function Text.compute_fragments(State, line_index)
line_cache.fragments = {} line_cache.fragments = {}
local x = State.left local x = State.left
-- try to wrap at word boundaries -- 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_text = App.newText(love.graphics.getFont(), frag)
local frag_width = App.width(frag_text) local frag_width = App.width(frag_text)
--? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go') --? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')
@ -142,8 +142,8 @@ function Text.textinput(State, t)
end end
function Text.insert_at_cursor(State, t) function Text.insert_at_cursor(State, t)
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) local byte_offset = Text.offset(State.lines[State.cursor1.line], 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) 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) Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.pos = State.cursor1.pos+1 State.cursor1.pos = State.cursor1.pos+1
end end
@ -182,28 +182,23 @@ function Text.keychord_pressed(State, chord)
local before local before
if State.cursor1.pos > 1 then if State.cursor1.pos > 1 then
before = snapshot(State, State.cursor1.line) before = snapshot(State, State.cursor1.line)
local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1) local byte_start = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos-1)
local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) local byte_end = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos)
if byte_start then if byte_start then
if byte_end 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 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 end
State.cursor1.pos = State.cursor1.pos-1 State.cursor1.pos = State.cursor1.pos-1
end end
elseif State.cursor1.line > 1 then elseif State.cursor1.line > 1 then
before = snapshot(State, State.cursor1.line-1, State.cursor1.line) before = snapshot(State, State.cursor1.line-1, State.cursor1.line)
if State.lines[State.cursor1.line-1].mode == 'drawing' then -- join lines
table.remove(State.lines, State.cursor1.line-1) State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1])+1
table.remove(State.line_cache, State.cursor1.line-1) State.lines[State.cursor1.line-1] = State.lines[State.cursor1.line-1]..State.lines[State.cursor1.line]
else table.remove(State.lines, State.cursor1.line)
-- join lines table.remove(State.line_cache, State.cursor1.line)
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
State.cursor1.line = State.cursor1.line-1 State.cursor1.line = State.cursor1.line-1
end end
if State.screen_top1.line > #State.lines then if State.screen_top1.line > #State.lines then
@ -227,27 +222,25 @@ function Text.keychord_pressed(State, chord)
return return
end end
local before 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) before = snapshot(State, State.cursor1.line)
else else
before = snapshot(State, State.cursor1.line, State.cursor1.line+1) before = snapshot(State, State.cursor1.line, State.cursor1.line+1)
end end
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
local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) local byte_start = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos)
local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1) local byte_end = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos+1)
if byte_start then if byte_start then
if byte_end 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 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 end
-- no change to State.cursor1.pos -- no change to State.cursor1.pos
end end
elseif State.cursor1.line < #State.lines then elseif State.cursor1.line < #State.lines then
if State.lines[State.cursor1.line+1].mode == 'text' then -- join lines
-- join lines State.lines[State.cursor1.line] = State.lines[State.cursor1.line]..State.lines[State.cursor1.line+1]
State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
end
table.remove(State.lines, State.cursor1.line+1) table.remove(State.lines, State.cursor1.line+1)
table.remove(State.line_cache, State.cursor1.line+1) table.remove(State.line_cache, State.cursor1.line+1)
end end
@ -340,10 +333,10 @@ function Text.keychord_pressed(State, chord)
end end
function Text.insert_return(State) function Text.insert_return(State)
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) local byte_offset = Text.offset(State.lines[State.cursor1.line], State.cursor1.pos)
table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)}) table.insert(State.lines, State.cursor1.line+1, string.sub(State.lines[State.cursor1.line], byte_offset))
table.insert(State.line_cache, State.cursor1.line+1, {}) 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) Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.line = State.cursor1.line+1 State.cursor1.line = State.cursor1.line+1
State.cursor1.pos = 1 State.cursor1.pos = 1
@ -358,11 +351,7 @@ function Text.pageup(State)
while y >= State.top do while y >= State.top do
--? print(y, top2.line, top2.screen_line, top2.screen_pos) --? 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.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
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
top2 = Text.previous_screen_line(State, top2) top2 = Text.previous_screen_line(State, top2)
end end
State.screen_top1 = Text.to1(State, top2) State.screen_top1 = Text.to1(State, top2)
@ -399,35 +388,30 @@ function Text.pagedown(State)
end end
function Text.up(State) 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) --? 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) local screen_line_index,screen_line_starting_pos = Text.pos_at_start_of_cursor_screen_line(State)
if screen_line_starting_pos == 1 then if screen_line_starting_pos == 1 then
--? print('cursor is at first screen line of its line') --? print('cursor is at first screen line of its line')
-- line is done; skip to previous text line -- line is done; skip to previous text line
local new_cursor_line = State.cursor1.line if State.cursor1.line > 1 then
while new_cursor_line > 1 do local new_cursor_line = State.cursor1.line-1
new_cursor_line = new_cursor_line-1 --? print('found previous text line')
if State.lines[new_cursor_line].mode == 'text' then State.cursor1.line = new_cursor_line
--? print('found previous text line') Text.populate_screen_line_starting_pos(State, State.cursor1.line)
State.cursor1.line = new_cursor_line -- previous text line found, pick its final screen line
Text.populate_screen_line_starting_pos(State, State.cursor1.line) --? print('has multiple screen lines')
-- previous text line found, pick its final screen line local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos
--? print('has multiple screen lines') --? print(#screen_line_starting_pos)
local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]
--? print(#screen_line_starting_pos) --? print('previous screen line starts at pos '..tostring(screen_line_starting_pos)..' of its line')
screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos] if State.screen_top1.line > State.cursor1.line then
--? print('previous screen line starts at pos '..tostring(screen_line_starting_pos)..' of its line') State.screen_top1.line = State.cursor1.line
if State.screen_top1.line > State.cursor1.line then State.screen_top1.pos = screen_line_starting_pos
State.screen_top1.line = State.cursor1.line --? print('pos of top of screen is also '..tostring(State.screen_top1.pos)..' of the same 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
end 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 end
if State.cursor1.line < State.screen_top1.line then if State.cursor1.line < State.screen_top1.line then
State.screen_top1.line = State.cursor1.line State.screen_top1.line = State.cursor1.line
@ -442,28 +426,23 @@ function Text.up(State)
State.screen_top1.pos = new_screen_line_starting_pos State.screen_top1.pos = new_screen_line_starting_pos
--? print('also setting pos of top of screen to '..tostring(State.screen_top1.pos)) --? print('also setting pos of top of screen to '..tostring(State.screen_top1.pos))
end end
local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos) 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].data, new_screen_line_starting_byte_offset) 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 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)) --? print('cursor pos is now '..tostring(State.cursor1.pos))
end end
end end
function Text.down(State) 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) --? 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 if Text.cursor_at_final_screen_line(State) then
-- line is done, skip to next text line -- line is done, skip to next text line
--? print('cursor at final screen line of its line') --? print('cursor at final screen line of its line')
local new_cursor_line = State.cursor1.line if State.cursor1.line < #State.lines then
while new_cursor_line < #State.lines do local new_cursor_line = State.cursor1.line+1
new_cursor_line = new_cursor_line+1 State.cursor1.line = new_cursor_line
if State.lines[new_cursor_line].mode == 'text' then State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line], State.cursor_x, State.left)
State.cursor1.line = new_cursor_line --? print(State.cursor1.pos)
State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line].data, State.cursor_x, State.left)
--? print(State.cursor1.pos)
break
end
end end
if State.cursor1.line > State.screen_bottom1.line then if State.cursor1.line > State.screen_bottom1.line then
--? print('screen top before:', State.screen_top1.line, State.screen_top1.pos) --? 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) 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] 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)) --? 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 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].data, new_screen_line_starting_byte_offset) 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 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) --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
if scroll_down then if scroll_down then
@ -502,7 +481,7 @@ function Text.start_of_line(State)
end end
function Text.end_of_line(State) 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 _,botpos = Text.pos_at_start_of_cursor_screen_line(State)
local botline1 = {line=State.cursor1.line, pos=botpos} local botline1 = {line=State.cursor1.line, pos=botpos}
if Text.cursor_past_screen_bottom(State) then if Text.cursor_past_screen_bottom(State) then
@ -516,7 +495,7 @@ function Text.word_left(State)
if State.cursor1.pos == 1 then if State.cursor1.pos == 1 then
break break
end 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 break
end end
Text.left(State) Text.left(State)
@ -528,7 +507,7 @@ function Text.word_left(State)
break break
end end
assert(State.cursor1.pos > 1) 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 break
end end
end end
@ -537,20 +516,20 @@ end
function Text.word_right(State) function Text.word_right(State)
-- skip some whitespace -- skip some whitespace
while true do 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 break
end 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 break
end end
Text.right_without_scroll(State) Text.right_without_scroll(State)
end end
while true do while true do
Text.right_without_scroll(State) 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 break
end 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 break
end end
end end
@ -569,19 +548,11 @@ function Text.match(s, pos, pat)
end end
function Text.left(State) function Text.left(State)
assert(State.lines[State.cursor1.line].mode == 'text')
if State.cursor1.pos > 1 then if State.cursor1.pos > 1 then
State.cursor1.pos = State.cursor1.pos-1 State.cursor1.pos = State.cursor1.pos-1
else elseif State.cursor1.line > 1 then
local new_cursor_line = State.cursor1.line State.cursor1.line = State.cursor1.line-1
while new_cursor_line > 1 do State.cursor1.pos = utf8.len(State.lines[State.cursor1.line]) + 1
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
end end
if Text.lt1(State.cursor1, State.screen_top1) then if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1) local top2 = Text.to2(State, State.screen_top1)
@ -598,19 +569,11 @@ function Text.right(State)
end end
function Text.right_without_scroll(State) 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]) then
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
State.cursor1.pos = State.cursor1.pos+1 State.cursor1.pos = State.cursor1.pos+1
else elseif State.cursor1.line <= #State.lines-1 then
local new_cursor_line = State.cursor1.line State.cursor1.line = State.cursor1.line+1
while new_cursor_line <= #State.lines-1 do State.cursor1.pos = 1
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
end end
end end
@ -633,23 +596,7 @@ function Text.cursor_at_final_screen_line(State)
end end
function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
local y = State.top if State.top > App.screen.height - State.line_height then
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
--? print('scroll up') --? print('scroll up')
Text.snap_cursor_to_bottom_of_screen(State) Text.snap_cursor_to_bottom_of_screen(State)
end end
@ -665,24 +612,11 @@ function Text.snap_cursor_to_bottom_of_screen(State)
while true do while true do
--? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos) --? 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.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
local h = State.line_height if y - h < State.top then
if y - h < State.top then break
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
end end
y = y - h
top2 = Text.previous_screen_line(State, top2) top2 = Text.previous_screen_line(State, top2)
end end
--? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos) --? 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) 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 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_pos = line_cache.screen_line_starting_pos[screen_line_index]
local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos) 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.data, screen_line_starting_byte_offset)) --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line, screen_line_starting_byte_offset))
local nexty = y + State.line_height local nexty = y + State.line_height
if my < nexty then if my < nexty then
-- On all wrapped screen lines but the final one, clicks past end of -- 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') --? print('past end of non-final line; return')
return line_cache.screen_line_starting_pos[screen_line_index+1]-1 return line_cache.screen_line_starting_pos[screen_line_index+1]-1
end 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) --? 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 return screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1
end end
@ -737,14 +671,14 @@ function Text.screen_line_width(State, line_index, i)
local line = State.lines[line_index] local line = State.lines[line_index]
local line_cache = State.line_cache[line_index] local line_cache = State.line_cache[line_index]
local start_pos = line_cache.screen_line_starting_pos[i] 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 local screen_line
if i < #line_cache.screen_line_starting_pos then if i < #line_cache.screen_line_starting_pos then
local past_end_pos = line_cache.screen_line_starting_pos[i+1] local past_end_pos = line_cache.screen_line_starting_pos[i+1]
local past_end_offset = Text.offset(line.data, past_end_pos) local past_end_offset = Text.offset(line, past_end_pos)
screen_line = string.sub(line.data, start_offset, past_end_offset-1) screen_line = string.sub(line, start_offset, past_end_offset-1)
else else
screen_line = string.sub(line.data, start_pos) screen_line = string.sub(line, start_pos)
end end
local screen_line_text = App.newText(love.graphics.getFont(), screen_line) local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
return App.width(screen_line_text) return App.width(screen_line_text)
@ -847,9 +781,6 @@ function Text.x(s, pos)
end end
function Text.to2(State, loc1) 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} local result = {line=loc1.line, screen_line=1}
Text.populate_screen_line_starting_pos(State, loc1.line) Text.populate_screen_line_starting_pos(State, loc1.line)
for i=#State.line_cache[loc1.line].screen_line_starting_pos,1,-1 do 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} return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}
elseif loc2.line == 1 then elseif loc2.line == 1 then
return loc2 return loc2
elseif State.lines[loc2.line-1].mode == 'drawing' then
return {line=loc2.line-1, screen_line=1, screen_pos=1}
else else
local l = State.lines[loc2.line-1] local l = State.lines[loc2.line-1]
Text.populate_screen_line_starting_pos(State, 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) function Text.populate_screen_line_starting_pos(State, line_index)
local line = State.lines[line_index] local line = State.lines[line_index]
if line.mode ~= 'text' then return end
local line_cache = State.line_cache[line_index] local line_cache = State.line_cache[line_index]
if line_cache.screen_line_starting_pos then if line_cache.screen_line_starting_pos then
return return

View File

@ -14,34 +14,6 @@ function test_initial_state()
check_eq(Editor_state.screen_top1.pos, 1, 'F - test_initial_state/screen_top:pos') check_eq(Editor_state.screen_top1.pos, 1, 'F - test_initial_state/screen_top:pos')
end 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() function test_backspace_from_start_of_final_line()
io.write('\ntest_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 -- 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') edit.run_after_keychord(Editor_state, 'right')
-- no change to data, selection is reset -- no change to data, selection is reset
check_nil(Editor_state.selection1.line, 'F - test_cursor_movement_without_shift_resets_selection') 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 end
function test_edit_deletes_selection() function test_edit_deletes_selection()
@ -647,7 +619,7 @@ function test_edit_deletes_selection()
-- press a key -- press a key
edit.run_after_textinput(Editor_state, 'x') edit.run_after_textinput(Editor_state, 'x')
-- selected text is deleted and replaced with the key -- 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 end
function test_edit_with_shift_key_deletes_selection() 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') App.fake_key_release('lshift')
-- selected text is deleted and replaced with the key -- selected text is deleted and replaced with the key
check_nil(Editor_state.selection1.line, 'F - test_edit_with_shift_key_deletes_selection') 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 end
function test_copy_does_not_reset_selection() function test_copy_does_not_reset_selection()
@ -708,7 +680,7 @@ function test_cut()
edit.run_after_keychord(Editor_state, 'C-x') edit.run_after_keychord(Editor_state, 'C-x')
check_eq(App.clipboard, 'a', 'F - test_cut/clipboard') check_eq(App.clipboard, 'a', 'F - test_cut/clipboard')
-- selected text is deleted -- 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 end
function test_paste_replaces_selection() function test_paste_replaces_selection()
@ -729,7 +701,7 @@ function test_paste_replaces_selection()
edit.run_after_keychord(Editor_state, 'C-v') edit.run_after_keychord(Editor_state, 'C-v')
-- selection is reset since shift key is not pressed -- selection is reset since shift key is not pressed
-- selection includes the newline, so it's also deleted -- 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 end
function test_deleting_selection_may_scroll() function test_deleting_selection_may_scroll()
@ -755,7 +727,7 @@ function test_deleting_selection_may_scroll()
edit.run_after_keychord(Editor_state, 'backspace') edit.run_after_keychord(Editor_state, 'backspace')
-- page scrolls up -- page scrolls up
check_eq(Editor_state.screen_top1.line, 1, 'F - test_deleting_selection_may_scroll') 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 end
function test_edit_wrapping_text() function test_edit_wrapping_text()
@ -821,8 +793,8 @@ function test_insert_newline_at_start_of_line()
edit.run_after_keychord(Editor_state, 'return') 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.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.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[1], '', '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[2], 'abc', 'F - test_insert_newline_at_start_of_line/data:2')
end end
function test_insert_from_clipboard() function test_insert_from_clipboard()
@ -994,36 +966,6 @@ function test_pagedown()
App.screen.check(y, 'ghi', 'F - test_pagedown/screen:2') App.screen.check(y, 'ghi', 'F - test_pagedown/screen:2')
end 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() function test_pagedown_often_shows_start_of_wrapping_line()
io.write('\ntest_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 -- 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} Editor_state.cursor1 = {line=2, pos=1}
-- backspace joins with previous line -- backspace joins with previous line
edit.run_after_keychord(Editor_state, 'backspace') 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 end
-- some tests for operating over selections created using Shift- chords -- 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} Editor_state.selection1 = {line=1, pos=2}
-- backspace deletes the selected character, even though it's after the cursor -- backspace deletes the selected character, even though it's after the cursor
edit.run_after_keychord(Editor_state, 'backspace') 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 -- 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.line, 1, "F - test_backspace_over_selection/cursor:line")
check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection/cursor:pos") 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} Editor_state.selection1 = {line=1, pos=1}
-- backspace deletes the selected character -- backspace deletes the selected character
edit.run_after_keychord(Editor_state, 'backspace') 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 -- 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.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") 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} Editor_state.selection1 = {line=4, pos=2}
-- backspace deletes the region and joins the remaining portions of lines on either side -- backspace deletes the region and joins the remaining portions of lines on either side
edit.run_after_keychord(Editor_state, 'backspace') 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[1], '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[2], 'mno', "F - test_backspace_over_multiple_lines/data:2")
-- cursor remains at start of selection -- 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.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") 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} Editor_state.selection1 = {line=1, pos=4}
-- backspace deletes rest of line without joining to any other line -- backspace deletes rest of line without joining to any other line
edit.run_after_keychord(Editor_state, 'backspace') 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[1], '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[2], 'def', "F - test_backspace_to_start_of_line/data:2")
-- cursor remains at start of selection -- 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.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") 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} Editor_state.selection1 = {line=2, pos=3}
-- backspace deletes beginning of line without joining to any other line -- backspace deletes beginning of line without joining to any other line
edit.run_after_keychord(Editor_state, 'backspace') 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[1], '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[2], 'f', "F - test_backspace_to_start_of_line/data:2")
-- cursor remains at start of selection -- 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.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") 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) edit.draw(Editor_state)
-- delete selected text -- delete selected text
edit.run_after_textinput(Editor_state, 'x') 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') check_nil(Editor_state.selection1.line, 'F - test_undo_restores_selection/baseline:selection')
-- undo -- undo
edit.run_after_keychord(Editor_state, 'C-z') edit.run_after_keychord(Editor_state, 'C-z')
@ -2022,7 +1964,7 @@ function test_search()
io.write('\ntest_search') io.write('\ntest_search')
App.screen.init{width=120, height=60} App.screen.init{width=120, height=60}
Editor_state = edit.initialize_test_state() 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) Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=1, pos=1} Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1} Editor_state.screen_top1 = {line=1, pos=1}

View File

@ -60,19 +60,7 @@ function snapshot(State, s,e)
-- deep copy lines without cached stuff like text fragments -- deep copy lines without cached stuff like text fragments
for i=s,e do for i=s,e do
local line = State.lines[i] local line = State.lines[i]
if line.mode == 'text' then table.insert(event.lines, State.lines[i])
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
end end
return event return event
end end