diff --git a/README.md b/README.md index 9df3cfb..63e8e26 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,8 @@ -# Plain text with lines +# An editor for plain text. -An editor for plain text where you can also seamlessly insert line drawings. Designed above all to be easy to modify and give you early warning if your modifications break something. -http://akkartik.name/lines.html - ## Getting started Install [LÖVE](https://love2d.org). It's just a 5MB download, open-source and @@ -17,13 +14,13 @@ optionally with a file path to edit. Alternatively, turn it into a .love file you can double-click on: ``` -$ zip -r /tmp/lines2.love *.lua +$ zip -r /tmp/text2.love *.lua ``` -By default, lines2.love reads/writes the file `lines.txt` in +By default, text2.love reads/writes the file `lines.txt` in [a directory relative to this app](https://love2d.org/wiki/love.filesystem.getSourceBaseDirectory). -To open a different file, drop it on the lines2.love window. +To open a different file, drop it on the text2.love window. ## Keyboard shortcuts @@ -35,12 +32,7 @@ While editing text: * `alt+right`/`alt+left` to jump to the next/previous word, respectively * mouse drag or `shift` + movement to select text, `ctrl+a` to select all -For shortcuts while editing drawings, consult the online help. Either: -* hover on a drawing and hit `ctrl+h`, or -* click on a drawing to start a stroke and then press and hold `h` to see your - options at any point during a stroke. - -lines2.love has been exclusively tested so far with a US keyboard layout. If +Exclusively tested so far with a US keyboard layout. If you use a different layout, please let me know if things worked, or if you found anything amiss: http://akkartik.name/contact @@ -51,38 +43,16 @@ found anything amiss: http://akkartik.name/contact * No support yet for right-to-left languages. * Undo/redo may be sluggish in large files. Large files may grow sluggish in - other ways. lines2.love works well in all circumstances with files under - 50KB. + other ways. Works well in all circumstances with files under 50KB. * If you kill the process, say by force-quitting because things things get sluggish, you can lose data. -* lines2.love assumes a file always contains at least one line of text. You - can violate this invariant by editing the file outside lines2.love. Don't do - that. - -* If you make the first line a drawing there's currently no way to insert - lines above it. - -* Help screen may show up in multiple drawings at a time. That feels a bit - klunky. - -* Clicking on a drawing to focus cursor on it adds a point. Orphaned points - disappears on reload, but still. Klunky. To avoid this you can move the - cursor using the keyboard, but who can remember that? - -* No clipping yet for drawings. In particular, circles/squares/rectangles and - point labels can overflow a drawing. - * If you ever see a crash when clicking on the mouse, it might be because a mouse press and release need to happen in separate frames. Try pressing and releasing more slowly and let me know if that helps or not. This is klunky, sorry. -* Touchpads can drag the mouse pointer using a light touch or a heavy click. - On Linux, drags using the light touch get interrupted when a key is pressed. - You'll have to press down to drag. - * Can't scroll while selecting text with mouse. * No scrollbars yet. That stuff is hard. @@ -92,18 +62,8 @@ found anything amiss: http://akkartik.name/contact This repo is a fork of [lines.love](http://akkartik.name/lines.html), aiming to be more elegant and have fewer bugs. Updates to it can be downloaded from: -* https://git.merveilles.town/akkartik/lines2.love -* https://git.sr.ht/~akkartik/lines2.love - -## Associated tools - -* https://codeberg.org/akkartik/lines2md exports files to Markdown and - (non-editable) SVG. -* https://git.sr.ht/~akkartik/lines2html.love exports files to html and inline - SVG. -* https://codeberg.org/eril/lines2html.love provides the option to export - just the drawings to a directory. Also provides a CLI, which should be more - natural for many people. +* https://git.merveilles.town/akkartik/text2.love +* https://git.sr.ht/~akkartik/text2.love ## Feedback diff --git a/drawing.lua b/drawing.lua deleted file mode 100644 index 9f9d7af..0000000 --- a/drawing.lua +++ /dev/null @@ -1,724 +0,0 @@ --- primitives for editing drawings -Drawing = {} - --- All drawings span 100% of some conceptual 'page width' and divide it up --- into 256 parts. -function Drawing.draw(Editor, line_index, starty) - local line = Editor.lines[line_index] - local pmx,pmy = love.mouse.getPosition() - local height = Drawing.pixels(line.h, Editor.width) - if line_index == Editor.cursor.line then - App.color(Cursor_color) - love.graphics.rectangle('line', Editor.left, starty, Editor.width, height) - elseif geom.in_rect(pmx,pmy, Editor.left, starty, Editor.width, height) then - App.color(Icon_color) - love.graphics.rectangle('line', Editor.left, starty, Editor.width, height) - end - if geom.in_rect(pmx,pmy, Editor.left, starty, Editor.width, height) then - App.color(Icon_color) - if icon[Editor.current_drawing_mode] then - icon[Editor.current_drawing_mode](Editor.right-22, starty+4) - else - icon[Editor.previous_drawing_mode](Editor.right-22, starty+4) - end - - if love.mouse.isDown(1) and love.keyboard.isDown('h') then - draw_help_with_mouse_pressed(Editor, line_index) - return - end - end - - if line.show_help then - draw_help_without_mouse_pressed(Editor, line_index) - return - end - - local mx = Drawing.coord(pmx-Editor.left, Editor.width) - local my = Drawing.coord(pmy-starty, Editor.width) - - for _,shape in ipairs(line.shapes) do - 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, starty, Editor.left,Editor.right) - end - - local function px(x) return Drawing.pixels(x, Editor.width)+Editor.left end - local function py(y) return Drawing.pixels(y, Editor.width)+starty end - for i,p in ipairs(line.points) do - if p.deleted == nil then - if Drawing.near(p, mx,my, Editor.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 Editor.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_width - if p.name == '' then - name_width = Editor.font:getWidth('m') - else - name_width = Editor.font:getWidth(p.name) - end - love.graphics.rectangle('fill', x,y, name_width, Editor.line_height) - end - end - end - end - App.color(Current_stroke_color) - Drawing.draw_pending_shape(line, starty, Editor.left,Editor.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 - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end -end - -function Drawing.draw_pending_shape(drawing, top, left,right) - local width = right-left - local pmx,pmy = love.mouse.getPosition() - 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_release - 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 r = round(geom.dist(center.x, center.y, mx, my)) - local cx,cy = px(center.x), py(center.y) - love.graphics.circle('line', cx,cy, Drawing.pixels(r, width)) - 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 - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end -end - -function Drawing.in_current_drawing(Editor, x,y, left,right) - if Editor.cursor.mode ~= 'drawing' then return false end - assert(Editor.lines[Editor.cursor.line].mode == 'drawing') - return Drawing.in_drawing(Editor, Editor.cursor.line, x,y, left,right) -end - -function Drawing.in_drawing(Editor, drawing_index, x,y, left,right) - assert(Editor.lines[drawing_index].mode == 'drawing') - local _, starty = edit.to_coord(Editor, {mode='drawing', line=drawing_index}) - if starty == nil then return false end -- outside current page - local drawing = Editor.lines[drawing_index] - local width = right-left - return y >= starty and y < starty + Drawing.pixels(drawing.h, width) and x >= left and x < right -end - -function Drawing.mouse_press(Editor, drawing_index, x,y, mouse_button) - local drawing = Editor.lines[drawing_index] - local _, starty = edit.to_coord(Editor, {mode='drawing', line=drawing_index}) - local cx = Drawing.coord(x-Editor.left, Editor.width) - local cy = Drawing.coord(y-starty, Editor.width) - if Editor.current_drawing_mode == 'freehand' then - drawing.pending = {mode=Editor.current_drawing_mode, points={{x=cx, y=cy}}} - elseif Editor.current_drawing_mode == 'line' or Editor.current_drawing_mode == 'manhattan' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, Editor.width) - drawing.pending = {mode=Editor.current_drawing_mode, p1=j} - elseif Editor.current_drawing_mode == 'polygon' or Editor.current_drawing_mode == 'rectangle' or Editor.current_drawing_mode == 'square' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, Editor.width) - drawing.pending = {mode=Editor.current_drawing_mode, vertices={j}} - elseif Editor.current_drawing_mode == 'circle' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, Editor.width) - drawing.pending = {mode=Editor.current_drawing_mode, center=j} - elseif Editor.current_drawing_mode == 'move' then - -- all the action is in mouse_release - elseif Editor.current_drawing_mode == 'name' then - -- nothing - else - assert(false, ('unknown drawing mode %s'):format(Editor.current_drawing_mode)) - end -end - --- a couple of operations on drawings need to constantly check the state of the mouse -function Drawing.update(Editor) - if Editor.cursor.mode ~= 'drawing' then return end - local drawing = Editor.lines[Editor.cursor.line] - local _, starty = edit.to_coord(Editor, Editor.cursor) - if starty == nil then - -- some event cleared starty just this frame - -- draw in this frame will soon set starty - -- just skip this frame - return - end - assert(drawing.mode == 'drawing', 'Drawing.update: line is not a drawing') - local pmx,pmy = love.mouse.getPosition() - local mx = Drawing.coord(pmx-Editor.left, Editor.width) - local my = Drawing.coord(pmy-starty, Editor.width) - if love.mouse.isDown(1) then - if Drawing.in_current_drawing(Editor, pmx,pmy, Editor.left,Editor.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 Editor.current_drawing_mode == 'move' then - if Drawing.in_current_drawing(Editor, pmx, pmy, Editor.left,Editor.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_release(Editor, x,y, mouse_button) - if Editor.cursor.mode ~= 'drawing' then return end - if Editor.current_drawing_mode == 'move' then - Editor.current_drawing_mode = Editor.previous_drawing_mode - Editor.previous_drawing_mode = nil - else - local drawing = Editor.lines[Editor.cursor.line] - local _, starty = edit.to_coord(Editor, Editor.cursor) - 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-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.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-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.width) - else - drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, Editor.width) - end - local p2 = drawing.points[drawing.pending.p2] - love.mouse.setPosition(Editor.left+Drawing.pixels(p2.x, Editor.width), starty+Drawing.pixels(p2.y, Editor.width)) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'polygon' then - local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.width)) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'rectangle' then - assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: rectangle has too many pending vertices') - if #drawing.pending.vertices == 2 then - local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.width)) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, Editor.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, 'Drawing.mouse_release: square has too many pending vertices') - if #drawing.pending.vertices == 2 then - local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.width)) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, Editor.width)) - table.insert(drawing.shapes, drawing.pending) - end - end - elseif drawing.pending.mode == 'circle' then - local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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 - assert(false, ('unknown drawing mode %s'):format(drawing.pending.mode)) - end - drawing.pending = {} - end - end -end - -function Drawing.keychord_press(Editor, chord) - local pmx,pmy = love.mouse.getPosition() - if chord == 'C-p' and not love.mouse.isDown(1) then - Editor.current_drawing_mode = 'freehand' - elseif love.mouse.isDown(1) and chord == 'l' then - Editor.current_drawing_mode = 'line' - local drawing = Editor.lines[Editor.cursor.line] - 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, Editor.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 love.mouse.isDown(1) then - Editor.current_drawing_mode = 'line' - elseif love.mouse.isDown(1) and chord == 'm' then - Editor.current_drawing_mode = 'manhattan' - local drawing = Drawing.select_drawing_at_mouse(Editor) - 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, Editor.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 love.mouse.isDown(1) then - Editor.current_drawing_mode = 'manhattan' - elseif chord == 'C-g' and not love.mouse.isDown(1) then - Editor.current_drawing_mode = 'polygon' - elseif love.mouse.isDown(1) and chord == 'g' then - Editor.current_drawing_mode = 'polygon' - local drawing = Editor.lines[Editor.cursor.line] - 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, Editor.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 love.mouse.isDown(1) then - Editor.current_drawing_mode = 'rectangle' - elseif love.mouse.isDown(1) and chord == 'r' then - Editor.current_drawing_mode = 'rectangle' - local drawing = Editor.lines[Editor.cursor.line] - 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, Editor.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 love.mouse.isDown(1) then - Editor.current_drawing_mode = 'square' - elseif love.mouse.isDown(1) and chord == 's' then - Editor.current_drawing_mode = 'square' - local drawing = Editor.lines[Editor.cursor.line] - 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, Editor.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 love.mouse.isDown(1) and chord == 'p' and Editor.current_drawing_mode == 'polygon' then - local drawing = Editor.lines[Editor.cursor.line] - local _, starty = edit.to_coord(Editor, Editor.cursor) - local mx,my = Drawing.coord(pmx-Editor.left, Editor.width), Drawing.coord(pmy-starty, Editor.width) - local j = Drawing.find_or_insert_point(drawing.points, mx,my, Editor.width) - table.insert(drawing.pending.vertices, j) - elseif love.mouse.isDown(1) and chord == 'p' and (Editor.current_drawing_mode == 'rectangle' or Editor.current_drawing_mode == 'square') then - local drawing = Editor.lines[Editor.cursor.line] - local _, starty = edit.to_coord(Editor, Editor.cursor) - local mx,my = Drawing.coord(pmx-Editor.left, Editor.width), Drawing.coord(pmy-starty, Editor.width) - local j = Drawing.find_or_insert_point(drawing.points, mx,my, Editor.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 love.mouse.isDown(1) then - Editor.current_drawing_mode = 'circle' - elseif love.mouse.isDown(1) and chord == 'a' and Editor.current_drawing_mode == 'circle' then - local drawing = Editor.lines[Editor.cursor.line] - local _, starty = edit.to_coord(Editor, Editor.cursor) - drawing.pending.mode = 'arc' - local mx,my = Drawing.coord(pmx-Editor.left, Editor.width), Drawing.coord(pmy-starty, Editor.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 love.mouse.isDown(1) and chord == 'o' then - Editor.current_drawing_mode = 'circle' - local drawing = Editor.lines[Editor.cursor.line] - 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, Editor.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 love.mouse.isDown(1) then - if Drawing.in_current_drawing(Editor, pmx, pmy, Editor.left,Editor.right) then - local drawing = Editor.lines[Editor.cursor.line] - local point_index,p = Drawing.select_point_at_mouse(Editor) - if Editor.previous_drawing_mode == nil then - Editor.previous_drawing_mode = Editor.current_drawing_mode - end - Editor.current_drawing_mode = 'move' - drawing.pending = {mode=Editor.current_drawing_mode, target_point=p, target_point_index=point_index} - end - elseif chord == 'C-n' and not love.mouse.isDown(1) then - if Drawing.in_current_drawing(Editor, pmx, pmy, Editor.left,Editor.right) then - local drawing = Editor.lines[Editor.cursor.line] - local point_index,p = Drawing.select_point_at_mouse(Editor) - if Editor.previous_drawing_mode == nil then - Editor.previous_drawing_mode = Editor.current_drawing_mode - end - Editor.current_drawing_mode = 'name' - p.name = '' - drawing.pending = {mode=Editor.current_drawing_mode, target_point=point_index} - end - elseif chord == 'C-d' and not love.mouse.isDown(1) then - if Drawing.in_current_drawing(Editor, pmx, pmy, Editor.left,Editor.right) then - local _, starty = edit.to_coord(Editor, Editor.cursor) - local mx, my = Drawing.coord(pmx-Editor.left, Editor.width), Drawing.coord(pmy-starty, Editor.width) - local drawing = Editor.lines[Editor.cursor.line] - local point_index,p = Drawing.select_point_at_mouse(Editor) - for _,shape in ipairs(drawing.shapes) do - if point_index and Drawing.contains_point(shape, point_index) then - if shape.mode == 'polygon' then - local idx = table.find(shape.vertices, point_index) - assert(idx, 'point to delete is not in vertices') - table.remove(shape.vertices, idx) - if #shape.vertices < 3 then - shape.mode = 'deleted' - end - else - shape.mode = 'deleted' - end - end - if geom.on_shape(mx,my, drawing, shape) then - shape.mode = 'deleted' - end - end - if point_index then - drawing.points[point_index].deleted = true - end - end - elseif chord == 'C-h' and not love.mouse.isDown(1) then - local drawing = Drawing.select_drawing_at_mouse(Editor) - if drawing then - drawing.show_help = true - end - elseif chord == 'escape' and love.mouse.isDown(1) then - local _,drawing = Drawing.current_drawing(Editor) - 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.select_point_at_mouse(Editor) - if Editor.cursor.mode ~= 'drawing' then return end - local x,y = love.mouse.getPosition() - if not Drawing.in_current_drawing(Editor, x,y, Editor.left,Editor.right) then return end - local _, starty = edit.to_coord(Editor, Editor.cursor) - local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.width) - local drawing = Editor.lines[Editor.cursor.line] - assert(drawing.mode == 'drawing') - for i,point in ipairs(drawing.points) do - if Drawing.near(point, mx,my, Editor.width) then - return i,point - end - end -end - -function Drawing.select_drawing_at_mouse(Editor) - for drawing_index,drawing in ipairs(Editor.lines) do - if drawing.mode == 'drawing' then - local x,y = love.mouse.getPosition() - if Drawing.in_drawing(Editor, drawing_index, x,y, Editor.left,Editor.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 - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end -end - -function Drawing.smoothen(shape) - assert(shape.mode == 'freehand', 'can only smoothen freehand shapes') - 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.find_or_insert_point(points, x,y, width) - -- check if UI would snap the two points together - for i,point in ipairs(points) do - if Drawing.near(point, x,y, width) then - return i - end - end - table.insert(points, {x=x, y=y}) - return #points -end - -function Drawing.near(point, x,y, width) - local px,py = Drawing.pixels(x, width),Drawing.pixels(y, width) - local cx,cy = Drawing.pixels(point.x, width), Drawing.pixels(point.y, width) - return (cx-px)*(cx-px) + (cy-py)*(cy-py) < Same_point_distance*Same_point_distance -end - -function Drawing.pixels(n, width) -- parts to pixels - return math.floor(n*width/256) -end -function Drawing.coord(n, width) -- pixels to parts - return math.floor(n*256/width) -end - -function table.find(h, x) - for k,v in pairs(h) do - if v == x then - return k - end - end -end diff --git a/edit.lua b/edit.lua index 4450694..a6c892d 100644 --- a/edit.lua +++ b/edit.lua @@ -1,25 +1,12 @@ -- some constants people might like to tweak Text_color = {r=0, g=0, b=0} Cursor_color = {r=1, g=0, b=0} -Stroke_color = {r=0, g=0, b=0} -Current_stroke_color = {r=0.7, g=0.7, b=0.7} -- in process of being drawn -Current_name_background_color = {r=1, g=0, b=0, a=0.1} -- name currently being edited -Focus_stroke_color = {r=1, g=0, b=0} -- what mouse is hovering over Highlight_color = {r=0.7, g=0.7, b=0.9} -- selected text -Icon_color = {r=0.7, g=0.7, b=0.7} -- color of current mode icon in drawings -Help_color = {r=0, g=0.5, b=0} -Help_background_color = {r=0, g=0.5, b=0, a=0.1} Margin_top = 15 Margin_left = 25 Margin_right = 25 -Drawing_padding_top = 10 -Drawing_padding_bottom = 10 -Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom - -Same_point_distance = 4 -- pixel distance at which two points are considered the same - edit = {} function new_editor(top, left, right, bottom, font, font_height, line_height) -- currently always draws to bottom of screen @@ -34,27 +21,12 @@ function new_editor(top, left, right, bottom, font, font_height, line_height) - font_height = font_height, line_height = line_height, - -- The editor is for editing an array of lines. A line is either text or a drawing. + -- The editor is for editing an array of lines. -- The array of lines can never be empty; there must be at least one line for positioning a cursor at. -- - -- A text line is a table with: + -- A line is a table with: -- mode = 'text', -- string data, - -- A drawing line is a table with: - -- mode = 'drawing' - -- 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 -- We need to track a couple of _locations_: @@ -62,7 +34,6 @@ function new_editor(top, left, right, bottom, font, font_height, line_height) - cursor = nil, -- location where editing will occur -- Valid location values: -- {mode='text', line=, pos=} - -- {mode='drawing', line=} selection1 = {}, -- some extra state to compute selection between mouse press and release @@ -70,16 +41,12 @@ function new_editor(top, left, right, bottom, font, font_height, line_height) - old_selection1 = nil, mousepress_shift = nil, - current_drawing_mode = 'line', -- one of the available shape modes - previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points - filename = love.filesystem.getSourceBaseDirectory()..'/lines.txt', -- '/' should work even on Windows next_save = nil, -- undo history = {}, next_history = 1, - drawing_before = nil, -- extra state for drawing operations -- search search_term = nil, @@ -90,13 +57,8 @@ end -- new_editor function edit.scroll_to_top(Editor) assert(#Editor.lines > 0) - if Editor.lines[1].mode == 'text' then - Editor.screen_top = {mode='text', line=1, pos=1} - Editor.cursor = {mode='text', line=1, pos=1} - else - Editor.screen_top = {mode='drawing', line=1} - Editor.cursor = {mode='drawing', line=1} - end + Editor.screen_top = {mode='text', line=1, pos=1} + Editor.cursor = {mode='text', line=1, pos=1} end function edit.valid_loc(Editor, loc) @@ -142,69 +104,47 @@ function edit.draw(Editor) local y = Editor.top for line_index, line in array.each(Editor.lines, Editor.screen_top.line) do - if line.mode == 'text' then - local x = Editor.left - local initpos = 1 - if line_index == Editor.screen_top.line then - initpos = Editor.screen_top.pos - end ---? print('screen line', line_index, initpos, y) - if line.data == '' then - -- button to insert new drawing - button(Editor, 'draw', {x=Editor.left-Margin_left+4, y=y+4, w=12,h=12, bg={r=1,g=1,b=0}, - icon = icon.insert_drawing, - onpress1 = function() - Editor.drawing_before = snapshot(Editor, line_index-1, line_index) - table.insert(Editor.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}}) - Editor.cursor = {mode='drawing', line=line_index} - schedule_save(Editor) - record_undo_event(Editor, {before=Editor.drawing_before, after=snapshot(Editor, line_index-1, line_index+1)}) - end, - }) - else - for pos,char in utf8chars(line.data, initpos) do - local w = Editor.font:getWidth(char) - if char:match('%s') then - if Text.line_wrap_at_word_boundary(Editor, x, line.data, pos) then - do_it(x,y, w, line_index, pos, char) - x = Editor.left - y = y + Editor.line_height - if y + Editor.line_height > Editor.bottom then - break - end ---? print('screen line', line_index, pos+1, y) - draw_just_cursor(x,y, line_index, pos) - else - do_it(x,y, w, line_index, pos, char) - x = x + w - end - else - if x+w > Editor.right then - draw_just_cursor(x,y, line_index, pos) - x = Editor.left - y = y + Editor.line_height - if y + Editor.line_height > Editor.bottom then - break - end ---? print('screen line', line_index, pos, y) - do_it(x,y, w, line_index, pos, char) - else - do_it(x,y, w, line_index, pos, char) - end - x = x + w - end - end - end - -- draw cursor if it's at end of line - do_it(x,y, 0, line_index, utf8.len(line.data)+1, '') - y = y + Editor.line_height - elseif line.mode == 'drawing' then - local h = Drawing_padding_height + Drawing.pixels(line.h, Editor.width) - Drawing.draw(Editor, line_index, y+Drawing_padding_top) - y = y + h - else - assert(false, ('unknown line mode %s'):format(line.mode)) + local x = Editor.left + local initpos = 1 + if line_index == Editor.screen_top.line then + initpos = Editor.screen_top.pos end +--? print('screen line', line_index, initpos, y) + for pos,char in utf8chars(line.data, initpos) do + local w = Editor.font:getWidth(char) + if char:match('%s') then + if Text.line_wrap_at_word_boundary(Editor, x, line.data, pos) then + do_it(x,y, w, line_index, pos, char) + x = Editor.left + y = y + Editor.line_height + if y + Editor.line_height > Editor.bottom then + break + end +--? print('screen line', line_index, pos+1, y) + draw_just_cursor(x,y, line_index, pos) + else + do_it(x,y, w, line_index, pos, char) + x = x + w + end + else + if x+w > Editor.right then + draw_just_cursor(x,y, line_index, pos) + x = Editor.left + y = y + Editor.line_height + if y + Editor.line_height > Editor.bottom then + break + end +--? print('screen line', line_index, pos, y) + do_it(x,y, w, line_index, pos, char) + else + do_it(x,y, w, line_index, pos, char) + end + x = x + w + end + end + -- draw cursor if it's at end of line + do_it(x,y, 0, line_index, utf8.len(line.data)+1, '') + y = y + Editor.line_height if y + Editor.line_height > Editor.bottom then break end @@ -215,7 +155,6 @@ function edit.draw(Editor) end function edit.update(Editor, dt) - Drawing.update(Editor, dt) if Editor.next_save and Editor.next_save < Current_time then save_to_disk(Editor) Editor.next_save = nil @@ -254,25 +193,20 @@ function edit.mouse_press(Editor, mx,my, mouse_button) end Editor.cursor = loc - if loc.mode == 'text' then - -- prepare for a drag selecting text - -- delicate dance between cursor/selection and old cursor/selection - -- scenarios: - -- regular press+release: sets cursor, clears selection - -- shift press+release: - -- sets selection to old cursor if not set otherwise leaves it untouched - -- sets cursor - -- press and hold to start a selection: sets selection on press, cursor on release - -- press and hold, then press shift: ignore shift - -- i.e. mouse_release should never look at shift state - Editor.old_cursor1 = Editor.cursor - Editor.old_selection1 = Editor.selection1 - Editor.mousepress_shift = shift_down() - Editor.selection1 = deepcopy(loc) - else - Editor.drawing_before = snapshot(Editor, loc.line) - Drawing.mouse_press(Editor, loc.line, mx,my, mouse_button) - end + -- prepare for a drag selecting text + -- delicate dance between cursor/selection and old cursor/selection + -- scenarios: + -- regular press+release: sets cursor, clears selection + -- shift press+release: + -- sets selection to old cursor if not set otherwise leaves it untouched + -- sets cursor + -- press and hold to start a selection: sets selection on press, cursor on release + -- press and hold, then press shift: ignore shift + -- i.e. mouse_release should never look at shift state + Editor.old_cursor1 = Editor.cursor + Editor.old_selection1 = Editor.selection1 + Editor.mousepress_shift = shift_down() + Editor.selection1 = deepcopy(loc) end function edit.mouse_release(Editor, mx,my, mouse_button) @@ -280,18 +214,8 @@ function edit.mouse_release(Editor, mx,my, mouse_button) Editor.mouse_down = nil local loc = edit.to_loc(Editor, mx,my) - if loc.mode == 'text' then - Editor.cursor = loc - edit.clean_up_mouse_press(Editor) - else - Editor.selection1 = {} - Drawing.mouse_release(Editor, mx,my, mouse_button) - schedule_save(Editor) - if Editor.drawing_before then - record_undo_event(Editor, {before=Editor.drawing_before, after=snapshot(Editor, Editor.cursor.line)}) - Editor.drawing_before = nil - end - end + Editor.cursor = loc + edit.clean_up_mouse_press(Editor) end function edit.clean_up_mouse_press(Editor) @@ -327,12 +251,6 @@ function edit.text_input(Editor, t) if Editor.search_term then Editor.search_term = Editor.search_term..t Text.search_next(Editor) - elseif Editor.cursor.mode == 'drawing' and Editor.current_drawing_mode == 'name' then - local before = snapshot(Editor, Editor.cursor.line) - local drawing = Editor.lines[Editor.cursor.line] - local p = drawing.points[drawing.pending.target_point] - p.name = p.name..t - record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)}) else -- why is this here? Text.text_input(Editor, t) @@ -439,42 +357,6 @@ function edit.keychord_press(Editor, chord, key) maybe_snap_cursor_to_top_of_screen(Editor) record_undo_event(Editor, {before=before, after=snapshot(Editor, before_line, Editor.cursor.line)}) schedule_save(Editor) - -- dispatch to drawing or text - elseif love.mouse.isDown(1) or chord:sub(1,2) == 'C-' then - if Editor.cursor.mode == 'drawing' then - local before = snapshot(Editor, Editor.cursor.line) - Drawing.keychord_press(Editor, chord) - record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)}) - schedule_save(Editor) - end - elseif chord == 'escape' and not love.mouse.isDown(1) then - for _,line in ipairs(Editor.lines) do - if line.mode == 'drawing' then - line.show_help = false - end - end - elseif Editor.cursor.mode == 'drawing' and Editor.current_drawing_mode == 'name' then - if chord == 'return' then - Editor.current_drawing_mode = Editor.previous_drawing_mode - Editor.previous_drawing_mode = nil - else - local before = snapshot(Editor, Editor.cursor.line) - local drawing = Editor.lines[Editor.cursor.line] - local p = drawing.points[drawing.pending.target_point] - if chord == 'escape' then - p.name = nil - record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)}) - elseif chord == 'backspace' then - local len = utf8.len(p.name) - if len > 0 then - 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(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)}) - end - end - end - schedule_save(Editor) else Text.keychord_press(Editor, chord) end diff --git a/file.lua b/file.lua index f629620..77a88d6 100644 --- a/file.lua +++ b/file.lua @@ -24,11 +24,7 @@ function load_from_file(infile) while true do local line = infile_next_line() if line == nil then break end - if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated - table.insert(result, load_drawing(infile_next_line)) - else - table.insert(result, {mode='text', data=line}) - end + table.insert(result, {mode='text', data=line}) end end if #result == 0 then @@ -44,99 +40,17 @@ function save_to_disk(Editor) error('failed to write to "'..Editor.filename..'"') end for _,line in ipairs(Editor.lines) do - if line.mode == 'drawing' then - store_drawing(outfile, line) - else - outfile:write(line.data) - outfile:write('\n') - end + outfile:write(line.data) + outfile:write('\n') end outfile:close() end -function load_drawing(infile_next_line) - local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}} - while true do - local line = infile_next_line() - assert(line, 'drawing in file is incomplete') - if line == '```' then break end - local shape = json.decode(line) - if shape.mode == 'freehand' then - -- no changes needed - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local name = shape.p1.name - shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p2].name = name - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - for i,p in ipairs(shape.vertices) do - local name = p.name - shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.vertices[i]].name = name - end - elseif shape.mode == 'circle' or shape.mode == 'arc' then - local name = shape.center.name - shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.center].name = name - elseif shape.mode == 'deleted' then - -- ignore - else - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - 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)) - outfile:write('\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) - outfile:write('\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) - outfile:write('\n') - elseif shape.mode == 'circle' then - outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius})) - outfile:write('\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})) - outfile:write('\n') - elseif shape.mode == 'deleted' then - -- ignore - else - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end - end - outfile:write('```\n') -end - -- for tests function load_array(a) local result = {} - local next_line = ipairs(a) - local i,line,drawing = 0, '' - while true do - i,line = next_line(a, i) - if i == nil then break end - if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated - i, drawing = load_drawing_from_array(next_line, a, i) - table.insert(result, drawing) - else - table.insert(result, {mode='text', data=line}) - end + for _,line in ipairs(a) do + table.insert(result, {mode='text', data=line}) end if #result == 0 then table.insert(result, {mode='text', data=''}) @@ -144,43 +58,6 @@ function load_array(a) return result end -function load_drawing_from_array(iter, a, i) - local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}} - local line - while true do - i, line = iter(a, i) - assert(i, 'drawing in array is incomplete') - if line == '```' then break end - local shape = json.decode(line) - if shape.mode == 'freehand' then - -- no changes needed - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local name = shape.p1.name - shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p2].name = name - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - for i,p in ipairs(shape.vertices) do - local name = p.name - shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.vertices[i]].name = name - end - elseif shape.mode == 'circle' or shape.mode == 'arc' then - local name = shape.center.name - shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.center].name = name - elseif shape.mode == 'deleted' then - -- ignore - else - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end - table.insert(drawing.shapes, shape) - end - return i, drawing -end - function open_for_reading(filename) local result = nativefs.newFile(filename) local ok, err = result:open('r') diff --git a/geom.lua b/geom.lua deleted file mode 100644 index 5abb603..0000000 --- a/geom.lua +++ /dev/null @@ -1,171 +0,0 @@ -geom = {} - -function geom.in_rect(x,y, top,left,width,height) - return x >= left and x <= left+width and y >= top and y <= top+height -end - -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 - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end -end - -function geom.on_freehand(x,y, drawing, shape) - local prev - for _,p in ipairs(shape.points) do - if prev then - if geom.on_line(x,y, drawing, {p1=prev, p2=p}) then - return true - end - end - prev = p - end - return false -end - -function geom.on_line(x,y, drawing, shape) - local p1,p2 - if type(shape.p1) == 'number' then - p1 = drawing.points[shape.p1] - p2 = drawing.points[shape.p2] - else - p1 = shape.p1 - p2 = shape.p2 - end - if p1.x == p2.x then - if math.abs(p1.x-x) > 2 then - return false - end - local y1,y2 = p1.y,p2.y - if y1 > y2 then - y1,y2 = y2,y1 - end - return y >= y1-2 and y <= y2+2 - end - -- has the right slope and intercept - local m = (p2.y - p1.y) / (p2.x - p1.x) - local yp = p1.y + m*(x-p1.x) - if yp < y-2 or yp > y+2 then - return false - end - -- between endpoints - local k = (x-p1.x) / (p2.x-p1.x) - return k > -0.005 and k < 1.005 -end - -function geom.on_polygon(x,y, drawing, shape) - local prev - for _,p in ipairs(shape.vertices) do - if prev then - if geom.on_line(x,y, drawing, {p1=prev, p2=p}) then - return true - end - end - prev = p - end - return geom.on_line(x,y, drawing, {p1=shape.vertices[1], p2=shape.vertices[#shape.vertices]}) -end - --- are (x3,y3) and (x4,y4) on the same side of the line between (x1,y1) and (x2,y2) -function geom.same_side(x1,y1, x2,y2, x3,y3, x4,y4) - if x1 == x2 then - return math.sign(x3-x1) == math.sign(x4-x1) - end - if y1 == y2 then - return math.sign(y3-y1) == math.sign(y4-y1) - end - local m = (y2-y1)/(x2-x1) - return math.sign(m*(x3-x1) + y1-y3) == math.sign(m*(x4-x1) + y1-y4) -end - -function math.sign(x) - if x > 0 then - return 1 - elseif x == 0 then - return 0 - elseif x < 0 then - return -1 - end -end - -function geom.angle_with_hint(x1, y1, x2, y2, hint) - local result = geom.angle(x1,y1, x2,y2) - if hint then - -- Smooth the discontinuity where angle goes from positive to negative. - -- The hint is a memory of which way we drew it last time. - while result > hint+math.pi/10 do - result = result-math.pi*2 - end - while result < hint-math.pi/10 do - result = result+math.pi*2 - end - end - return result -end - --- result is from -π/2 to 3π/2, approximately adding math.atan2 from Lua 5.3 --- (LÖVE is Lua 5.1) -function geom.angle(x1,y1, x2,y2) - local result = math.atan((y2-y1)/(x2-x1)) - if x2 < x1 then - result = result+math.pi - end - return result -end - --- is the line between x,y and cx,cy at an angle between s and e? -function geom.angle_between(ox,oy, x,y, s,e) - local angle = geom.angle(ox,oy, x,y) - if s > e then - s,e = e,s - end - -- I'm not sure this is right or ideal.. - angle = angle-math.pi*2 - if s <= angle and angle <= e then - return true - end - angle = angle+math.pi*2 - if s <= angle and angle <= e then - return true - end - angle = angle+math.pi*2 - return s <= angle and angle <= e -end - -function geom.dist(x1,y1, x2,y2) return ((x2-x1)^2+(y2-y1)^2)^0.5 end diff --git a/help.lua b/help.lua deleted file mode 100644 index 8177fcb..0000000 --- a/help.lua +++ /dev/null @@ -1,147 +0,0 @@ -function draw_help_without_mouse_pressed(Editor, drawing_index) - local drawing = Editor.lines[drawing_index] - local starty = Text.starty(Editor, drawing_index) - App.color(Help_color) - local y = starty+10 - love.graphics.print("Things you can do:", Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("* Press the mouse button to start drawing a "..current_shape(Editor), Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("* Hover on a point and press 'ctrl+u' to pick it up and start moving it,", Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("then press the mouse button to drop it", Editor.left+30+Editor.font:getWidth('* '),y) - y = y + Editor.line_height - love.graphics.print("* Hover on a point and press 'ctrl+n', type a name, then press 'enter'", Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("* Hover on a point or shape and press 'ctrl+d' to delete it", Editor.left+30,y) - y = y + Editor.line_height - if Editor.current_drawing_mode ~= 'freehand' then - love.graphics.print("* Press 'ctrl+p' to switch to drawing freehand strokes", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'line' then - love.graphics.print("* Press 'ctrl+l' to switch to drawing lines", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'manhattan' then - love.graphics.print("* Press 'ctrl+m' to switch to drawing horizontal/vertical lines", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'circle' then - love.graphics.print("* Press 'ctrl+o' to switch to drawing circles/arcs", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'polygon' then - love.graphics.print("* Press 'ctrl+g' to switch to drawing polygons", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'rectangle' then - love.graphics.print("* Press 'ctrl+r' to switch to drawing rectangles", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'square' then - love.graphics.print("* Press 'ctrl+s' to switch to drawing squares", Editor.left+30,y) - y = y + Editor.line_height - end - love.graphics.print("* Press 'ctrl+=' or 'ctrl+-' to zoom in or out, ctrl+0 to reset zoom", Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("Press 'esc' now to hide this message", Editor.left+30,y) - y = y + Editor.line_height - App.color(Help_background_color) - love.graphics.rectangle('fill', Editor.left,starty, Editor.width, math.max(Drawing.pixels(drawing.h, Editor.width),y-starty)) -end - -function draw_help_with_mouse_pressed(Editor, drawing_index) - local drawing = Editor.lines[drawing_index] - local starty = Text.starty(Editor, drawing_index) - App.color(Help_color) - local y = starty+10 - love.graphics.print("You're currently drawing a "..current_shape(Editor, drawing.pending), Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print('Things you can do now:', Editor.left+30,y) - y = y + Editor.line_height - if Editor.current_drawing_mode == 'freehand' then - love.graphics.print('* Release the mouse button to finish drawing the stroke', Editor.left+30,y) - y = y + Editor.line_height - elseif Editor.current_drawing_mode == 'line' or Editor.current_drawing_mode == 'manhattan' then - love.graphics.print('* Release the mouse button to finish drawing the line', Editor.left+30,y) - y = y + Editor.line_height - elseif Editor.current_drawing_mode == 'circle' then - if drawing.pending.mode == 'circle' then - love.graphics.print('* Release the mouse button to finish drawing the circle', Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("* Press 'a' to draw just an arc of a circle", Editor.left+30,y) - else - love.graphics.print('* Release the mouse button to finish drawing the arc', Editor.left+30,y) - end - y = y + Editor.line_height - elseif Editor.current_drawing_mode == 'polygon' then - love.graphics.print('* Release the mouse button to finish drawing the polygon', Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("* Press 'p' to add a vertex to the polygon", Editor.left+30,y) - y = y + Editor.line_height - elseif Editor.current_drawing_mode == 'rectangle' then - if #drawing.pending.vertices < 2 then - love.graphics.print("* Press 'p' to add a vertex to the rectangle", Editor.left+30,y) - y = y + Editor.line_height - else - love.graphics.print('* Release the mouse button to finish drawing the rectangle', Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("* Press 'p' to replace the second vertex of the rectangle", Editor.left+30,y) - y = y + Editor.line_height - end - elseif Editor.current_drawing_mode == 'square' then - if #drawing.pending.vertices < 2 then - love.graphics.print("* Press 'p' to add a vertex to the square", Editor.left+30,y) - y = y + Editor.line_height - else - love.graphics.print('* Release the mouse button to finish drawing the square', Editor.left+30,y) - y = y + Editor.line_height - love.graphics.print("* Press 'p' to replace the second vertex of the square", Editor.left+30,y) - y = y + Editor.line_height - end - end - love.graphics.print("* Press 'esc' then release the mouse button to cancel the current shape", Editor.left+30,y) - y = y + Editor.line_height - y = y + Editor.line_height - if Editor.current_drawing_mode ~= 'line' then - love.graphics.print("* Press 'l' to switch to drawing lines", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'manhattan' then - love.graphics.print("* Press 'm' to switch to drawing horizontal/vertical lines", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'circle' then - love.graphics.print("* Press 'o' to switch to drawing circles/arcs", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'polygon' then - love.graphics.print("* Press 'g' to switch to drawing polygons", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'rectangle' then - love.graphics.print("* Press 'r' to switch to drawing rectangles", Editor.left+30,y) - y = y + Editor.line_height - end - if Editor.current_drawing_mode ~= 'square' then - love.graphics.print("* Press 's' to switch to drawing squares", Editor.left+30,y) - y = y + Editor.line_height - end - App.color(Help_background_color) - love.graphics.rectangle('fill', Editor.left,starty, Editor.width, math.max(Drawing.pixels(drawing.h, Editor.width),y-starty)) -end - -function current_shape(Editor, shape) - if Editor.current_drawing_mode == 'freehand' then - return 'freehand stroke' - elseif Editor.current_drawing_mode == 'line' then - return 'straight line' - elseif Editor.current_drawing_mode == 'manhattan' then - return 'horizontal/vertical line' - elseif Editor.current_drawing_mode == 'circle' and shape and shape.start_angle then - return 'arc' - else - return Editor.current_drawing_mode - end -end diff --git a/icons.lua b/icons.lua deleted file mode 100644 index b8d9aa7..0000000 --- a/icons.lua +++ /dev/null @@ -1,67 +0,0 @@ -icon = {} - -function icon.insert_drawing(button_params) - local x,y = button_params.x, button_params.y - App.color(Icon_color) - love.graphics.rectangle('line', x,y, 12,12) - love.graphics.line(x,y+6, x+12,y+6) - love.graphics.line(x+6,y, x+6,y+12) -end - -function icon.hyperlink_decoration(button_params) - local x,y = button_params.x, button_params.y - -- hack: set the hyperlink color so that caller can draw the text of the - -- hyperlink in the same color - App.color(Hyperlink_decoration_color) - love.graphics.line(x,y+Editor.line_height, x+button_params.w,y+Editor.line_height) -end - -function icon.freehand(x, y) - love.graphics.line(x+4,y+7,x+5,y+5) - love.graphics.line(x+5,y+5,x+7,y+4) - love.graphics.line(x+7,y+4,x+9,y+3) - love.graphics.line(x+9,y+3,x+10,y+5) - love.graphics.line(x+10,y+5,x+12,y+6) - love.graphics.line(x+12,y+6,x+13,y+8) - love.graphics.line(x+13,y+8,x+13,y+10) - love.graphics.line(x+13,y+10,x+14,y+12) - love.graphics.line(x+14,y+12,x+15,y+14) - love.graphics.line(x+15,y+14,x+15,y+16) -end - -function icon.line(x, y) - love.graphics.line(x+4,y+2, x+16,y+18) -end - -function icon.manhattan(x, y) - love.graphics.line(x+4,y+20, x+4,y+2) - love.graphics.line(x+4,y+2, x+10,y+2) - love.graphics.line(x+10,y+2, x+10,y+10) - love.graphics.line(x+10,y+10, x+18,y+10) -end - -function icon.polygon(x, y) - love.graphics.line(x+8,y+2, x+14,y+2) - love.graphics.line(x+14,y+2, x+18,y+10) - love.graphics.line(x+18,y+10, x+10,y+18) - love.graphics.line(x+10,y+18, x+4,y+12) - love.graphics.line(x+4,y+12, x+8,y+2) -end - -function icon.rectangle(x, y) - love.graphics.line(x+4,y+8, x+4,y+16) - love.graphics.line(x+4,y+16, x+16,y+16) - love.graphics.line(x+16,y+16, x+16,y+8) - love.graphics.line(x+16,y+8, x+4,y+8) -end - -function icon.square(x, y) - love.graphics.line(x+6,y+6, x+6,y+16) - love.graphics.line(x+6,y+16, x+16,y+16) - love.graphics.line(x+16,y+16, x+16,y+6) - love.graphics.line(x+16,y+6, x+6,y+6) -end - -function icon.circle(x, y) - love.graphics.circle('line', x+10,y+10, 8) -end diff --git a/main.lua b/main.lua index d88f06e..601c908 100644 --- a/main.lua +++ b/main.lua @@ -12,11 +12,6 @@ require('button') require('app') -require('icons') -require('drawing') -require('geom') -require('help') - require('file') require('edit') require('text') diff --git a/move.lua b/move.lua index 447a83f..2fe856c 100644 --- a/move.lua +++ b/move.lua @@ -1,13 +1,9 @@ ---- moving around the editor in terms of locations (loc) -- --- locations within text lines look like this: +-- locations look like this: -- {mode='text', line=, pos=} -- (pos counts in utf8 codepoints starting from 1) -- --- locations within drawings look like this: --- {mode='drawing', line=} --- (drawing lines are atomic as far as the cursor is concerned) --- -- all movements are built primarily using the following primitives, defined -- further down: -- - to_loc: (x, y) -> loc @@ -29,8 +25,6 @@ -- -- I have tried to make these definitions as clear as possible while being fast enough. -- My mental model for trading off performance for clarity: --- - text is the bottleneck; my line drawings are clear and cheap no matter --- how you go about things. -- - any computation limited to the number of characters a screen can show -- will be an order of magnitude faster than it needs to be to draw 30 -- frames per second. @@ -128,13 +122,11 @@ function Text.pagedown(Editor) end -- 1 scan function Text.start_of_line(Editor) - if Editor.cursor.mode == 'drawing' then return end Editor.cursor.pos = 1 maybe_snap_cursor_to_top_of_screen(Editor) -- 0-1 scan end -- 0-1 scan function Text.end_of_line(Editor) - if Editor.cursor.mode == 'drawing' then return end Editor.cursor.pos = utf8.len(Editor.lines[Editor.cursor.line].data) + 1 maybe_snap_cursor_to_bottom_of_screen(Editor) -- 0-1 scan end -- 0-1 scan @@ -227,59 +219,50 @@ function edit.to_loc(Editor, mx,my) local prevloc for line_index,line in array.each(Editor.lines, Editor.screen_top.line) do prevloc = {mode=line.mode, line=line_index} - if line.mode == 'text' then - local x = Editor.left - local initpos = 1 - if line_index == Editor.screen_top.line then - initpos = Editor.screen_top.pos - end - for pos, char in utf8chars(line.data, initpos) do - prevloc.pos = pos - local w = Editor.font:getWidth(char) -- width of char - if char:match('%s') then - if Text.line_wrap_at_word_boundary(Editor, x, line.data, pos) then - if my < y+Editor.line_height then - return {mode='text', line=line_index, pos=pos} - end - x = Editor.left - y = y + Editor.line_height - else - if my < y+Editor.line_height and mx < x+w then - return {mode='text', line=line_index, pos=pos} - end - x = x + w + local x = Editor.left + local initpos = 1 + if line_index == Editor.screen_top.line then + initpos = Editor.screen_top.pos + end + for pos, char in utf8chars(line.data, initpos) do + prevloc.pos = pos + local w = Editor.font:getWidth(char) -- width of char + if char:match('%s') then + if Text.line_wrap_at_word_boundary(Editor, x, line.data, pos) then + if my < y+Editor.line_height then + return {mode='text', line=line_index, pos=pos} + end + x = Editor.left + y = y + Editor.line_height + else + if my < y+Editor.line_height and mx < x+w then + return {mode='text', line=line_index, pos=pos} + end + x = x + w + end + else + if x+w > Editor.right then + if my < y+Editor.line_height then + return {mode='text', line=line_index, pos=pos} + end + x = Editor.left + y = y + Editor.line_height + if my < y+Editor.line_height and mx < x+w then + return {mode='text', line=line_index, pos=pos} end else - if x+w > Editor.right then - if my < y+Editor.line_height then - return {mode='text', line=line_index, pos=pos} - end - x = Editor.left - y = y + Editor.line_height - if my < y+Editor.line_height and mx < x+w then - return {mode='text', line=line_index, pos=pos} - end - else - if my < y+Editor.line_height and mx < x+w then - return {mode='text', line=line_index, pos=pos} - end + if my < y+Editor.line_height and mx < x+w then + return {mode='text', line=line_index, pos=pos} end - x = x+w end + x = x+w end - prevloc.pos = utf8.len(line.data)+1 - if my < y+Editor.line_height then - return {mode='text', line=line_index, pos=utf8.len(line.data)+1} - end - y = y + Editor.line_height - else - -- drawing - local h = Drawing.height(Editor, line) - if my < y+h then - return {mode='drawing', line=line_index} - end - y = y + h end + prevloc.pos = utf8.len(line.data)+1 + if my < y+Editor.line_height then + return {mode='text', line=line_index, pos=utf8.len(line.data)+1} + end + y = y + Editor.line_height if y + Editor.line_height > Editor.bottom then break end @@ -292,54 +275,45 @@ function edit.to_coord(Editor, loc) -- scans if edit.lt(loc, Editor.screen_top) then return end local y = Editor.top for line_index, line in array.each(Editor.lines, Editor.screen_top.line) do - if line.mode == 'text' then - local x = Editor.left - local initpos = 1 - if line_index == Editor.screen_top.line then - initpos = Editor.screen_top.pos - end - for pos,char in utf8chars(line.data, initpos) do - local w = Editor.font:getWidth(char) - if char:match('%s') then - if Text.line_wrap_at_word_boundary(Editor, x, line.data, pos) then - if loc.mode == 'text' and loc.line == line_index and loc.pos == pos then - return x, y - end - x = Editor.left - y = y + Editor.line_height - else - if loc.mode == 'text' and loc.line == line_index and loc.pos == pos then - return x, y - end - x = x + w + local x = Editor.left + local initpos = 1 + if line_index == Editor.screen_top.line then + initpos = Editor.screen_top.pos + end + for pos,char in utf8chars(line.data, initpos) do + local w = Editor.font:getWidth(char) + if char:match('%s') then + if Text.line_wrap_at_word_boundary(Editor, x, line.data, pos) then + if loc.mode == 'text' and loc.line == line_index and loc.pos == pos then + return x, y end + x = Editor.left + y = y + Editor.line_height else - if x+w > Editor.right then - x = Editor.left - y = y + Editor.line_height - if loc.mode == 'text' and loc.line == line_index and loc.pos == pos then - return x, y - end - else - if loc.mode == 'text' and loc.line == line_index and loc.pos == pos then - return x, y - end + if loc.mode == 'text' and loc.line == line_index and loc.pos == pos then + return x, y end x = x + w end + else + if x+w > Editor.right then + x = Editor.left + y = y + Editor.line_height + if loc.mode == 'text' and loc.line == line_index and loc.pos == pos then + return x, y + end + else + if loc.mode == 'text' and loc.line == line_index and loc.pos == pos then + return x, y + end + end + x = x + w end - if loc.mode == 'text' and loc.line == line_index and loc.pos == utf8.len(line.data)+1 then - return x, y - end - y = y + Editor.line_height - elseif line.mode == 'drawing' then - if loc.mode == 'drawing' and loc.line == line_index then - return Editor.left, y - end - y = y + Drawing.height(Editor, line) - else - assert(false, ('unknown line mode %s'):format(line.mode)) end + if loc.mode == 'text' and loc.line == line_index and loc.pos == utf8.len(line.data)+1 then + return x, y + end + y = y + Editor.line_height if y + Editor.line_height > Editor.bottom then break end @@ -353,30 +327,18 @@ function edit.down(Editor, loc, dy) -- scans local y = 0 local prevloc = loc for line_index, line in array.each(Editor.lines, loc.line) do - if line.mode == 'text' then - local screen_line_starts = Text.screen_line_starts(Editor, line_index) -- scan - local start_counting_screen_line_index = 1 - if line_index == loc.line then - start_counting_screen_line_index = Text.start_of_screen_line(Editor, loc) -- extra scan for just one line - end - for screen_line_index = start_counting_screen_line_index,#screen_line_starts do - local currloc = {mode='text', line=line_index, pos=screen_line_starts[screen_line_index]} - if y+Editor.line_height > dy then - return currloc - end - y = y + Editor.line_height - prevloc = currloc - end - elseif line.mode == 'drawing' then - local currloc = {mode='drawing', line=line_index} - local h = Drawing.height(Editor, line) - if y + h > dy then + local screen_line_starts = Text.screen_line_starts(Editor, line_index) -- scan + local start_counting_screen_line_index = 1 + if line_index == loc.line then + start_counting_screen_line_index = Text.start_of_screen_line(Editor, loc) -- extra scan for just one line + end + for screen_line_index = start_counting_screen_line_index,#screen_line_starts do + local currloc = {mode='text', line=line_index, pos=screen_line_starts[screen_line_index]} + if y+Editor.line_height > dy then return currloc end - y = y + h + y = y + Editor.line_height prevloc = currloc - else - assert(false, ('unknown line mode %s'):format(line.mode)) end if y + Editor.line_height > Editor.bottom - Editor.top then return nil @@ -392,30 +354,16 @@ function edit.up(Editor, loc, dy) -- scans local y = 0 for line_index = loc.line,1,-1 do local line = Editor.lines[line_index] - if line.mode == 'text' then - local screen_line_starts = Text.screen_line_starts(Editor, line_index) -- scan - local ignore_below_screen_line_index = #screen_line_starts+1 - if line_index == loc.line then - ignore_below_screen_line_index = Text.start_of_screen_line(Editor, loc) -- extra scan for just one line + local screen_line_starts = Text.screen_line_starts(Editor, line_index) -- scan + local ignore_below_screen_line_index = #screen_line_starts+1 + if line_index == loc.line then + ignore_below_screen_line_index = Text.start_of_screen_line(Editor, loc) -- extra scan for just one line + end + for screen_line_index = ignore_below_screen_line_index-1,1,-1 do + if y+Editor.line_height > dy then + return {mode='text', line=line_index, pos=screen_line_starts[screen_line_index]} end - for screen_line_index = ignore_below_screen_line_index-1,1,-1 do - if y+Editor.line_height > dy then - return {mode='text', line=line_index, pos=screen_line_starts[screen_line_index]} - end - y = y + Editor.line_height - end - elseif line.mode == 'drawing' then - local h = Drawing.height(Editor, line) - if line_index == loc.line then - -- ignore the current loc entirely if it's a drawing - h = 0 - end - if y + h > dy then - return {mode='drawing', line=line_index} - end - y = y + h - else - assert(false, ('unknown line mode %s'):format(line.mode)) + y = y + Editor.line_height end if y + Editor.line_height > Editor.bottom - Editor.top then return nil @@ -431,37 +379,20 @@ function edit._up_whole_screen_lines(Editor, loc, dy) -- scans local y = 0 for line_index = loc.line,1,-1 do local line = Editor.lines[line_index] - if line.mode == 'text' then - local screen_line_starts = Text.screen_line_starts(Editor, line_index) -- scan - local ignore_below_screen_line_index = #screen_line_starts+1 - if line_index == loc.line then - ignore_below_screen_line_index = Text.start_of_screen_line(Editor, loc) -- extra scan for just one line - end - for screen_line_index = ignore_below_screen_line_index-1,1,-1 do - local currloc = {mode='text', line=line_index, pos=screen_line_starts[screen_line_index]} - if y+Editor.line_height == dy then - return currloc - elseif y+Editor.line_height > dy then - return prevloc - end - y = y + Editor.line_height - prevloc = currloc - end - elseif line.mode == 'drawing' then - local h = Drawing.height(Editor, line) - if line_index == loc.line then - -- ignore the current loc entirely if it's a drawing - h = 0 - end - if y + h == dy then - return {mode='drawing', line=line_index} - elseif y + h > dy then + local screen_line_starts = Text.screen_line_starts(Editor, line_index) -- scan + local ignore_below_screen_line_index = #screen_line_starts+1 + if line_index == loc.line then + ignore_below_screen_line_index = Text.start_of_screen_line(Editor, loc) -- extra scan for just one line + end + for screen_line_index = ignore_below_screen_line_index-1,1,-1 do + local currloc = {mode='text', line=line_index, pos=screen_line_starts[screen_line_index]} + if y+Editor.line_height == dy then + return currloc + elseif y+Editor.line_height > dy then return prevloc end - y = y + h - prevloc = {mode='drawing', line=line_index} - else - assert(false, ('unknown line mode %s'):format(line.mode)) + y = y + Editor.line_height + prevloc = currloc end if y + Editor.line_height > Editor.bottom - Editor.top then return nil @@ -472,19 +403,11 @@ end -- 1 scan function edit.top(Editor) assert(#Editor.lines > 0) - if Editor.lines[1].mode == 'text' then - return {mode='text', line=1, pos=1} - else - return {mode='drawing', line=1} - end + return {mode='text', line=1, pos=1} end -- find the location at x=x0 on the same screen line as loc function edit.hor(Editor, loc, x0) -- scans line - if loc.mode == 'drawing' then - assert(Editor.lines[loc.line].mode == 'drawing') - return deepcopy(loc) - end local _, start_pos_of_screen_line = Text.start_of_screen_line(Editor, loc) -- scan local line = Editor.lines[loc.line] -- look within the screen line @@ -582,17 +505,7 @@ function Text.line_wrap_at_word_boundary(Editor, x, line, pos) end function edit.screen_line_height(Editor, loc) - if loc.mode == 'text' then - return Editor.line_height - else - local line = Editor.lines[loc.line] - assert(line.mode == 'drawing') - return Drawing.height(Editor, line) - end -end - -function Drawing.height(Editor, line) - return Drawing_padding_height + Drawing.pixels(line.h, Editor.width) + return Editor.line_height end function edit.eq(a, b) @@ -606,9 +519,6 @@ function edit.lt(a, b) if a.line > b.line then return false end - if a.mode == 'drawing' then - return false -- no further comparisons within drawings - end return a.pos < b.pos end @@ -619,9 +529,6 @@ function edit.le(a, b) if a.line > b.line then return false end - if a.mode == 'drawing' then - return true -- no further comparisons within drawings - end return a.pos <= b.pos end diff --git a/search.lua b/search.lua index 2d97038..afc3c4c 100644 --- a/search.lua +++ b/search.lua @@ -136,7 +136,6 @@ end function in_search(Editor, line_index, pos) if Editor.search_term == nil then return false end if #Editor.search_term == 0 then return false end - if Editor.cursor.mode == 'drawing' then return false end if line_index ~= Editor.cursor.line then return false end return find_at(Editor.lines[line_index].data, Editor.search_term, Editor.cursor.pos) and Editor.cursor.pos <= pos and pos <= Editor.cursor.pos+utf8.len(Editor.search_term)-1 diff --git a/text.lua b/text.lua index 2e8959e..bf830ca 100644 --- a/text.lua +++ b/text.lua @@ -73,14 +73,10 @@ function Text.keychord_press(Editor, chord) end elseif Editor.cursor.line > 1 then before = snapshot(Editor, Editor.cursor.line-1, Editor.cursor.line) - if Editor.lines[Editor.cursor.line-1].mode == 'drawing' then - table.remove(Editor.lines, Editor.cursor.line-1) - else - -- join lines - Editor.cursor.pos = utf8.len(Editor.lines[Editor.cursor.line-1].data)+1 - Editor.lines[Editor.cursor.line-1].data = Editor.lines[Editor.cursor.line-1].data..Editor.lines[Editor.cursor.line].data - table.remove(Editor.lines, Editor.cursor.line) - end + -- join lines + Editor.cursor.pos = utf8.len(Editor.lines[Editor.cursor.line-1].data)+1 + Editor.lines[Editor.cursor.line-1].data = Editor.lines[Editor.cursor.line-1].data..Editor.lines[Editor.cursor.line].data + table.remove(Editor.lines, Editor.cursor.line) Editor.cursor.line = Editor.cursor.line-1 end if Editor.screen_top.line > #Editor.lines then @@ -93,55 +89,38 @@ function Text.keychord_press(Editor, chord) schedule_save(Editor) end elseif chord == 'delete' then - if Editor.cursor.mode == 'drawing' then - assert(Editor.cursor.line < #Editor.lines) - local before = snapshot(Editor, Editor.cursor.line, Editor.cursor.line+1) - table.remove(Editor.lines, Editor.cursor.line) - assert(Editor.cursor.line <= #Editor.lines) - if Editor.lines[Editor.cursor.line].mode == 'text' then - Editor.cursor = {mode='text', line=Editor.cursor.line, pos=1} - else - Editor.cursor = {mode='drawing', line=Editor.cursor.line} - end - -- no need to scroll, but screen_top may have switched mode - if Editor.screen_top.line == Editor.cursor.line then - Editor.screen_top = deepcopy(Editor.cursor) - end - record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)}) - else - -- cursor in text line - if Editor.selection1.line then - Text.delete_selection(Editor, Editor.left, Editor.right) - schedule_save(Editor) - return - end - local before - if Editor.cursor.pos <= utf8.len(Editor.lines[Editor.cursor.line].data) then - before = snapshot(Editor, Editor.cursor.line) - else - before = snapshot(Editor, Editor.cursor.line, Editor.cursor.line+1) - end - if Editor.cursor.pos <= utf8.len(Editor.lines[Editor.cursor.line].data) then - local byte_start = utf8.offset(Editor.lines[Editor.cursor.line].data, Editor.cursor.pos) - local byte_end = utf8.offset(Editor.lines[Editor.cursor.line].data, Editor.cursor.pos+1) - if byte_start then - if byte_end then - Editor.lines[Editor.cursor.line].data = string.sub(Editor.lines[Editor.cursor.line].data, 1, byte_start-1)..string.sub(Editor.lines[Editor.cursor.line].data, byte_end) - else - Editor.lines[Editor.cursor.line].data = string.sub(Editor.lines[Editor.cursor.line].data, 1, byte_start-1) - end - -- no change to Editor.cursor.pos - end - elseif Editor.cursor.line < #Editor.lines then - if Editor.lines[Editor.cursor.line+1].mode == 'text' then - -- join lines - Editor.lines[Editor.cursor.line].data = Editor.lines[Editor.cursor.line].data..Editor.lines[Editor.cursor.line+1].data - end - table.remove(Editor.lines, Editor.cursor.line+1) - end - record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)}) + -- cursor in text line + if Editor.selection1.line then + Text.delete_selection(Editor, Editor.left, Editor.right) schedule_save(Editor) + return end + local before + if Editor.cursor.pos <= utf8.len(Editor.lines[Editor.cursor.line].data) then + before = snapshot(Editor, Editor.cursor.line) + else + before = snapshot(Editor, Editor.cursor.line, Editor.cursor.line+1) + end + if Editor.cursor.pos <= utf8.len(Editor.lines[Editor.cursor.line].data) then + local byte_start = utf8.offset(Editor.lines[Editor.cursor.line].data, Editor.cursor.pos) + local byte_end = utf8.offset(Editor.lines[Editor.cursor.line].data, Editor.cursor.pos+1) + if byte_start then + if byte_end then + Editor.lines[Editor.cursor.line].data = string.sub(Editor.lines[Editor.cursor.line].data, 1, byte_start-1)..string.sub(Editor.lines[Editor.cursor.line].data, byte_end) + else + Editor.lines[Editor.cursor.line].data = string.sub(Editor.lines[Editor.cursor.line].data, 1, byte_start-1) + end + -- no change to Editor.cursor.pos + end + elseif Editor.cursor.line < #Editor.lines then + if Editor.lines[Editor.cursor.line+1].mode == 'text' then + -- join lines + Editor.lines[Editor.cursor.line].data = Editor.lines[Editor.cursor.line].data..Editor.lines[Editor.cursor.line+1].data + end + table.remove(Editor.lines, Editor.cursor.line+1) + end + record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)}) + schedule_save(Editor) --== shortcuts that move the cursor elseif chord == 'left' then Text.left(Editor) @@ -228,11 +207,6 @@ function Text.keychord_press(Editor, chord) end function Text.insert_return(Editor) - if Editor.cursor.mode == 'drawing' then - table.insert(Editor.lines, Editor.cursor.line+1, {mode='text', data=''}) - Editor.cursor = {mode='text', line=Editor.cursor.line+1, pos=1} - return - end local byte_offset = Text.offset(Editor.lines[Editor.cursor.line].data, Editor.cursor.pos) table.insert(Editor.lines, Editor.cursor.line+1, {mode='text', data=string.sub(Editor.lines[Editor.cursor.line].data, byte_offset)}) Editor.lines[Editor.cursor.line].data = string.sub(Editor.lines[Editor.cursor.line].data, 1, byte_offset-1) diff --git a/undo.lua b/undo.lua index 381d0f4..291da00 100644 --- a/undo.lua +++ b/undo.lua @@ -50,8 +50,6 @@ function snapshot(Editor, s,e) screen_top=deepcopy(Editor.screen_top), selection=deepcopy(Editor.selection1), cursor=deepcopy(Editor.cursor), - current_drawing_mode=Drawing_mode, - previous_drawing_mode=Editor.previous_drawing_mode, lines={}, start_line=s, end_line=e,