new fork: rip out drawing support

[?]
Aug 14, 2022, 4:17 PM
MD3W5IRAC6UQALQE4LJC52VQNDO3I3HXF3XE2XHDABXBYJBUVAXQC

Dependencies

  • [2] WI7R44TD bugfix
  • [3] 2WGRQI5E missing shape modes in a couple more places
  • [4] JZKEIKO6 freudian typo
  • [5] E5FYDACS a likely source of issues
  • [6] UN7L3DNN avoid some string concatenations
  • [7] WIPDCP4U stop recording points for arcs
  • [8] F4QQIBEH clarify what "large files" means
  • [9] SBS2F7GR link to export tool
  • [10] C4VTBATA .
  • [11] FM7UDV2G no, bring back that defense
  • [12] 225JKBBO clean up a cross-test leakage
  • [13] 3OTESDW6 move drawing.starty into line cache
  • [14] FNJF2FMQ bugfix: online help
  • [15] SPNMXTYR have file API operate on state object
  • [16] IFTYOERM line.y -> line_cache.starty in a few more places
  • [17] KJQ5FEYV round coordinates to integers in a few places
  • [18] QJB4UHNV round one coordinate
  • [19] JIK7ZRYI bugfix: imprecision in drawing
  • [20] EAEGCJV5 rename
  • [21] 7QQXO4YY bugfix: handle drawings when updating screen top
  • [22] 7PZ4CQFV search: transparently handle drawings everywhere
  • [23] AMOPICKV bugfix: check after cursor on same line when searching upwards
  • [24] 5STHSG4U remove some duplication
  • [25] PNBKVYZ4 more cogent onboarding instructions
  • [26] 6YWPSNUK new mirror
  • [27] YT5P6TO6 bugfix: save previous file when dropping a new one on
  • [28] C6QTJYA4 keep online help inside of drawing
  • [29] 34TC5SYK record another known issue I don't know how to fix yet
  • [30] K4OBZSHE add args to some functions
  • [31] QYIFOHW3 first test!
  • [32] H2DPLWMV snapshot: wrapping long lines at word boundaries
  • [33] ILOA5BYF separate data structure for each line's cache data
  • [34] R3JZDBI2 drop heavyweight near check on file load/store
  • [35] D4FEFHQC flesh out Readme
  • [36] 2CH77LZC add args to some functions
  • [37] LUNH47XX make text and drawings the same width
  • [38] MXA3RZYK deduce left/right from state where possible
  • [39] M6TH7VSZ rip out notion of Line_width
  • [40] 3HVBAZPA add state arg to a few functions
  • [41] APYPFFS3 call edit rather than App callbacks in tests
  • [42] AD34IX2Z couple more tests
  • [43] 5T2E3PDV couple of bugfixes to file-handling
  • [44] HYEAFRZ2 split mouse_pressed events between Text and Drawing
  • [45] QXVD2RIF add state arg to Drawing.mouse_released
  • [46] VCMS2CWT bugfix: escape key to hide online help
  • [47] VTYCPKNH dead code
  • [48] AJB4LFRB try to maintain a reasonable line width
  • [49] TLOAPLBJ add a license
  • [50] BOFNXP5G clicking now moves the cursor even on long, wrapped lines
  • [51] ZHLO7K3M add args to some functions
  • [52] ZPUQSPQP extract a few methods
  • [53] WAQFRM2U fix contact link
  • [54] 2L4DL7PG go through App in a couple more places
  • [55] WJBZZQE4 fold together two largely similar cases
  • [56] HMODUNJE scroll on backspace
  • [57] H22OAXWE couple of TODOs
  • [58] R3XGABER chunk up some long lines
  • [59] BJ2C6F2B ignore 'name' mode in a few places
  • [60] 7JH2ZT3F add state arg to Drawing.draw
  • [61] JFFUF5AL override mouse state lookups in tests
  • [62] FS2ITYYH record a known issue
  • [63] 5DOTWNVM right margin
  • [64] PFT5Y2ZY move
  • [65] 4CXVIEBS add args to some functions
  • [66] QCPXQ2E3 add state arg to a few functions
  • [67] PTT4K4EU use the provided args everywhere
  • [68] 3QNOKBFM beginnings of a test harness
  • [69] RSZD5A7G forgot to add json.lua
  • [70] 42LVB4DE test: naming a point
  • [71] BERHYBXM fix help for rectangles and squares
  • [72] 253TWKDA clean up
  • [73] KZ5GAYRP this fixes the immediate regression
  • [74] 25V2GA6J taking stock
  • [75] P4376EXK add state arg to few functions
  • [76] KTZQ57HV replace globals with args in a few functions
  • [77] UV4EWOLY add args to some functions
  • [78] AVQ5MC5D finish uppercasing all globals
  • [79] HIKLULFQ extract a function
  • [80] OGUV4HSA remove some memory leaks from rendered fragments
  • [81] YCDYGEZU include drawing index in a few places
  • [82] JAXPXLEB set current_drawing_index with current_drawing
  • [83] DSLD74DK lots more tests
  • [84] ZZ2B5RPQ extract variables for drawing padding
  • [85] S2YQBEYC snapshot: test for a new regression
  • [86] CUIV2LE5 some typos
  • [87] EM276IH3 make a function oblivious to line data structure
  • [88] PYLBFWWE overzealous search-and-replace
  • [89] 2Y5GGGJ4 correct a mis-named threshold
  • [90] YGCT2D2O start loading settings as applicable
  • [91] HPVT467W initialize contains test state
  • [92] FFBIY74N bugfix: 'escape' to cancel a stroke
  • [93] IMEJA43L snapshot
  • [94] 2H67P75X switch arg for a function
  • [95] DRFE3B3Z mouse buttons are integers, not strings
  • [96] 66X36NZN a little more prose describing manual_tests
  • [97] CIQN2MDE bugfix: typing a capital letter deletes selection
  • [98] DHI6IJCN selecting text and deleting selections
  • [99] 65HNIAOS make freehand drawings smoother
  • [100] T4FRZSYL delete an ancient, unused file
  • [101] DJSIRUMD a more radical attempt at ignoring nil y's
  • [102] XRLJDW3W casting about for more helpers to extract..
  • [103] LYN3L74W correct commit f3abc2cbf2
  • [104] E6TMJY2T lighter color for in-progress strokes
  • [105] R5QXEHUI somebody stop me
  • [106] R22PA3XR fix a second BSOD in #4 :/
  • [107] 7EQLPB3O bugfix: don't delete selection when moving cursor
  • [108] 62JEPVQ3 bugfix: backspace from start of final line
  • [109] YPHKZVWM extract a new variable
  • [110] UZVWYRTY missing temporary modes in a couple more places
  • [111] MYC7XR5Q bugfix: lines that aren't drawn from the start
  • [112] 5ZA3BRNY add state arg to a few functions
  • [113] BLWAYPKV extract a module
  • [114] DLQMM265 scroll past first page
  • [115] FYS7TCDW bugfix
  • [116] SPSW74Y5 add state arg to Text.keychord_pressed
  • [117] CLRJI4QK bugfix: backspace from start of file
  • [118] XNFTJHC4 split keyboard handling between Text and Drawing
  • [119] FKNXK2OA switch to line index in a function
  • [120] V5MJRFOZ bugfix: down arrow doesn't scroll up unnecessarily
  • [121] KMRJOSLY bugfix: delete selection before pasting
  • [122] AH744RFR show when we're naming a point
  • [123] TGZAJUEF bring back a set of constants
  • [124] TGHAJBES use line cache for drawings as well
  • [125] EGH7XDBK support non-text lines in Text.to2
  • [126] 6J3NXBYG affordance to adjust width for word wrap
  • [127] CE4LZV4T drop last couple of manual tests
  • [128] 5L7K4GBD clicking to the right of a wrapped line
  • [129] TVCPXAAU rename
  • [130] BJ5X5O4A let's prevent the text cursor from ever getting on a drawing
  • [131] NHA7RUFI move current mode indicator slightly
  • [132] XX7G2FFJ intermingle freehand line drawings with text
  • [133] 2ZYV7D3W handle tab characters
  • [134] BXJMGTV2 hoist couple of variables out
  • [135] MP2TBKU6 bugfix: crash in Text.up() after return
  • [136] 6LIPEQ5I more robust transitions to temporary modes
  • [137] KOYAJWE4 extract a couple more methods
  • [138] 6LJZN727 handle chords
  • [139] L6XA5EY2 test: moving a point
  • [140] NQH7DEEW bugfix: missed rename in one file
  • [141] OWK3U6VD tests for drawing polygons
  • [142] EMRPLZPW drop an arg from a function
  • [143] PTDO2SOT add state arg to schedule_save
  • [144] F3OOGMME switch freehand hotkey to eliminate conflict with search
  • [145] VJ77YABH more efficient undo/redo
  • [146] PR4KIAZD first stab at equally hacky cursor down support
  • [147] 4KC7I3E2 make colors easier to edit
  • [148] 3QQZ7W4E bring couple more globals back to the app level
  • [149] GGJEDJOO add args to some functions
  • [150] UHB4GARJ left/right margin -> left/right coordinates
  • [151] HALS7E5U more clearly skip prints before screen top
  • [152] 6VQIWTQU standardize on ordering of cases
  • [153] C45WCXJ2 keep drawings within the line width slider as well
  • [154] PVEZKGAC bugfix: recompute screen lines in backspace/delete
  • [155] ODLKHO7B switch to line index in a function
  • [156] QFC3WRDZ chunking by simple local variable
  • [157] 3RGHOJ25 DRY some code
  • [158] 7DPPMI2U another integer coordinate
  • [159] WLJCIXYM add state arg to a few functions
  • [160] KEPVDTCG .
  • [161] CPZGQT72 go through and fix similar issues
  • [162] 4CTZOJPC stop pretending globals are local
  • [163] 3ZSUBI57 drop some redundant args from Text.draw
  • [164] SRVDX4I5 local var
  • [165] 7OUJM7DL one missing transition between shape modes
  • [166] YJGADSGK delete unused arg
  • [167] SVJZZDC3 snapshot - no, that's all wrong
  • [168] 2K2YDMFH ignore 'deleted' shapes when saving to disk
  • [169] BXYVMS4A use available variables
  • [170] 2RXZ3PGO beginning of a new approach to scroll+wrap
  • [171] IEHG6ORO new fork for #1
  • [172] 242L3OQX bugfix: ensure Cursor_line is always on a text line
  • [173] 23MA4T3G add state arg to Drawing.keychord_pressed
  • [174] WZFMGVDT switch to line index in a function
  • [175] 6UZ2JNZE yet another key conflict
  • [176] NDHQN23G done passing left/right margins everywhere
  • [177] WTDKUACN rectangle and square shapes
  • [178] T7SJSJIH test: undo naming a point
  • [179] KICO5EE5 typos
  • [180] WLWNS6FB a bug I've never run into
  • [181] BYG5CEMV support for naming points
  • [182] ZTMRQZSW Readme
  • [183] AYE2VEGJ extract a couple of methods
  • [184] VHQCNMAR several more modules
  • [185] NUCZBE77 bugfix: alignment of help screen
  • [186] VXORMHME delete experimental REPL
  • [187] 7CLGG7J2 test: autosave after any shape
  • [188] 2L5MEZV3 experiment: new edit namespace
  • [189] U7M4M2F7 bugfix: don't rely on Screen_bottom1 while scrolling
  • [190] BW2IUB3K keep all text cache writes inside text.lua
  • [191] 2LOQ5ALJ add args to some functions
  • [192] 73OCE2MC after much struggle, a brute-force undo
  • [193] 6DE7RBZ6 move mouse_released events to Drawing
  • [194] 3OKKTUT4 up and down arrow now moving by screen line where possible
  • [195] LAW2O3NW extract variable Margin_left
  • [196] 52ZZ5TIE switch to line index in a function
  • [197] T57DTBX6 add args to some functions
  • [198] Y4VYNEGF test: autosave after name/move/delete of point
  • [199] PWHZPJJM always show current filename in window title
  • [200] HGC5RGJP switch to line index in a function
  • [201] F63Q4OV7 several bugfixes
  • [202] TRNWIQN6 more precise height calculation when scrolling up as much as possible while keeping cursor on screen
  • [203] EHSUSZMK more idiomatic variable names
  • [204] 62PZGSUC optimization: moving cursor to next word
  • [205] Z4KNS42N to open a file without a terminal, drag it on!
  • [206] U2TKUOID bugfix: undo drawing creation
  • [207] LXTTOB33 extract a couple of files
  • [208] LSYLEVBD drop some redundant args when clearing the cache
  • [209] EETIR4GX bugfix: skip over drawings when searching
  • [210] WSXSEZQ2 switch circles to 'o' to avoid conflicting with copy
  • [211] CRYGI3LR more drawing tests
  • [212] Z5HLXU4P add state arg to a few functions
  • [213] RT6EV6OP delegate update events to drawings
  • [214] KQWIMWJ5 deemphasize the terminal in Readme
  • [215] 7SFHSB47 rename
  • [216] B4YZWV6S bugfix: checking if a point is on a manhattan line
  • [217] YTSPVDZH first successful pagedown test, first bug found by test
  • [218] 7DYUAOI6 test: undo moving point
  • [219] NVSWVPW5 move
  • [220] WPW3AVFS more precise shape selection
  • [221] SQLVYKVJ rename
  • [222] 4AXV2HG4 all pending manual tests done!
  • [223] KAUD3YIK tests: deleting points/shapes
  • [224] IDGP4BJZ new known issue with drawings
  • [225] BULPIBEG beginnings of a module for the text editor
  • [226] OMTGHWMA yet another bugfix. But for how long?
  • [227] W2CQ7YNG more chunks, same approach
  • [228] VSBSWTE4 bugfix: where cursor is drawn
  • [229] LLAOOMUL bugfix: search upwards
  • [230] PYGMASTV disable some debug prints
  • [231] V3EABA35 skip multiple consecutive whitespace
  • [232] FZCKGO2I make local functions look different
  • [233] HOSPP2AN crisp font rendering
  • [234] VG75U7IM bugfix: typing should delete highlighted text
  • [235] ERQKFTPV extract method
  • [236] AVFRVNFR better handle moving points
  • [237] P5QNVXSN drop final mention of state global beyond main.lua
  • [238] DXT4QTAH a few more integer coordinates
  • [239] NYQ7HD4D move
  • [240] K2X6G75Z start writing some tests for drawings
  • [241] PX3736DX better error message
  • [242] MTJEVRJR add state arg to a few functions
  • [243] PJEQCTBL add state arg to Drawing.update
  • [244] BU3LUPY3 bugfix in help
  • [245] GSPXUEQO .
  • [246] DLQAEAC7 add state arg to Drawing.mouse_pressed
  • [247] LF7BWEG4 group all editor globals
  • [248] 4J2L6JMR bugfix: deleting a selection spanning pages
  • [249] 4WAFGF4Z selection bugfix
  • [250] 2MA33THZ few more transitions between shapes
  • [251] VFJEVPPO bugfix: function names
  • [252] O7QH4N4W speeding up copy, attempt 1
  • [253] 2XLZCWZC bugfix: rectangles and squares are now saved
  • [254] MDXGMZU2 disable all debug prints
  • [255] CTJ3IZGS add args to some functions
  • [256] VDJSUX2Q typos
  • [257] PX7DDEMO autosave slightly less aggressively
  • [258] IWYLK45K clicking to the right of a line within line width
  • [259] S2MISTTM add state arg to a few functions
  • [260] 4YDBYBA4 clean up memory leak experiments
  • [261] F65ADDGL add state arg to a few functions
  • [262] MLG2OGU7 things seem to feel snappier now
  • [263] D2GCFTTT clean up repl functionality
  • [264] FZBXBUFF bugfix: search
  • [265] ESETRNLB bugfix: printing the first part of a line at the bottom made it seem non-wrapping
  • [266] CVSRHMJ2 experiment: slightly adaptive scrolling
  • [267] 2TQR4PSY add args to some functions
  • [268] KECEMMMR extract couple of functions
  • [269] MGOQ5XAV start uppercasing globals
  • [270] CBPV5SSI stop handling nil screen_line_starting_pos everywhere
  • [271] GSV7DABC make online help fit within a drawing
  • [272] 4VKEE43Z bugfix
  • [273] TO6Y2G3U more decoupling editor tests from App
  • [274] PLKNHYZ4 extract a function
  • [275] BPWFKBXT new test: dragging and dropping a file on lines.love
  • [276] X3F7ECSL add state arg to some functions
  • [277] GN3IF4WF bugfix: pasting newlines
  • [278] CNCYMM6A make test initializations a little more obvious
  • [279] PK5U572C drop some extra args
  • [280] P66MRF3U bugfix: don't append metadata when it already exists
  • [281] 3GFQP6IR stop saving the entire file when modifying drawings
  • [282] OTIBCAUJ love2d scaffold
  • [283] HTWAM4NZ bugfix: scrolling in left/right movements
  • [284] OP643FFG move
  • [285] LNUHQOGH start passing in Editor_state explicitly
  • [286] MSOQI3A5 bugfix: check before cursor on same line
  • [287] CZRMAMSB clearer discription of how to run lines.love
  • [288] 2JLVAYHB start decoupling editor tests from App
  • [289] AVTNUQYR basic test-enabled framework
  • [290] FHSZYAZ2 more precise search highlighting
  • [291] 2LC3BM2N support other whitespace chars in word movements

Change contents

  • file deletion: drawing_tests.lua (----------)
    [27.2][27.1534:1575](),[27.1575][27.15:15]()
    -- 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_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)
    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.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
    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')
    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)
    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.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
    local drawing = Editor_state.lines[1]
    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°
    check_eq(#drawing.shapes, 1, 'F - test_draw_arc/#shapes')
    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)
    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.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_keychord(Editor_state, 'g') -- polygon mode
    -- second point
    edit.run_after_keychord(Editor_state, 'p') -- add point
    -- final point
    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)
    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.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_keychord(Editor_state, 'r') -- rectangle mode
    -- second point/first edge
    edit.run_after_keychord(Editor_state, 'p')
    -- override second point/first edge
    edit.run_after_keychord(Editor_state, 'p')
    -- release (decides 'thickness' of rectangle perpendicular to first edge)
    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)
    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.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_keychord(Editor_state, 'r') -- rectangle mode
    -- second point/first edge
    edit.run_after_keychord(Editor_state, 'p')
    -- override second point/first edge
    local drawing = Editor_state.lines[1]
    edit.run_after_keychord(Editor_state, 'p')
    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)
    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.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_keychord(Editor_state, 's') -- square mode
    -- second point/first edge
    edit.run_after_keychord(Editor_state, 'p')
    -- override second point/first edge
    edit.run_after_keychord(Editor_state, 'p')
    -- release (decides which side of first edge to draw square on)
    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
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    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'
    -- draw a line
    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
    check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:1')
    check_eq(p2.name, 'A', 'F - test_name_point')
    -- still in 'name' mode
    -- exit 'name' mode
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_name_point/mode:3')
    check_eq(p2.name, 'A', 'F - test_name_point')
    end
    function test_delete_point_from_polygon()
    io.write('\ntest_delete_point_from_polygon')
    -- create a drawing with two lines connected at a point
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    -- first point
    edit.run_after_keychord(Editor_state, 'g') -- polygon mode
    -- second point
    edit.run_after_keychord(Editor_state, 'p') -- add point
    -- third point
    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
    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
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    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'
    -- draw a line
    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')
    -- enter 'name' mode without moving the mouse
    check_eq(p2.name, 'A', 'F - test_undo_name_point/baseline')
    -- undo
    local drawing = Editor_state.lines[1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p2.name, '', 'F - test_undo_name_point') -- not quite what it was before, but close enough
    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
    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
    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
    check_eq(Editor_state.next_history, 4, 'F - test_undo_move_point/next_history')
    -- undo
    local drawing = Editor_state.lines[1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p2.x, 35, 'F - test_undo_move_point/x')
    check_eq(p2.y, 36, 'F - test_undo_move_point/y')
    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
    end
    function test_undo_delete_point()
    io.write('\ntest_undo_delete_point')
    -- create a drawing with two lines connected at a point
    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
    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
    local drawing = Editor_state.lines[1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    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')
    -- undo is saved
    Text.redraw_all(Editor_state)
    check_eq(#Editor_state.lines[1].shapes, 2, 'F - test_undo_delete_point/save')
    load_from_disk(Editor_state)
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    check_eq(Editor_state.next_history, 3, 'F - test_undo_move_point/next_history')
    edit.run_after_keychord(Editor_state, 'C-z')
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36)
    Editor_state.lines = load_array{'```lines', '```', ''}
    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)
    Text.redraw_all(Editor_state)
    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'
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- undo is saved
    Text.redraw_all(Editor_state)
    local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
    load_from_disk(Editor_state)
    check_eq(Editor_state.next_history, 2, 'F - test_undo_move_point/next_history')
    edit.run_after_keychord(Editor_state, 'C-z')
    edit.run_after_keychord(Editor_state, 'C-z') -- bug: need to undo twice
    edit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1)
    edit.run_after_keychord(Editor_state, 'C-u')
    edit.update(Editor_state, 0.05)
    App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)
    Editor_state.lines = load_array{'```lines', '```', ''}
    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)
    Text.redraw_all(Editor_state)
    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'
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- undo is saved
    Text.redraw_all(Editor_state)
    local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
    load_from_disk(Editor_state)
    check_eq(Editor_state.next_history, 3, 'F - test_undo_name_point/next_history')
    edit.run_after_keychord(Editor_state, 'C-z')
    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)
    edit.run_after_keychord(Editor_state, 'C-n')
    edit.run_after_textinput(Editor_state, 'A')
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(#Editor_state.history, 1, 'F - test_undo_name_point/baseline/history:1')
    --? print('a', Editor_state.lines.current_drawing)
    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)
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1)
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- change is saved
    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')
    load_from_disk(Editor_state)
    end
    function test_move_point()
    io.write('\ntest_move_point')
    -- create a drawing with a line
    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)
    -- line is saved to disk
    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')
    -- enter 'move' mode without moving the mouse
    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
    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
    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
    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')
    load_from_disk(Editor_state)
    end
    function test_delete_lines_at_point()
    io.write('\ntest_delete_lines_at_point')
    -- create a drawing with two lines connected at a point
    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
    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')
    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
    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
    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
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    -- first point
    edit.run_after_keychord(Editor_state, 'g') -- polygon mode
    -- second point
    edit.run_after_keychord(Editor_state, 'p') -- add point
    -- third point
    edit.run_after_keychord(Editor_state, 'p') -- add point
    -- fourth point
    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
    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')
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1)
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26)
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    App.mouse_move(Editor_state.left+25, Editor_state.top+Drawing_padding_top+26)
    Editor_state.lines = load_array{'```lines', '```', ''}
    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)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    -- wait for some time
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- deleted points disappear after file is reloaded
    Text.redraw_all(Editor_state)
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_delete_lines_at_point/save')
    load_from_disk(Editor_state)
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36)
    Editor_state.lines = load_array{'```lines', '```', ''}
    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)
    Text.redraw_all(Editor_state)
    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'
    function test_move_point_on_manhattan_line()
    io.write('\ntest_move_point_on_manhattan_line')
    -- create a drawing with a manhattan line
    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')
    -- enter 'move' mode
    check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point_on_manhattan_line/mode:1')
    -- move point
    -- line is no longer manhattan
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point_on_manhattan_line/baseline/shape:1')
    end
    edit.update(Editor_state, 0.05)
    App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)
    edit.run_after_keychord(Editor_state, 'C-u')
    edit.draw(Editor_state)
    Editor_state.lines = load_array{'```lines', '```', ''}
    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)
    Text.redraw_all(Editor_state)
    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'
    edit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1)
    edit.update(Editor_state, 0.05)
    App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)
    edit.run_after_keychord(Editor_state, 'C-u')
    edit.draw(Editor_state)
    load_from_disk(Editor_state)
    edit.update(Editor_state, 0)
    Editor_state.lines = load_array{'```lines', '```', ''}
    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)
    Text.redraw_all(Editor_state)
    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'
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:2')
    edit.run_after_textinput(Editor_state, 'A')
    edit.run_after_keychord(Editor_state, 'C-n')
    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_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1)
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+66)
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_square/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76)
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle_intermediate/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1)
    App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76)
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1)
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_polygon/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    check_eq(#drawing.points, 1, 'F - test_draw_arc/#points')
    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)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_arc/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'circle'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    -- 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.
    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')
    load_from_disk(Editor_state)
    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)
    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.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
    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)
    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.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
    local drawing = Editor_state.lines[1]
    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)
    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_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)
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    -- hover over drawing and press 'o' without holding mouse
    edit.run_after_keychord(Editor_state, 'o')
    -- no change to 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)
    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.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
    local drawing = Editor_state.lines[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)
    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')
    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)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle_mid_stroke/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_keys_do_not_affect_shape_when_mouse_up/drawing_mode')
    App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    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)
    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.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
    -- cancel
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 0, 'F - test_cancel_stroke/#shapes')
    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)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_cancel_stroke/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    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'
    App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_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+26, 1)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_horizontal_line/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'manhattan'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_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)
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_line/baseline/y')
    Editor_state.lines = load_array{'```lines', '```', ''}
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    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'
    function test_creating_drawing_saves()
    io.write('\ntest_creating_drawing_saves')
    App.screen.init{width=120, height=60}
    -- click on button to create drawing
    -- file not immediately saved
    check_nil(App.filesystem['foo'], 'F - test_creating_drawing_saves/early')
    -- wait until save
    App.wait_fake_time(3.1)
    -- filesystem contains drawing and an empty line of text
    check_eq(App.filesystem['foo'], '```lines\n```\n\n', 'F - test_creating_drawing_saves')
    end
    edit.update(Editor_state, 0)
    edit.update(Editor_state, 0.01)
    edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1)
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{}
    edit.draw(Editor_state)
    Text.redraw_all(Editor_state)
    Editor_state = edit.initialize_test_state()
  • file deletion: drawing.lua (----------)
    [27.2][27.1542:1577](),[27.1577][27.98:98]()
    -- primitives for editing drawings
    Drawing = {}
    require 'drawing_tests'
    else
    end
    return
    end
    end
    if line.show_help then
    return
    end
    for _,shape in ipairs(line.shapes) do
    assert(shape)
    else
    end
    end
    if p.deleted == nil then
    else
    end
    if p.name then
    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
    local name_text
    if p.name == '' then
    else
    end
    end
    love.graphics.rectangle('fill', x,y, App.width(name_text), State.line_height)
    name_text = App.newText(love.graphics.getFont(), p.name)
    name_text = State.em
    -- TODO: avoid computing name width on every repaint
    App.color(Current_name_background_color)
    -- TODO: clip
    local x,y = px(p.x)+5, py(p.y)+5
    end
    end
    end
    end
    end
    if shape.mode == 'freehand' then
    local prev = nil
    for _,point in ipairs(shape.points) do
    if prev then
    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]
    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
    end
    prev = curr
    end
    -- close the loop
    local curr = drawing.points[shape.vertices[1]]
    elseif shape.mode == 'circle' then
    local center = drawing.points[shape.center]
    elseif shape.mode == 'arc' then
    local center = drawing.points[shape.center]
    elseif shape.mode == 'deleted' then
    -- ignore
    else
    print(shape.mode)
    assert(false)
    end
    end
    local shape = drawing.pending
    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]
    elseif shape.mode == 'manhattan' then
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    if math.abs(mx-p1.x) > math.abs(my-p1.y) then
    else
    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
    end
    prev = curr
    end
    elseif shape.mode == 'rectangle' then
    local first = drawing.points[shape.vertices[1]]
    if #shape.vertices == 1 then
    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)
    elseif shape.mode == 'square' then
    local first = drawing.points[shape.vertices[1]]
    if #shape.vertices == 1 then
    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)
    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
    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)
    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
    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
    else
    assert(false)
    end
    end
    -- a couple of operations on drawings need to constantly check the state of the mouse
    assert(drawing.mode == 'drawing')
    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
    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
    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
    end
    elseif State.lines.current_drawing then
    local drawing = State.lines.current_drawing
    local line_cache = State.line_cache[State.lines.current_drawing_index]
    -- the last point added during update is good enough
    end
    if math.abs(mx-p1.x) > math.abs(my-p1.y) then
    else
    end
    end
    end
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)
    end
    else
    -- too few points; draw nothing
    end
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my)
    end
    end
    elseif drawing.pending.mode == 'circle' then
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local center = drawing.points[drawing.pending.center]
    table.insert(drawing.shapes, drawing.pending)
    drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    table.insert(drawing.shapes, drawing.pending)
    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))
    elseif drawing.pending.mode == 'square' then
    assert(#drawing.pending.vertices <= 2)
    if #drawing.pending.vertices == 2 then
    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 mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    table.insert(drawing.shapes, drawing.pending)
    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))
    end
    end
    elseif drawing.pending.mode == 'name' then
    -- drop it
    else
    assert(false)
    print(drawing.pending.mode)
    end
    end
    end
    end
    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
    if drawing.pending.mode == 'freehand' then
    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
    elseif App.mouse_down(1) and chord == 'm' then
    if drawing.pending.mode == 'freehand' then
    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
    elseif chord == 'C-g' and not App.mouse_down(1) then
    elseif App.mouse_down(1) and chord == 'g' then
    if drawing.pending.mode == 'freehand' then
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    if drawing.pending.vertices == nil then
    drawing.pending.vertices = {drawing.pending.p1}
    end
    -- 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'
    while #drawing.pending.vertices >= 2 do
    table.remove(drawing.pending.vertices)
    end
    table.insert(drawing.pending.vertices, j)
    drawing.pending.mode = 'arc'
    local center = drawing.points[drawing.pending.center]
    drawing.pending.start_angle = geom.angle(center.x,center.y, mx,my)
    if drawing.pending.mode == 'freehand' then
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    drawing.pending.center = drawing.pending.p1
    drawing.pending.center = drawing.pending.vertices[1]
    end
    drawing.pending.mode = 'circle'
    if drawing then
    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
    p.name = ''
    end
    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
    if drawing then
    shape.mode = 'deleted'
    end
    if drawing then
    drawing.show_help = true
    end
    end
    end
    if drawing.mode == 'drawing' then
    end
    end
    end
    return nil
    end
    if drawing.mode == 'drawing' then
    for i,shape in ipairs(drawing.shapes) do
    assert(shape)
    if geom.on_shape(mx,my, drawing, shape) then
    end
    end
    end
    end
    end
    end
    if drawing.mode == 'drawing' then
    for i,point in ipairs(drawing.points) do
    assert(point)
    end
    end
    end
    end
    end
    end
    if drawing.mode == 'drawing' 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
    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.insert_point(points, x,y)
    for i,point in ipairs(points) do
    return i
    end
    end
    table.insert(points, {x=x, y=y})
    return #points
    end
    end
    end
    end
    function table.find(h, x)
    for k,v in pairs(h) do
    if v == x then
    return k
    end
    end
    function Drawing.coord(n, width) -- pixels to parts
    return math.floor(n*256/width)
    function Drawing.pixels(n, width) -- parts to pixels
    return math.floor(n*width/256)
    return (cx-px)*(cx-px) + (cy-py)*(cy-py) < Same_point_distance*Same_point_distance
    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)
    if Drawing.near(point, x,y, width) then
    table.insert(points, {x=x, y=y})
    return #points
    end
    -- check if UI would snap the two points together
    function Drawing.find_or_insert_point(points, x,y, width)
    function round(num)
    return math.floor(num+.5)
    end
    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]
    end
    b.x = round((a.x + b.x + c.x)/3)
    b.y = round((a.y + b.y + c.y)/3)
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' 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
    function Drawing.select_drawing_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    if Drawing.near(point, mx,my, State.width) then
    return drawing_index,drawing,line_cache,i,point
    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)
    function Drawing.select_point_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    return drawing,line_cache,i,shape
    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)
    function Drawing.select_shape_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    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
    local x, y = App.mouse_x(), App.mouse_y()
    for drawing_index,drawing in ipairs(State.lines) do
    function Drawing.current_drawing(State)
    local fourthx = firstx+deltay
    local fourthy = firsty-deltax
    return thirdx,thirdy, fourthx,fourthy
    end
    end
    function Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y)
    if firstx == secondx then
    end
    if firsty == secondy then
    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
    -- 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
    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
    local fourthx = round(((firstx-a*firsty) - a*c) / (a*a + 1))
    local fourthy = round((a*(-firstx + a*firsty) - c) / (a*a + 1))
    local thirdx = round(((x-a*y) - a*c) / (a*a + 1))
    local thirdy = round((a*(-x + a*y) - c) / (a*a + 1))
    return secondx,y, firstx,y
    return x,secondy, x,firsty
    elseif chord == 'escape' and App.mouse_down(1) then
    drawing.pending = {}
    local _,drawing = Drawing.current_drawing(State)
    elseif chord == 'C-h' and not App.mouse_down(1) then
    local drawing = Drawing.select_drawing_at_mouse(State)
    local drawing,_,_,shape = Drawing.select_shape_at_mouse(State)
    elseif chord == 'C-d' and not App.mouse_down(1) then
    local _,drawing,_,i,p = Drawing.select_point_at_mouse(State)
    drawing.pending = {mode=State.current_drawing_mode, target_point=point_index}
    State.lines.current_drawing_index = drawing_index
    State.lines.current_drawing = drawing
    -- don't clobber
    end
    State.current_drawing_mode = 'name'
    State.previous_drawing_mode = State.current_drawing_mode
    if State.previous_drawing_mode == nil then
    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
    if State.previous_drawing_mode == nil then
    State.previous_drawing_mode = State.current_drawing_mode
    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)
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    drawing.pending.center = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    elseif App.mouse_down(1) and chord == 'o' then
    State.current_drawing_mode = 'circle'
    local _,drawing = Drawing.current_drawing(State)
    drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))
    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)
    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)
    elseif App.mouse_down(1) and chord == 'r' then
    if drawing.pending.mode == 'freehand' then
    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 == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.vertices = {drawing.pending.center}
    end
    drawing.pending.mode = 'rectangle'
    elseif App.mouse_down(1) and chord == 's' then
    if drawing.pending.mode == 'freehand' then
    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'
    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 j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    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)
    elseif App.mouse_down(1) and chord == 'p' and State.current_drawing_mode == 'polygon' then
    local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    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)
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    State.current_drawing_mode = 'square'
    local _,drawing = Drawing.current_drawing(State)
    elseif chord == 'C-s' and not App.mouse_down(1) then
    State.current_drawing_mode = 'square'
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'square' then
    -- reuse existing (1-2) vertices
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    State.current_drawing_mode = 'rectangle'
    local _,drawing = Drawing.current_drawing(State)
    elseif chord == 'C-r' and not App.mouse_down(1) then
    State.current_drawing_mode = 'rectangle'
    elseif drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    State.current_drawing_mode = 'polygon'
    local _,drawing = Drawing.current_drawing(State)
    State.current_drawing_mode = 'polygon'
    State.current_drawing_mode = 'manhattan'
    drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    State.current_drawing_mode = 'manhattan'
    local drawing = Drawing.select_drawing_at_mouse(State)
    State.current_drawing_mode = 'line'
    drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    State.current_drawing_mode = 'line'
    local _,drawing = Drawing.current_drawing(State)
    function Drawing.keychord_pressed(State, chord)
    State.lines.current_drawing.pending = {}
    State.lines.current_drawing = nil
    elseif drawing.pending.mode == 'arc' then
    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)
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    elseif drawing.pending.mode == 'rectangle' then
    assert(#drawing.pending.vertices <= 2)
    if #drawing.pending.vertices == 2 then
    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 mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    elseif drawing.pending.mode == 'polygon' then
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    table.insert(drawing.shapes, drawing.pending)
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, mx,my, State.width))
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    local p2 = drawing.points[drawing.pending.p2]
    table.insert(drawing.shapes, drawing.pending)
    App.mouse_move(State.left+Drawing.pixels(p2.x, State.width), line_cache.starty+Drawing.pixels(p2.y, State.width))
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, State.width)
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx, p1.y, State.width)
    elseif drawing.pending.mode == 'manhattan' then
    local p1 = drawing.points[drawing.pending.p1]
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    Drawing.smoothen(drawing.pending)
    table.insert(drawing.shapes, drawing.pending)
    elseif drawing.pending.mode == 'line' then
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    table.insert(drawing.shapes, drawing.pending)
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if drawing.pending then
    if drawing.pending.mode == nil then
    -- nothing pending
    elseif drawing.pending.mode == 'freehand' then
    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
    elseif State.current_drawing_mode == 'move' then
    if Drawing.in_drawing(drawing, line_cache, pmx, pmy, State.left,State.right) then
    table.insert(drawing.pending.points, {x=mx, y=my})
    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)
    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]
    print(State.current_drawing_mode)
    local cx = Drawing.coord(x-State.left, State.width)
    if State.current_drawing_mode == 'freehand' then
    elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then
    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
    drawing.pending = {mode=State.current_drawing_mode, vertices={j}}
    elseif State.current_drawing_mode == 'circle' then
    drawing.pending = {mode=State.current_drawing_mode, center=j}
    elseif State.current_drawing_mode == 'move' then
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    drawing.pending = {mode=State.current_drawing_mode, points={{x=cx, y=cy}}}
    local cy = Drawing.coord(y-line_cache.starty, State.width)
    -- all the action is in mouse_released
    -- nothing
    elseif State.current_drawing_mode == 'name' then
    function Drawing.mouse_pressed(State, drawing_index, x,y, button)
    local drawing = State.lines[drawing_index]
    local line_cache = State.line_cache[drawing_index]
    function Drawing.in_drawing(drawing, line_cache, x,y, left,right)
    if line_cache.starty == nil then return false end -- outside current page
    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()))
    local cx,cy = px(center.x), py(center.y)
    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))
    love.graphics.line(px(first.x),py(first.y), pmx,pmy)
    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))
    love.graphics.line(px(first.x),py(first.y), pmx,pmy)
    love.graphics.line(px(prev.x),py(prev.y), pmx,pmy)
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    love.graphics.line(px(p1.x),py(p1.y), px(p1.x),pmy)
    love.graphics.line(px(p1.x),py(p1.y), pmx, py(p1.y))
    local p1 = drawing.points[shape.p1]
    love.graphics.line(px(p1.x),py(p1.y), pmx,pmy)
    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)
    function Drawing.draw_pending_shape(drawing, top, left,right)
    local width = right-left
    local pmx,pmy = App.mouse_x(), App.mouse_y()
    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 function px(x) return Drawing.pixels(x, width)+left end
    local function py(y) return Drawing.pixels(y, width)+top end
    love.graphics.arc('line', 'open', px(center.x),py(center.y), Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)
    love.graphics.circle('line', px(center.x),py(center.y), Drawing.pixels(shape.radius, width))
    -- TODO: clip
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    love.graphics.line(px(p1.x),py(p1.y), px(p2.x),py(p2.y))
    love.graphics.line(px(prev.x),py(prev.y), px(point.x),py(point.y))
    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
    App.color(Current_stroke_color)
    Drawing.draw_pending_shape(line, line_cache.starty, State.left,State.right)
    App.color(Stroke_color)
    love.graphics.circle('fill', px(p.x),py(p.y), 2)
    App.color(Focus_stroke_color)
    love.graphics.circle('line', px(p.x),py(p.y), Same_point_distance)
    if Drawing.near(p, mx,my, State.width) then
    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
    Drawing.draw_shape(line, shape, line_cache.starty, State.left,State.right)
    App.color(Stroke_color)
    if geom.on_shape(mx,my, line, shape) then
    App.color(Focus_stroke_color)
    local mx = Drawing.coord(pmx-State.left, State.width)
    local my = Drawing.coord(pmy-line_cache.starty, State.width)
    draw_help_without_mouse_pressed(State, line_index)
    if App.mouse_down(1) and love.keyboard.isDown('h') then
    draw_help_with_mouse_pressed(State, line_index)
    icon[State.previous_drawing_mode](State.right-22, line_cache.starty+4)
    -- All drawings span 100% of some conceptual 'page width' and divide it up
    -- into 256 parts.
    local pmx,pmy = App.mouse_x(), App.mouse_y()
    App.color(Icon_color)
    if icon[State.current_drawing_mode] then
    icon[State.current_drawing_mode](State.right-22, line_cache.starty+4)
    love.graphics.rectangle('line', State.left,line_cache.starty, State.width,Drawing.pixels(line.h, State.width))
    if pmx < State.right and pmy > line_cache.starty and pmy < line_cache.starty+Drawing.pixels(line.h, State.width) then
    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
  • file deletion: icons.lua (----------)
    [27.2][27.6587:6620](),[27.6620][27.5323:5323]()
    icon = {}
    function icon.insert_drawing(x, y)
    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.circle(x, y)
    love.graphics.circle('line', x+10,y+10, 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
    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)
    App.color(Icon_color)
  • file deletion: help.lua (----------)
    [27.2][27.11545:11577](),[27.11577][27.6622:6622]()
    love.graphics.print("Things you can do:", State.left+30,y)
    y = y + State.line_height
    y = y + State.line_height
    y = y + State.line_height
    y = y + State.line_height
    y = y + State.line_height
    y = y + State.line_height
    if State.current_drawing_mode ~= 'freehand' then
    y = y + State.line_height
    end
    end
    end
    end
    end
    end
    end
    end
    love.graphics.print("You're currently drawing a "..current_shape(State, drawing.pending), State.left+30,y)
    y = y + State.line_height
    y = y + State.line_height
    if State.current_drawing_mode == 'freehand' then
    y = y + State.line_height
    elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then
    y = y + State.line_height
    elseif State.current_drawing_mode == 'circle' then
    if drawing.pending.mode == 'circle' then
    else
    end
    end
    end
    end
    end
    end
    end
    end
    end
    return 'freehand stroke'
    return 'straight line'
    return 'horizontal/vertical line'
    return 'arc'
    else
    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
    return State.current_drawing_mode
    elseif State.current_drawing_mode == 'circle' and shape and shape.start_angle then
    elseif State.current_drawing_mode == 'manhattan' then
    elseif State.current_drawing_mode == 'line' then
    function current_shape(State, shape)
    if State.current_drawing_mode == 'freehand' then
    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))
    if State.current_drawing_mode ~= 'square' then
    y = y + State.line_height
    love.graphics.print("* Press 's' to switch to drawing squares", State.left+30,y)
    if State.current_drawing_mode ~= 'rectangle' then
    y = y + State.line_height
    love.graphics.print("* Press 'r' to switch to drawing rectangles", State.left+30,y)
    if State.current_drawing_mode ~= 'polygon' then
    y = y + State.line_height
    love.graphics.print("* Press 'g' to switch to drawing polygons", State.left+30,y)
    if State.current_drawing_mode ~= 'circle' then
    y = y + State.line_height
    love.graphics.print("* Press 'o' to switch to drawing circles/arcs", State.left+30,y)
    if State.current_drawing_mode ~= 'manhattan' then
    y = y + State.line_height
    love.graphics.print("* Press 'm' to switch to drawing horizontal/vertical lines", State.left+30,y)
    y = y + State.line_height
    y = y + State.line_height
    if State.current_drawing_mode ~= 'line' then
    y = y + State.line_height
    love.graphics.print("* Press 'l' to switch to drawing lines", State.left+30,y)
    love.graphics.print("* Press 'esc' then release the mouse button to cancel the current shape", State.left+30,y)
    if #drawing.pending.vertices < 2 then
    y = y + State.line_height
    else
    y = y + State.line_height
    y = y + State.line_height
    love.graphics.print("* Press 'p' to replace the second vertex of the rectangle", State.left+30,y)
    end
    if #drawing.pending.vertices < 2 then
    y = y + State.line_height
    else
    y = y + State.line_height
    y = y + State.line_height
    love.graphics.print("* Press 'p' to replace the second vertex of the square", State.left+30,y)
    end
    love.graphics.print('* Release the mouse button to finish drawing the square', State.left+30,y)
    love.graphics.print("* Press 'p' to add a vertex to the square", State.left+30,y)
    elseif State.current_drawing_mode == 'square' then
    love.graphics.print('* Release the mouse button to finish drawing the rectangle', State.left+30,y)
    love.graphics.print("* Press 'p' to add a vertex to the rectangle", State.left+30,y)
    y = y + State.line_height
    elseif State.current_drawing_mode == 'polygon' then
    y = y + State.line_height
    y = y + State.line_height
    elseif State.current_drawing_mode == 'rectangle' then
    love.graphics.print("* Press 'p' to add a vertex to the polygon", State.left+30,y)
    love.graphics.print('* Release the mouse button to finish drawing the polygon', State.left+30,y)
    love.graphics.print('* Release the mouse button to finish drawing the arc', 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)
    love.graphics.print('* Release the mouse button to finish drawing the circle', State.left+30,y)
    love.graphics.print('* Release the mouse button to finish drawing the line', State.left+30,y)
    love.graphics.print('* Release the mouse button to finish drawing the stroke', State.left+30,y)
    love.graphics.print('Things you can do now:', State.left+30,y)
    App.color(Help_color)
    local y = line_cache.starty+10
    function draw_help_with_mouse_pressed(State, drawing_index)
    local drawing = State.lines[drawing_index]
    local line_cache = State.line_cache[drawing_index]
    y = y + State.line_height
    y = y + State.line_height
    love.graphics.print("Press 'esc' now to hide this message", State.left+30,y)
    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))
    love.graphics.print("* Press 'ctrl+=' or 'ctrl+-' to zoom in or out, ctrl+0 to reset zoom", State.left+30,y)
    if State.current_drawing_mode ~= 'square' then
    y = y + State.line_height
    love.graphics.print("* Press 'ctrl+s' to switch to drawing squares", State.left+30,y)
    if State.current_drawing_mode ~= 'rectangle' then
    y = y + State.line_height
    love.graphics.print("* Press 'ctrl+r' to switch to drawing rectangles", State.left+30,y)
    if State.current_drawing_mode ~= 'polygon' then
    y = y + State.line_height
    love.graphics.print("* Press 'ctrl+g' to switch to drawing polygons", State.left+30,y)
    if State.current_drawing_mode ~= 'circle' then
    y = y + State.line_height
    love.graphics.print("* Press 'ctrl+o' to switch to drawing circles/arcs", State.left+30,y)
    if State.current_drawing_mode ~= 'manhattan' then
    y = y + State.line_height
    love.graphics.print("* Press 'ctrl+m' to switch to drawing horizontal/vertical lines", State.left+30,y)
    if State.current_drawing_mode ~= 'line' then
    y = y + State.line_height
    love.graphics.print("* Press 'ctrl+l' to switch to drawing lines", State.left+30,y)
    love.graphics.print("* Press 'ctrl+p' to switch to drawing freehand strokes", State.left+30,y)
    love.graphics.print("* Hover on a point or shape and press 'ctrl+d' to delete it", State.left+30,y)
    love.graphics.print("* Hover on a point and press 'ctrl+n', type a name, then press 'enter'", State.left+30,y)
    love.graphics.print("then press the mouse button to drop it", State.left+30+bullet_indent(),y)
    love.graphics.print("* Hover on a point and press 'ctrl+u' to pick it up and start moving it,", State.left+30,y)
    love.graphics.print("* Press the mouse button to start drawing a "..current_shape(State), State.left+30,y)
    App.color(Help_color)
    local y = line_cache.starty+10
    function draw_help_without_mouse_pressed(State, drawing_index)
    local drawing = State.lines[drawing_index]
    local line_cache = State.line_cache[drawing_index]
  • file deletion: geom.lua (----------)
    [27.2][27.15049:15081](),[27.15081][27.11579:11579]()
    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
    return geom.on_polygon(x,y, drawing, shape)
    elseif shape.mode == 'circle' then
    local center = drawing.points[shape.center]
    elseif shape.mode == 'arc' then
    local center = drawing.points[shape.center]
    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
    return false
    end
    local y1,y2 = p1.y,p2.y
    if y1 > y2 then
    y1,y2 = y2,y1
    end
    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)
    return false
    end
    -- between endpoints
    local k = (x-p1.x) / (p2.x-p1.x)
    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
    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)
    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
    local angle = geom.angle(ox,oy, x,y)
    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
    return k > -0.005 and k < 1.005
    if yp < y-2 or yp > y+2 then
    return y >= y1-2 and y <= y2+2
    if math.abs(p1.x-x) > 2 then
    local dist = geom.dist(center.x,center.y, x,y)
    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 == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' 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
    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
    end
    return x >= x1-2 and x <= x2+2
    return y >= y1-2 and y <= y2+2
    geom = {}
  • replacement in undo.lua at line 63
    [27.295][27.1592:1978](),[27.1017][27.1592:1978](),[27.1102][27.1592:1978](),[27.1592][27.1592:1978](),[27.1978][16.1:239](),[16.239][27.2236:2296](),[27.2236][27.2236:2296]()
    if line.mode == 'text' then
    table.insert(event.lines, {mode='text', data=line.data})
    elseif line.mode == 'drawing' then
    local points=deepcopy(line.points)
    --? print('copying', line.points, 'with', #line.points, 'points into', points)
    local shapes=deepcopy(line.shapes)
    --? print('copying', line.shapes, 'with', #line.shapes, 'shapes into', shapes)
    table.insert(event.lines, {mode='drawing', h=line.h, points=points, shapes=shapes, pending={}})
    --? table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})
    else
    print(line.mode)
    assert(false)
    end
    [27.1017]
    [27.2296]
    table.insert(event.lines, State.lines[i])
  • edit in text_tests.lua at line 15
    [27.1623][27.474:604](),[27.474][27.474:604](),[27.604][27.49:95](),[27.95][27.1624:1660](),[27.211][27.1624:1660](),[27.604][27.1624:1660](),[27.1660][27.348:380](),[27.380][27.29:55](),[27.1660][27.29:55](),[27.55][27.212:280](),[27.77][27.687:738](),[27.280][27.687:738](),[27.1807][27.687:738](),[27.687][27.687:738](),[27.738][27.1808:1970](),[27.1970][27.874:1115](),[27.874][27.874:1115](),[27.1115][27.96:142](),[27.142][27.1971:2028](),[27.385][27.1971:2028](),[27.1115][27.1971:2028](),[27.2028][27.381:413](),[27.145][27.1159:1244](),[27.413][27.1159:1244](),[27.2114][27.1159:1244](),[27.1159][27.1159:1244](),[27.1244][27.2115:2147](),[27.2147][27.1263:1300](),[27.1263][27.1263:1300](),[27.1300][27.78:131](),[27.131][27.2148:2318](),[27.1338][27.2148:2318]()
    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')
  • replacement in text_tests.lua at line 604
    [27.13730][27.13730:13840]()
    check_eq(Editor_state.lines[1].data, 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data')
    [27.13730]
    [27.2]
    check_eq(Editor_state.lines[1], 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data')
  • replacement in text_tests.lua at line 622
    [27.452][27.14134:14215]()
    check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_edit_deletes_selection')
    [27.452]
    [27.2]
    check_eq(Editor_state.lines[1], 'xbc', 'F - test_edit_deletes_selection')
  • replacement in text_tests.lua at line 645
    [27.14601][27.14601:14702]()
    check_eq(Editor_state.lines[1].data, 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data')
    [27.14601]
    [27.2]
    check_eq(Editor_state.lines[1], 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data')
  • replacement in text_tests.lua at line 683
    [27.451][27.15370:15436]()
    check_eq(Editor_state.lines[1].data, 'bc', 'F - test_cut/data')
    [27.451]
    [27.504]
    check_eq(Editor_state.lines[1], 'bc', 'F - test_cut/data')
  • replacement in text_tests.lua at line 704
    [27.1068][27.15737:15823]()
    check_eq(Editor_state.lines[1].data, 'xyzdef', 'F - test_paste_replaces_selection')
    [27.1068]
    [27.520]
    check_eq(Editor_state.lines[1], 'xyzdef', 'F - test_paste_replaces_selection')
  • replacement in text_tests.lua at line 730
    [27.16335][27.16335:16428]()
    check_eq(Editor_state.lines[1].data, 'ahi', 'F - test_deleting_selection_may_scroll/data')
    [27.16335]
    [27.1008]
    check_eq(Editor_state.lines[1], 'ahi', 'F - test_deleting_selection_may_scroll/data')
  • replacement in text_tests.lua at line 796
    [27.18093][27.18093:18284]()
    check_eq(Editor_state.lines[1].data, '', 'F - test_insert_newline_at_start_of_line/data:1')
    check_eq(Editor_state.lines[2].data, 'abc', 'F - test_insert_newline_at_start_of_line/data:2')
    [27.18093]
    [27.4366]
    check_eq(Editor_state.lines[1], '', 'F - test_insert_newline_at_start_of_line/data:1')
    check_eq(Editor_state.lines[2], 'abc', 'F - test_insert_newline_at_start_of_line/data:2')
  • edit in text_tests.lua at line 967
    [27.7115][27.7115:7255](),[27.7255][27.2:29](),[27.29][27.7521:7589](),[27.7589][27.2352:2398](),[27.2398][27.24425:24493](),[27.7693][27.24425:24493](),[27.24425][27.24425:24493](),[27.24493][27.2315:2551](),[27.2551][27.24580:24802](),[27.24580][27.24580:24802](),[27.24802][27.2:80](),[27.80][27.7779:7946](),[27.85][27.7779:7946](),[27.170][27.7779:7946](),[27.24893][27.7779:7946](),[27.7779][27.7779:7946](),[27.7946][27.1536:1562](),[27.1562][27.7694:7723](),[27.7723][27.7982:8222](),[27.24930][27.7982:8222](),[27.7982][27.7982:8222](),[27.8222][27.3759:3811](),[27.3811][27.24931:25107](),[27.8259][27.24931:25107](),[27.25107][27.7724:7764](),[27.7764][27.8443:8517](),[27.25154][27.8443:8517](),[27.8443][27.8443:8517]()
    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')
  • replacement in text_tests.lua at line 1761
    [27.5518][27.47162:47252](),[27.2575][27.47162:47252]()
    check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary")
    [27.5518]
    [27.35921]
    check_eq(Editor_state.lines[1], 'abcdef', "F - test_backspace_past_line_boundary")
  • replacement in text_tests.lua at line 1778
    [27.5572][27.47558:47645](),[27.36543][27.47558:47645]()
    check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection/data")
    [27.5572]
    [27.36617]
    check_eq(Editor_state.lines[1], 'bc', "F - test_backspace_over_selection/data")
  • replacement in text_tests.lua at line 1797
    [27.5626][27.48220:48315](),[27.37369][27.48220:48315]()
    check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection_reverse/data")
    [27.5626]
    [27.37451]
    check_eq(Editor_state.lines[1], 'bc', "F - test_backspace_over_selection_reverse/data")
  • replacement in text_tests.lua at line 1816
    [27.5680][27.48914:49104](),[27.38262][27.48914:49104]()
    check_eq(Editor_state.lines[1].data, 'akl', "F - test_backspace_over_multiple_lines/data:1")
    check_eq(Editor_state.lines[2].data, 'mno', "F - test_backspace_over_multiple_lines/data:2")
    [27.5680]
    [27.38426]
    check_eq(Editor_state.lines[1], 'akl', "F - test_backspace_over_multiple_lines/data:1")
    check_eq(Editor_state.lines[2], 'mno', "F - test_backspace_over_multiple_lines/data:2")
  • replacement in text_tests.lua at line 1836
    [27.5734][27.49694:49876](),[27.39172][27.49694:49876]()
    check_eq(Editor_state.lines[1].data, 'a', "F - test_backspace_to_start_of_line/data:1")
    check_eq(Editor_state.lines[2].data, 'def', "F - test_backspace_to_start_of_line/data:2")
    [27.5734]
    [27.39328]
    check_eq(Editor_state.lines[1], 'a', "F - test_backspace_to_start_of_line/data:1")
    check_eq(Editor_state.lines[2], 'def', "F - test_backspace_to_start_of_line/data:2")
  • replacement in text_tests.lua at line 1856
    [27.5788][27.50457:50639](),[27.40076][27.50457:50639]()
    check_eq(Editor_state.lines[1].data, 'abc', "F - test_backspace_to_start_of_line/data:1")
    check_eq(Editor_state.lines[2].data, 'f', "F - test_backspace_to_start_of_line/data:2")
    [27.5788]
    [27.40232]
    check_eq(Editor_state.lines[1], 'abc', "F - test_backspace_to_start_of_line/data:1")
    check_eq(Editor_state.lines[2], 'f', "F - test_backspace_to_start_of_line/data:2")
  • replacement in text_tests.lua at line 1953
    [27.6032][27.53741:53832](),[27.412][27.53741:53832]()
    check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_undo_restores_selection/baseline')
    [27.6032]
    [27.53832]
    check_eq(Editor_state.lines[1], 'xbc', 'F - test_undo_restores_selection/baseline')
  • replacement in text_tests.lua at line 1967
    [27.140][27.2:76]()
    Editor_state.lines = load_array{'```lines', '```', 'def', 'ghi', 'deg'}
    [27.140]
    [27.202]
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'deg'}
  • replacement in text.lua at line 53
    [27.458][27.458:608]()
    if State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then
    [27.458]
    [27.1429]
    if State.lines[State.cursor1.line]:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then
  • replacement in text.lua at line 95
    [27.124][27.124:168]()
    for frag in line.data:gmatch('%S*%s*') do
    [27.124]
    [27.682]
    for frag in line:gmatch('%S*%s*') do
  • replacement in text.lua at line 145
    [27.79][27.79:344]()
    local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
    [27.79]
    [27.3]
    local byte_offset = Text.offset(State.lines[State.cursor1.line], State.cursor1.pos)
    State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line], byte_offset)
  • replacement in text.lua at line 185
    [27.1511][27.1388:1576](),[27.1388][27.1388:1576]()
    local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1)
    local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
    [27.1511]
    [27.2256]
    local byte_start = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos-1)
    local byte_end = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos)
  • replacement in text.lua at line 189
    [27.2306][27.1577:1752]()
    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)
    [27.2306]
    [27.2442]
    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)
  • replacement in text.lua at line 191
    [27.2455][27.1753:1868]()
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
    [27.2455]
    [27.2544]
    State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1)
  • replacement in text.lua at line 197
    [27.1585][27.2023:2145](),[27.2023][27.2023:2145](),[27.2145][27.105:166](),[27.166][27.2728:2761](),[27.2145][27.2728:2761](),[27.2527][27.2728:2761](),[27.4205][27.2728:2761](),[27.59583][27.2728:2761](),[27.2728][27.2728:2761](),[27.2761][27.2146:2405](),[27.2405][27.167:226](),[27.226][27.2948:2958](),[27.2405][27.2948:2958](),[27.2767][27.2948:2958](),[27.4270][27.2948:2958](),[27.59920][27.2948:2958](),[27.2948][27.2948:2958]()
    if State.lines[State.cursor1.line-1].mode == 'drawing' then
    table.remove(State.lines, State.cursor1.line-1)
    table.remove(State.line_cache, State.cursor1.line-1)
    else
    -- join lines
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1
    State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
    table.remove(State.lines, State.cursor1.line)
    table.remove(State.line_cache, State.cursor1.line)
    end
    [27.1585]
    [27.2406]
    -- join lines
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1])+1
    State.lines[State.cursor1.line-1] = State.lines[State.cursor1.line-1]..State.lines[State.cursor1.line]
    table.remove(State.lines, State.cursor1.line)
    table.remove(State.line_cache, State.cursor1.line)
  • replacement in text.lua at line 225
    [27.1869][27.3061:3141]()
    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
    [27.1869]
    [27.1676]
    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) then
  • replacement in text.lua at line 230
    [27.8554][27.3253:3521]()
    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
    local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
    local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1)
    [27.8554]
    [27.3265]
    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) then
    local byte_start = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos)
    local byte_end = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos+1)
  • replacement in text.lua at line 235
    [27.3315][27.3522:3697]()
    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)
    [27.3315]
    [27.3451]
    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)
  • replacement in text.lua at line 237
    [27.3464][27.3698:3813]()
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
    [27.3464]
    [27.3553]
    State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1)
  • replacement in text.lua at line 242
    [27.3907][27.4271:4334](),[27.4334][27.3754:3776](),[27.3754][27.3754:3776](),[27.3776][27.4030:4154](),[27.3646][27.3904:3914](),[27.4210][27.3904:3914](),[27.62131][27.3904:3914](),[27.3904][27.3904:3914]()
    if State.lines[State.cursor1.line+1].mode == 'text' then
    -- join lines
    State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
    end
    [27.3907]
    [27.4335]
    -- join lines
    State.lines[State.cursor1.line] = State.lines[State.cursor1.line]..State.lines[State.cursor1.line+1]
  • replacement in text.lua at line 336
    [27.2057][27.2057:2281]()
    local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
    table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)})
    [27.2057]
    [27.287]
    local byte_offset = Text.offset(State.lines[State.cursor1.line], State.cursor1.pos)
    table.insert(State.lines, State.cursor1.line+1, string.sub(State.lines[State.cursor1.line], byte_offset))
  • replacement in text.lua at line 339
    [27.346][27.2281:2389](),[27.4518][27.2281:2389](),[27.2281][27.2281:2389]()
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
    [27.346]
    [27.184]
    State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_offset-1)
  • replacement in text.lua at line 354
    [27.2828][27.2828:2993](),[27.2993][27.82:188](),[27.115][27.555:563](),[27.168][27.555:563](),[27.188][27.555:563](),[27.3092][27.555:563](),[27.66841][27.555:563](),[27.555][27.555:563]()
    if State.lines[State.screen_top1.line].mode == 'text' then
    y = y - State.line_height
    elseif State.lines[State.screen_top1.line].mode == 'drawing' then
    y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width)
    end
    [27.2828]
    [27.486]
    y = y - State.line_height
  • edit in text.lua at line 391
    [27.816][27.3868:3925](),[27.3868][27.3868:3925]()
    assert(State.lines[State.cursor1.line].mode == 'text')
  • replacement in text.lua at line 396
    [27.443][27.4030:4077](),[27.4077][27.484:559](),[27.67955][27.484:559](),[27.484][27.484:559](),[27.559][27.4078:4136](),[27.4136][27.611:657](),[27.68021][27.611:657](),[27.611][27.611:657](),[27.657][27.4137:4182](),[27.4182][27.208:282](),[27.251][27.826:937](),[27.282][27.826:937](),[27.365][27.826:937](),[27.1023][27.826:937](),[27.4275][27.826:937](),[27.68181][27.826:937](),[27.779][27.826:937](),[27.937][27.347:450](),[27.450][27.1023:1264](),[27.4374][27.1023:1264](),[27.4627][27.1023:1264](),[27.68294][27.1023:1264](),[27.1023][27.1023:1264](),[27.1264][27.4375:4655](),[27.4655][27.1508:1520](),[27.68617][27.1508:1520](),[27.1508][27.1508:1520](),[27.1520][27.4656:4882](),[27.4882][27.1024:1138](),[27.99][27.1691:1705](),[27.1138][27.1691:1705](),[27.4990][27.1691:1705](),[27.68994][27.1691:1705](),[27.1691][27.1691:1705]()
    local new_cursor_line = State.cursor1.line
    while new_cursor_line > 1 do
    new_cursor_line = new_cursor_line-1
    if State.lines[new_cursor_line].mode == 'text' then
    --? print('found previous text line')
    State.cursor1.line = new_cursor_line
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    -- previous text line found, pick its final screen line
    --? print('has multiple screen lines')
    local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos
    --? print(#screen_line_starting_pos)
    screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]
    --? print('previous screen line starts at pos '..tostring(screen_line_starting_pos)..' of its line')
    if State.screen_top1.line > State.cursor1.line then
    State.screen_top1.line = State.cursor1.line
    State.screen_top1.pos = screen_line_starting_pos
    --? print('pos of top of screen is also '..tostring(State.screen_top1.pos)..' of the same line')
    end
    local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset)
    State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    break
    [27.443]
    [27.2443]
    if State.cursor1.line > 1 then
    local new_cursor_line = State.cursor1.line-1
    --? print('found previous text line')
    State.cursor1.line = new_cursor_line
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    -- previous text line found, pick its final screen line
    --? print('has multiple screen lines')
    local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos
    --? print(#screen_line_starting_pos)
    screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]
    --? print('previous screen line starts at pos '..tostring(screen_line_starting_pos)..' of its line')
    if State.screen_top1.line > State.cursor1.line then
    State.screen_top1.line = State.cursor1.line
    State.screen_top1.pos = screen_line_starting_pos
    --? print('pos of top of screen is also '..tostring(State.screen_top1.pos)..' of the same line')
  • edit in text.lua at line 412
    [27.2453]
    [27.1706]
    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
  • replacement in text.lua at line 429
    [27.2441][27.5473:5703]()
    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
    [27.2441]
    [27.1139]
    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line], new_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line], new_screen_line_starting_byte_offset)
  • edit in text.lua at line 437
    [27.1280][27.5916:5973](),[27.5916][27.5916:5973]()
    assert(State.lines[State.cursor1.line].mode == 'text')
  • replacement in text.lua at line 441
    [27.3017][27.6133:6224](),[27.6224][27.3096:3138](),[27.70385][27.3096:3138](),[27.3096][27.3096:3138](),[27.3138][27.6225:6328](),[27.6328][27.1332:1450](),[27.1450][27.6440:6477](),[27.6440][27.6440:6477](),[27.6477][27.3342:3356](),[27.70687][27.3342:3356](),[27.3342][27.3342:3356](),[27.3356][27.2689:2699](),[27.2689][27.2689:2699]()
    local new_cursor_line = State.cursor1.line
    while new_cursor_line < #State.lines do
    new_cursor_line = new_cursor_line+1
    if State.lines[new_cursor_line].mode == 'text' then
    State.cursor1.line = new_cursor_line
    State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line].data, State.cursor_x, State.left)
    --? print(State.cursor1.pos)
    break
    end
    [27.3017]
    [27.1313]
    if State.cursor1.line < #State.lines then
    local new_cursor_line = State.cursor1.line+1
    State.cursor1.line = new_cursor_line
    State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line], State.cursor_x, State.left)
    --? print(State.cursor1.pos)
  • replacement in text.lua at line 463
    [27.4338][27.6881:7111]()
    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
    [27.4338]
    [27.1606]
    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line], new_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line], new_screen_line_starting_byte_offset)
  • replacement in text.lua at line 484
    [27.1805][27.7778:7851](),[27.7778][27.7778:7851]()
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
    [27.1805]
    [27.1806]
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line]) + 1
  • replacement in text.lua at line 498
    [27.916][27.7991:8079]()
    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then
    [27.916]
    [27.986]
    if Text.match(State.lines[State.cursor1.line], State.cursor1.pos-1, '%S') then
  • replacement in text.lua at line 510
    [27.8150][27.8150:8238]()
    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
    [27.8150]
    [27.5221]
    if Text.match(State.lines[State.cursor1.line], State.cursor1.pos-1, '%s') then
  • replacement in text.lua at line 519
    [27.1178][27.8285:8364]()
    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
    [27.1178]
    [27.1239]
    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line]) then
  • replacement in text.lua at line 522
    [27.1259][27.8365:8451]()
    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then
    [27.1259]
    [27.1327]
    if Text.match(State.lines[State.cursor1.line], State.cursor1.pos, '%S') then
  • replacement in text.lua at line 529
    [27.148][27.8452:8531](),[27.35][27.8452:8531]()
    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
    [27.148]
    [27.1447]
    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line]) then
  • replacement in text.lua at line 532
    [27.1467][27.8532:8618]()
    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
    [27.1467]
    [27.5516]
    if Text.match(State.lines[State.cursor1.line], State.cursor1.pos, '%s') then
  • edit in text.lua at line 551
    [27.2106][27.188:245](),[27.188][27.188:245]()
    assert(State.lines[State.cursor1.line].mode == 'text')
  • replacement in text.lua at line 553
    [27.321][27.171:178](),[27.73224][27.171:178](),[27.171][27.171:178](),[27.178][27.322:369](),[27.369][27.219:294](),[27.73279][27.219:294](),[27.219][27.219:294](),[27.294][27.370:552](),[27.552][27.446:470](),[27.73497][27.446:470](),[27.446][27.446:470](),[27.560][27.560:568]()
    else
    local new_cursor_line = State.cursor1.line
    while new_cursor_line > 1 do
    new_cursor_line = new_cursor_line-1
    if State.lines[new_cursor_line].mode == 'text' then
    State.cursor1.line = new_cursor_line
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
    break
    end
    end
    [27.321]
    [27.6550]
    elseif State.cursor1.line > 1 then
    State.cursor1.line = State.cursor1.line-1
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line]) + 1
  • replacement in text.lua at line 572
    [27.823][27.823:958]()
    assert(State.lines[State.cursor1.line].mode == 'text')
    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
    [27.823]
    [27.958]
    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) then
  • replacement in text.lua at line 574
    [27.1002][27.738:745](),[27.73906][27.738:745](),[27.738][27.738:745](),[27.745][27.1003:1097](),[27.1097][27.827:869](),[27.74015][27.827:869](),[27.827][27.827:869](),[27.869][27.1098:1231](),[27.1231][27.984:1008](),[27.74170][27.984:1008](),[27.984][27.984:1008](),[27.1101][27.1101:1109]()
    else
    local new_cursor_line = State.cursor1.line
    while new_cursor_line <= #State.lines-1 do
    new_cursor_line = new_cursor_line+1
    if State.lines[new_cursor_line].mode == 'text' then
    State.cursor1.line = new_cursor_line
    State.cursor1.pos = 1
    break
    end
    end
    [27.1002]
    [27.6913]
    elseif State.cursor1.line <= #State.lines-1 then
    State.cursor1.line = State.cursor1.line+1
    State.cursor1.pos = 1
  • replacement in text.lua at line 599
    [27.2683][27.16712:16734](),[27.2263][27.16712:16734](),[27.16734][27.2292:2397](),[27.2292][27.2292:2397](),[27.2397][27.166:186](),[27.6763][27.166:186](),[27.74994][27.166:186](),[27.166][27.166:186](),[27.186][27.2398:2448](),[27.2448][27.189:289](),[27.222][27.2541:2589](),[27.289][27.2541:2589](),[27.2541][27.2541:2589](),[27.2589][27.220:287](),[27.6800][27.220:287](),[27.75228][27.220:287](),[27.220][27.220:287](),[27.287][27.2590:2737](),[27.2737][27.937:976](),[27.976][27.402:408](),[27.2737][27.402:408](),[27.5183][27.402:408](),[27.75411][27.402:408](),[27.402][27.402:408](),[27.408][27.2738:2861]()
    local y = State.top
    while State.cursor1.line <= #State.lines do
    if State.lines[State.cursor1.line].mode == 'text' then
    break
    end
    --? print('cursor skips', State.cursor1.line)
    y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width)
    State.cursor1.line = State.cursor1.line + 1
    end
    -- hack: insert a text line at bottom of file if necessary
    if State.cursor1.line > #State.lines then
    assert(State.cursor1.line == #State.lines+1)
    table.insert(State.lines, {mode='text', data=''})
    table.insert(State.line_cache, {})
    end
    --? print(y, App.screen.height, App.screen.height-State.line_height)
    if y > App.screen.height - State.line_height then
    [27.2683]
    [27.1554]
    if State.top > App.screen.height - State.line_height then
  • replacement in text.lua at line 615
    [27.394][27.3259:3369](),[27.3369][27.16735:16767](),[27.1424][27.545:585](),[27.3408][27.545:585](),[27.16767][27.545:585](),[27.76011][27.545:585](),[27.545][27.545:585](),[27.585][27.767:776](),[27.767][27.767:776](),[27.776][27.586:614](),[27.614][27.3409:3466](),[27.3466][27.665:767](),[27.76076][27.665:767](),[27.665][27.665:767](),[27.767][27.290:387](),[27.326][27.16768:16800](),[27.387][27.16768:16800](),[27.3557][27.16768:16800](),[27.1458][27.857:946](),[27.3596][27.857:946](),[27.16800][27.857:946](),[27.76227][27.857:946](),[27.857][27.857:946]()
    if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then
    local h = State.line_height
    if y - h < State.top then
    break
    end
    y = y - h
    else
    assert(top2.line > 1)
    assert(State.lines[top2.line-1].mode == 'drawing')
    -- We currently can't draw partial drawings, so either skip it entirely
    -- or not at all.
    local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width)
    if y - h < State.top then
    break
    end
    --? print('skipping drawing of height', h)
    y = y - h
    [27.394]
    [27.875]
    local h = State.line_height
    if y - h < State.top then
    break
  • edit in text.lua at line 619
    [27.883]
    [27.2833]
    y = y - h
  • replacement in text.lua at line 650
    [27.6001][27.1554:1648](),[27.1554][27.1554:1648](),[27.836][27.910:1037](),[27.1648][27.910:1037](),[27.910][27.910:1037]()
    local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
    --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
    [27.6001]
    [27.4121]
    local screen_line_starting_byte_offset = Text.offset(line, screen_line_starting_pos)
    --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line, screen_line_starting_byte_offset))
  • replacement in text.lua at line 661
    [27.1679][27.1679:1751]()
    local s = string.sub(line.data, screen_line_starting_byte_offset)
    [27.538]
    [27.3151]
    local s = string.sub(line, screen_line_starting_byte_offset)
  • replacement in text.lua at line 674
    [27.6333][27.1146:1203](),[27.1146][27.1146:1203]()
    local start_offset = Text.offset(line.data, start_pos)
    [27.6333]
    [27.1203]
    local start_offset = Text.offset(line, start_pos)
  • replacement in text.lua at line 678
    [27.6451][27.1328:1466](),[27.1328][27.1328:1466]()
    local past_end_offset = Text.offset(line.data, past_end_pos)
    screen_line = string.sub(line.data, start_offset, past_end_offset-1)
    [27.6451]
    [27.1466]
    local past_end_offset = Text.offset(line, past_end_pos)
    screen_line = string.sub(line, start_offset, past_end_offset-1)
  • replacement in text.lua at line 681
    [27.1473][27.1473:1524]()
    screen_line = string.sub(line.data, start_pos)
    [27.1473]
    [27.1524]
    screen_line = string.sub(line, start_pos)
  • edit in text.lua at line 784
    [20.34][20.34:142](),[20.142][27.103:109](),[27.103][27.103:109]()
    if State.lines[loc1.line].mode == 'drawing' then
    return {line=loc1.line, screen_line=1, screen_pos=1}
    end
  • edit in text.lua at line 845
    [20.897][20.897:1013]()
    elseif State.lines[loc2.line-1].mode == 'drawing' then
    return {line=loc2.line-1, screen_line=1, screen_pos=1}
  • edit in text.lua at line 854
    [27.712][21.3:44]()
    if line.mode ~= 'text' then return end
  • replacement in select.lua at line 56
    [27.242][27.1160:1309]()
    local lo_offset = Text.offset(line.data, lo)
    local hi_offset = Text.offset(line.data, hi)
    local pos_offset = Text.offset(line.data, pos)
    [27.242]
    [27.391]
    local lo_offset = Text.offset(line, lo)
    local hi_offset = Text.offset(line, hi)
    local pos_offset = Text.offset(line, pos)
  • replacement in select.lua at line 63
    [27.454][27.454:514]()
    local before = line.data:sub(pos_offset, lo_offset-1)
    [27.454]
    [27.514]
    local before = line:sub(pos_offset, lo_offset-1)
  • replacement in select.lua at line 68
    [27.706][27.706:758]()
    local s = line.data:sub(lo_offset, hi_offset-1)
    [27.706]
    [27.758]
    local s = line:sub(lo_offset, hi_offset-1)
  • replacement in select.lua at line 95
    [27.1531][27.46343:46375](),[27.79610][27.46343:46375](),[27.46343][27.46343:46375](),[27.46375][27.91:142](),[27.142][27.212:283](),[27.229][27.46471:46481](),[27.283][27.46471:46481](),[27.4292][27.46471:46481](),[27.6090][27.46471:46481](),[27.46471][27.46471:46481]()
    if line.mode == 'text' then
    if Text.in_line(State, line_index, x,y) then
    return line_index, Text.to_pos_on_line(State, line_index, x,y)
    end
    [27.1531]
    [27.46481]
    if Text.in_line(State, line_index, x,y) then
    return line_index, Text.to_pos_on_line(State, line_index, x,y)
  • replacement in select.lua at line 139
    [27.608][27.2656:2782](),[27.2656][27.2656:2782]()
    local min_offset = Text.offset(State.lines[minl].data, minp)
    local max_offset = Text.offset(State.lines[maxl].data, maxp)
    [27.608]
    [27.47487]
    local min_offset = Text.offset(State.lines[minl], minp)
    local max_offset = Text.offset(State.lines[maxl], maxp)
  • replacement in select.lua at line 143
    [27.47540][27.2783:2896]()
    State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset)
    [27.47540]
    [27.47635]
    State.lines[minl] = State.lines[minl]:sub(1, min_offset-1)..State.lines[minl]:sub(max_offset)
  • replacement in select.lua at line 147
    [27.47674][27.2897:2950]()
    local rhs = State.lines[maxl].data:sub(max_offset)
    [27.47674]
    [27.47721]
    local rhs = State.lines[maxl]:sub(max_offset)
  • replacement in select.lua at line 152
    [27.47780][27.2985:3061]()
    State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhs
    [27.47780]
    [27.47844]
    State.lines[minl] = State.lines[minl]:sub(1, min_offset-1)..rhs
  • replacement in select.lua at line 168
    [27.48224][27.3318:3444]()
    local min_offset = Text.offset(State.lines[minl].data, minp)
    local max_offset = Text.offset(State.lines[maxl].data, maxp)
    [27.48224]
    [27.48338]
    local min_offset = Text.offset(State.lines[minl], minp)
    local max_offset = Text.offset(State.lines[maxl], maxp)
  • replacement in select.lua at line 171
    [27.48361][27.3445:3509]()
    return State.lines[minl].data:sub(min_offset, max_offset-1)
    [27.48361]
    [27.48419]
    return State.lines[minl]:sub(min_offset, max_offset-1)
  • replacement in select.lua at line 174
    [27.48447][27.3510:3568]()
    local result = {State.lines[minl].data:sub(min_offset)}
    [27.48447]
    [27.48503]
    local result = {State.lines[minl]:sub(min_offset)}
  • replacement in select.lua at line 176
    [27.48528][27.3569:3659](),[27.100][27.48607:48615](),[27.3659][27.48607:48615](),[27.81719][27.48607:48615](),[27.48607][27.48607:48615]()
    if State.lines[i].mode == 'text' then
    table.insert(result, State.lines[i].data)
    end
    [27.48528]
    [27.48615]
    table.insert(result, State.lines[i])
  • replacement in select.lua at line 178
    [27.48621][27.3660:3728]()
    table.insert(result, State.lines[maxl].data:sub(1, max_offset-1))
    [27.48621]
    [27.163]
    table.insert(result, State.lines[maxl]:sub(1, max_offset-1))
  • replacement in search.lua at line 24
    [27.758][22.6:101]()
    local pos = find(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos)
    [27.758]
    [22.101]
    local pos = find(State.lines[State.cursor1.line], State.search_term, State.cursor1.pos)
  • replacement in search.lua at line 31
    [27.211][22.144:201]()
    pos = find(State.lines[i].data, State.search_term)
    [27.211]
    [22.201]
    pos = find(State.lines[i], State.search_term)
  • replacement in search.lua at line 42
    [27.368][22.297:354]()
    pos = find(State.lines[i].data, State.search_term)
    [27.368]
    [27.50189]
    pos = find(State.lines[i], State.search_term)
  • replacement in search.lua at line 52
    [27.833][22.355:427]()
    pos = find(State.lines[State.cursor1.line].data, State.search_term)
    [27.833]
    [22.427]
    pos = find(State.lines[State.cursor1.line], State.search_term)
  • replacement in search.lua at line 72
    [23.800][27.751:849](),[27.50784][27.751:849]()
    local pos = rfind(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos-1)
    [23.800]
    [27.50856]
    local pos = rfind(State.lines[State.cursor1.line], State.search_term, State.cursor1.pos-1)
  • replacement in search.lua at line 79
    [27.1238][27.1238:1296]()
    pos = rfind(State.lines[i].data, State.search_term)
    [27.1238]
    [27.50998]
    pos = rfind(State.lines[i], State.search_term)
  • replacement in search.lua at line 90
    [27.1411][27.1411:1469]()
    pos = rfind(State.lines[i].data, State.search_term)
    [27.1411]
    [27.51229]
    pos = rfind(State.lines[i], State.search_term)
  • replacement in search.lua at line 100
    [23.910][23.910:983]()
    pos = rfind(State.lines[State.cursor1.line].data, State.search_term)
    [23.910]
    [23.983]
    pos = rfind(State.lines[State.cursor1.line], State.search_term)
  • replacement in main_tests.lua at line 41
    [27.84890][27.84890:85118]()
    check_eq(Editor_state.lines[1].data, 'abc', 'F - test_drop_file/lines:1')
    check_eq(Editor_state.lines[2].data, 'def', 'F - test_drop_file/lines:2')
    check_eq(Editor_state.lines[3].data, 'ghi', 'F - test_drop_file/lines:3')
    [27.84890]
    [27.7]
    check_eq(Editor_state.lines[1], 'abc', 'F - test_drop_file/lines:1')
    check_eq(Editor_state.lines[2], 'def', 'F - test_drop_file/lines:2')
    check_eq(Editor_state.lines[3], 'ghi', 'F - test_drop_file/lines:3')
  • edit in main.lua at line 2
    [27.30]
    [27.3]
    json = require 'json'
  • edit in main.lua at line 47
    [27.85498][27.8:44]()
    edit.fixup_cursor(Editor_state)
  • edit in main.lua at line 50
    [27.7698][24.8:178]()
    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
  • edit in main.lua at line 127
    [27.16][24.179:213]()
    edit.fixup_cursor(Editor_state)
  • replacement in file.lua at line 15
    [27.15620][27.15620:15864]()
    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
    [27.15620]
    [27.15864]
    table.insert(result, line)
  • replacement in file.lua at line 19
    [27.15901][27.15901:15950]()
    table.insert(result, {mode='text', data=''})
    [27.15901]
    [27.15950]
    table.insert(result, '')
  • replacement in file.lua at line 30
    [15.419][27.16090:16169](),[27.16090][27.16090:16169](),[27.16169][6.14:51](),[6.51][27.16206:16214](),[27.16206][27.16206:16214]()
    if line.mode == 'drawing' then
    store_drawing(outfile, line)
    else
    outfile:write(line.data, '\n')
    end
    [15.419]
    [27.16214]
    outfile:write(line, '\n')
  • edit in file.lua at line 35
    [27.16243][27.16243:16524](),[27.16524][27.1072:1235](),[27.1235][27.16586:16664](),[27.16586][27.16586:16664](),[27.16664][27.1236:1306](),[27.1306][27.16664:16742](),[27.16664][27.16664:16742](),[27.16742][27.1307:1350](),[27.1350][27.139:234](),[27.16742][27.139:234](),[27.234][27.16782:16825](),[27.16782][27.16782:16825](),[27.16825][27.1351:1379](),[27.1379][27.16825:16899](),[27.16825][27.16825:16899](),[27.16899][27.1380:1434](),[27.1434][27.16899:16971](),[27.16899][27.16899:16971](),[27.16971][27.1435:1472](),[27.1472][27.16971:17060](),[27.16971][27.16971:17060](),[27.17060][27.10:57](),[27.57][27.10:66](),[27.57][27.235:288](),[27.66][27.235:288](),[27.1519][27.235:288](),[27.17060][27.235:288](),[27.288][27.17060:17287](),[27.17060][27.17060:17287](),[27.17287][6.52:98](),[6.98][27.17333:17507](),[27.17333][27.17333:17507](),[27.17507][6.99:131](),[6.131][27.289:384](),[27.17539][27.289:384](),[27.384][27.17579:17771](),[27.17579][27.17579:17771](),[27.17771][6.132:164](),[6.164][27.17803:17842](),[27.17803][27.17803:17842](),[27.17842][6.165:281](),[6.281][27.17958:17994](),[27.17958][27.17958:17994](),[27.17994][6.282:456](),[6.456][27.67:123](),[27.18168][27.67:123](),[27.123][27.385:438](),[27.18168][27.385:438](),[27.438][27.18168:18207](),[27.18168][27.18168:18207](),[27.18207][27.1376:1381]()
    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
  • replacement in file.lua at line 43
    [27.1566][27.2583:2603](),[27.2603][27.1566:1671](),[27.1566][27.1566:1671](),[27.1671][27.2604:2765](),[27.2765][27.1740:1749](),[27.1740][27.1740:1749](),[27.1749][27.2766:2800](),[27.2800][27.1749:1810](),[27.1749][27.1749:1810]()
    --? print(line)
    if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated
    --? print('inserting drawing')
    i, drawing = load_drawing_from_array(next_line, a, i)
    --? print('i now', i)
    table.insert(result, drawing)
    else
    --? print('inserting text')
    table.insert(result, {mode='text', data=line})
    end
    [27.1566]
    [27.1810]
    table.insert(result, line)
  • replacement in file.lua at line 46
    [27.1839][27.1839:1888]()
    table.insert(result, {mode='text', data=''})
    [27.1839]
    [27.1888]
    table.insert(result, '')
  • edit in file.lua at line 49
    [27.1910][27.1910:2106](),[27.2106][27.2801:2818](),[27.2818][27.2106:3102](),[27.2106][27.2106:3102](),[27.3102][27.58:105](),[27.105][27.124:180](),[27.105][27.3148:3255](),[27.180][27.3148:3255](),[27.3148][27.3148:3255](),[27.3255][27.2819:2839]()
    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
  • edit in edit.lua at line 4
    [27.519][27.519:712]()
    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
  • edit in edit.lua at line 6
    [27.839][27.839:997]()
    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}
  • edit in edit.lua at line 11
    [27.401][27.401:524](),[27.524][27.15:103](),[27.22248][27.15:103](),[27.103][27.22248:22249](),[27.22248][27.22248:22249]()
    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 in edit.lua at line 15
    [27.484][27.484:548]()
    require 'drawing'
    require 'geom'
    require 'help'
    require 'icons'
  • replacement in edit.lua at line 21
    [27.94338][27.94338:94459](),[27.94918][27.94918:95767](),[27.95767][27.7706:7763]()
    -- a line is either text or a drawing
    -- a text is a table with:
    -- mode = 'text',
    -- string data,
    -- a drawing is a table with:
    -- mode = 'drawing'
    -- a (y) coord in pixels (updated while painting screen),
    -- a (h)eight,
    -- an array of points, and
    -- an array of shapes
    -- a shape is a table containing:
    -- a mode
    -- an array points for mode 'freehand' (raw x,y coords; freehand drawings don't pollute the points array of a drawing)
    -- an array vertices for mode 'polygon', 'rectangle', 'square'
    -- p1, p2 for mode 'line'
    -- center, radius for mode 'circle'
    -- center, radius, start_angle, end_angle for mode 'arc'
    -- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide
    -- The field names are carefully chosen so that switching modes in midstream
    -- remembers previously entered points where that makes sense.
    lines = {{mode='text', data=''}}, -- array of lines
    [27.94338]
    [27.1993]
    lines = {''}, -- array of strings
  • edit in edit.lua at line 58
    [27.3184][27.97073:97217](),[27.97217][27.3320:3321](),[27.3320][27.3320:3321]()
    current_drawing_mode = 'line',
    previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points
  • edit in edit.lua at line 82
    [27.3868][27.89:123](),[27.123][24.221:333](),[24.333][27.347:366](),[27.347][27.347:366]()
    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
  • replacement in edit.lua at line 97
    [27.3138][27.15:91](),[27.91][27.273:298](),[27.298][27.92:143](),[27.4442][27.92:143](),[27.143][27.299:340](),[27.340][27.189:609](),[27.189][27.189:609](),[27.609][27.1896:1966](),[27.1966][27.609:972](),[27.8494][27.609:972](),[27.609][27.609:972](),[27.972][27.5229:5239](),[27.5229][27.5229:5239](),[27.5239][27.341:419](),[27.419][27.3585:3617](),[27.1038][27.3585:3617](),[27.3585][27.3585:3617](),[27.3617][27.1039:1066](),[27.1066][27.5308:5347](),[27.3617][27.5308:5347](),[27.99122][27.5308:5347](),[27.5308][27.5308:5347](),[27.5347][27.525:557](),[27.557][13.15:56](),[13.56][27.558:633](),[27.47][27.558:633](),[27.633][27.5483:5492](),[27.684][27.5483:5492](),[27.3725][27.5483:5492](),[27.99244][27.5483:5492](),[27.5483][27.5483:5492](),[27.5492][27.1067:1110]()
    if line.mode == 'text' then
    --? print('text.draw', y, line_index)
    local startpos = 1
    if line_index == State.screen_top1.line then
    startpos = State.screen_top1.pos
    end
    if line.data == '' then
    -- button to insert new drawing
    button('draw', {x=4,y=y+4, w=12,h=12, color={1,1,0},
    icon = icon.insert_drawing,
    onpress1 = function()
    Drawing.before = snapshot(State, line_index-1, line_index)
    table.insert(State.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})
    table.insert(State.line_cache, line_index, {})
    if State.cursor1.line >= line_index then
    State.cursor1.line = State.cursor1.line+1
    end
    schedule_save(State)
    record_undo_event(State, {before=Drawing.before, after=snapshot(State, line_index-1, line_index+1)})
    end,
    })
    end
    y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
    y = y + State.line_height
    --? print('=> y', y)
    elseif line.mode == 'drawing' then
    y = y+Drawing_padding_top
    Drawing.draw(State, line_index, y)
    y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom
    else
    print(line.mode)
    assert(false)
    [27.3138]
    [27.5845]
    --? print('text.draw', y, line_index)
    local startpos = 1
    if line_index == State.screen_top1.line then
    startpos = State.screen_top1.pos
  • edit in edit.lua at line 102
    [27.5853]
    [27.5853]
    y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
    y = y + State.line_height
    --? print('=> y', y)
  • replacement in edit.lua at line 109
    [27.5923][27.4061:4191]()
    --? print('screen bottom: '..tostring(State.screen_bottom1.pos)..' in '..tostring(State.lines[State.screen_bottom1.line].data))
    [27.5923]
    [27.4191]
    --? print('screen bottom: '..tostring(State.screen_bottom1.pos)..' in '..tostring(State.lines[State.screen_bottom1.line]))
  • edit in edit.lua at line 116
    [27.4252][27.15:43]()
    Drawing.update(State, dt)
  • replacement in edit.lua at line 141
    [27.4696][27.7059:7091](),[27.100424][27.7059:7091](),[27.7059][27.7059:7091](),[27.7091][27.153:204](),[27.60][27.7173:7631](),[27.204][27.7173:7631](),[27.4791][27.7173:7631](),[27.6356][27.7173:7631](),[27.22676][27.7173:7631](),[27.100533][27.7173:7631](),[27.7173][27.7173:7631](),[27.7631][10.15:82](),[10.82][27.4792:4961](),[27.7697][27.4792:4961](),[27.4961][27.7830:7859](),[27.100745][27.7830:7859](),[27.7830][27.7830:7859](),[27.7859][27.294:356](),[27.117][27.7952:7962](),[27.356][27.7952:7962](),[27.5067][27.7952:7962](),[27.6469][27.7952:7962](),[27.22758][27.7952:7962](),[27.100865][27.7952:7962](),[27.7952][27.7952:7962](),[27.7962][27.5068:5144](),[27.5144][27.8026:8089](),[27.100956][27.8026:8089](),[27.8026][27.8026:8089](),[27.8089][13.57:192](),[13.192][27.5145:5243](),[27.754][27.5145:5243](),[27.8134][27.5145:5243](),[27.5243][27.2242:2295](),[27.2295][13.193:261](),[27.77][27.8321:8345](),[13.261][27.8321:8345](),[27.8321][27.8321:8345]()
    if line.mode == 'text' then
    if Text.in_line(State, line_index, x,y) then
    -- delicate dance between cursor, selection and old cursor/selection
    -- scenarios:
    -- regular press+release: sets cursor, clears selection
    -- shift press+release:
    -- sets selection to old cursor if not set otherwise leaves it untouched
    -- sets cursor
    -- press and hold to start a selection: sets selection on press, cursor on release
    -- press and hold, then press shift: ignore shift
    -- i.e. mouse_released should never look at shift state
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    --? print('selection', State.selection1.line, State.selection1.pos)
    break
    end
    elseif line.mode == 'drawing' then
    local line_cache = State.line_cache[line_index]
    if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) then
    State.lines.current_drawing_index = line_index
    State.lines.current_drawing = line
    Drawing.before = snapshot(State, line_index)
    Drawing.mouse_pressed(State, line_index, x,y, mouse_button)
    break
    end
    [27.4696]
    [27.8345]
    if Text.in_line(State, line_index, x,y) then
    -- delicate dance between cursor, selection and old cursor/selection
    -- scenarios:
    -- regular press+release: sets cursor, clears selection
    -- shift press+release:
    -- sets selection to old cursor if not set otherwise leaves it untouched
    -- sets cursor
    -- press and hold to start a selection: sets selection on press, cursor on release
    -- press and hold, then press shift: ignore shift
    -- i.e. mouse_released should never look at shift state
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    --? print('selection', State.selection1.line, State.selection1.pos)
    break
  • replacement in edit.lua at line 167
    [27.8468][27.5339:5377](),[27.5377][27.15:68](),[27.68][27.420:445](),[27.445][27.8641:8668](),[27.8641][27.8641:8668](),[27.8668][27.2296:2410](),[27.2410][27.8762:8804](),[27.5478][27.8762:8804](),[27.101270][27.8762:8804](),[27.8762][27.8762:8804](),[27.8804][27.5479:5529](),[27.5529][27.8848:8882](),[27.101328][27.8848:8882](),[27.8848][27.8848:8882](),[27.8882][27.205:258](),[27.165][27.8966:9005](),[27.258][27.8966:9005](),[27.5626][27.8966:9005](),[27.6573][27.8966:9005](),[27.22831][27.8966:9005](),[27.101439][27.8966:9005](),[27.8966][27.8966:9005](),[27.9005][27.5627:5655](),[27.5655][27.9027:9058](),[27.101475][27.9027:9058](),[27.9027][27.9027:9058](),[27.9058][27.357:421](),[27.224][27.9153:9165](),[27.421][27.9153:9165](),[27.5763][27.9153:9165](),[27.6688][27.9153:9165](),[27.22915][27.9153:9165](),[27.101597][27.9153:9165](),[27.9153][27.9153:9165](),[27.9165][27.5764:5978](),[27.5978][27.9343:9360](),[27.101854][27.9343:9360](),[27.9343][27.9343:9360](),[27.9360][27.5979:6033](),[27.6033][27.9402:9432](),[27.101923][27.9402:9432](),[27.9402][27.9402:9432](),[27.9432][27.6034:6202](),[27.6202][27.9564:9594](),[27.102134][27.9564:9594](),[27.9564][27.9564:9594]()
    if State.lines.current_drawing then
    Drawing.mouse_released(State, x,y, mouse_button)
    schedule_save(State)
    if Drawing.before then
    record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
    Drawing.before = nil
    end
    else
    for line_index,line in ipairs(State.lines) do
    if line.mode == 'text' then
    if Text.in_line(State, line_index, x,y) then
    --? print('reset selection')
    State.cursor1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    --? print('cursor', State.cursor1.line, State.cursor1.pos)
    if State.mousepress_shift then
    if State.old_selection1.line == nil then
    State.selection1 = State.old_cursor1
    else
    State.selection1 = State.old_selection1
    end
    end
    State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
    if eq(State.cursor1, State.selection1) then
    State.selection1 = {}
    end
    break
    [27.8468]
    [27.9594]
    for line_index,line in ipairs(State.lines) do
    if Text.in_line(State, line_index, x,y) then
    --? print('reset selection')
    State.cursor1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    --? print('cursor', State.cursor1.line, State.cursor1.pos)
    if State.mousepress_shift then
    if State.old_selection1.line == nil then
    State.selection1 = State.old_cursor1
    else
    State.selection1 = State.old_selection1
  • edit in edit.lua at line 182
    [27.9616]
    [27.9616]
    State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
    if eq(State.cursor1, State.selection1) then
    State.selection1 = {}
    end
    break
  • edit in edit.lua at line 188
    [27.9624][27.6203:6276]()
    --? print('selection:', State.selection1.line, State.selection1.pos)
  • edit in edit.lua at line 189
    [27.9691]
    [27.9691]
    --? print('selection:', State.selection1.line, State.selection1.pos)
  • edit in edit.lua at line 198
    [27.2117][27.6496:6547](),[27.9975][27.6496:6547](),[27.6547][27.2411:2481](),[27.2481][27.6610:6658](),[27.6610][27.6610:6658](),[27.6658][27.10119:10201](),[27.102626][27.10119:10201](),[27.10119][27.10119:10201](),[27.10201][27.2482:2586]()
    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)})
  • edit in edit.lua at line 206
    [27.6831][27.6831:6873]()
    not State.lines.current_drawing and
  • replacement in edit.lua at line 313
    [27.2812][27.14470:14561](),[27.9002][27.14470:14561](),[27.105277][27.14470:14561](),[27.14470][27.14470:14561](),[27.14561][16.1250:1292](),[16.1292][27.15:81](),[27.14592][27.15:81](),[27.81][27.14653:14679](),[27.14653][27.14653:14679](),[27.14679][27.2813:2865](),[27.2865][27.15:60](),[27.14724][27.15:60](),[27.60][27.2866:2952](),[27.2952][27.578:605](),[27.14834][27.578:605](),[27.605][27.14856:14922](),[27.14856][27.14856:14922](),[27.14922][27.9003:9044](),[27.9044][27.14957:15043](),[27.105326][27.14957:15043](),[27.14957][27.14957:15043](),[27.15043][27.9045:9096](),[27.9096][27.15088:15118](),[27.105385][27.15088:15118](),[27.15088][27.15088:15118](),[27.15118][27.9097:9200](),[27.9200][27.15203:15212](),[27.105510][27.15203:15212](),[27.15203][27.15203:15212](),[27.15212][27.2953:3025](),[27.3025][27.9266:9316](),[27.9266][27.9266:9316](),[27.9316][27.15315:15429](),[27.105640][27.15315:15429](),[27.15315][27.15315:15429](),[27.15429][27.3026:3134](),[27.3134][27.15517:15745](),[27.9411][27.15517:15745](),[27.105742][27.15517:15745](),[27.15517][27.15517:15745](),[27.15745][27.3135:3243](),[27.3243][27.15833:15851](),[27.9506][27.15833:15851](),[27.105844][27.15833:15851](),[27.15833][27.15833:15851](),[27.15851][27.606:631]()
    -- dispatch to drawing or text
    elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
    -- DON'T reset line_cache.starty here
    local drawing_index, drawing = Drawing.current_drawing(State)
    if drawing_index then
    local before = snapshot(State, drawing_index)
    Drawing.keychord_pressed(State, chord)
    record_undo_event(State, {before=before, after=snapshot(State, drawing_index)})
    schedule_save(State)
    end
    elseif chord == 'escape' and not App.mouse_down(1) then
    for _,line in ipairs(State.lines) do
    if line.mode == 'drawing' then
    line.show_help = false
    end
    end
    elseif State.current_drawing_mode == 'name' then
    if chord == 'return' then
    State.current_drawing_mode = State.previous_drawing_mode
    State.previous_drawing_mode = nil
    else
    local before = snapshot(State, State.lines.current_drawing_index)
    local drawing = State.lines.current_drawing
    local p = drawing.points[drawing.pending.target_point]
    if chord == 'escape' then
    p.name = nil
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    elseif chord == 'backspace' then
    local len = utf8.len(p.name)
    local byte_offset = Text.offset(p.name, len-1)
    if len == 1 then byte_offset = 0 end
    p.name = string.sub(p.name, 1, byte_offset)
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    end
    end
    schedule_save(State)
    [27.2812]
    [27.15871]
    -- dispatch to text
  • replacement in README.md at line 1
    [27.6][27.14:38]()
    # Plain text with lines
    [27.6]
    [27.18]
    # 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.
  • edit in README.md at line 7
    [27.19][27.19:96]()
    An editor for plain text where you can also seamlessly insert line drawings.
  • edit in README.md at line 10
    [27.39][27.39:71](),[27.71][27.18:19]()
    http://akkartik.name/lines.html
  • replacement in README.md at line 17
    [25.242][25.242:273]()
    $ zip -r /tmp/lines.love *.lua
    [25.242]
    [25.273]
    $ zip -r /tmp/text.love *.lua
  • replacement in README.md at line 20
    [27.223][27.223:296]()
    By default, lines.love reads/writes the file `lines.txt` in your default
    [27.223]
    [27.296]
    By default, it reads/writes the file `lines.txt` in your default
  • replacement in README.md at line 23
    [27.379][27.59:119]()
    To open a different file, drop it on the lines.love window.
    [27.379]
    [27.504]
    To open a different file, drop it on the app window.
  • replacement in README.md at line 39
    [5.19][5.19:95]()
    lines.love has been exclusively tested so far with a US keyboard layout. If
    [5.19]
    [5.95]
    Exclusively tested so far with a US keyboard layout. If
  • replacement in README.md at line 50
    [27.95][8.19:101]()
    other ways. lines.love works well in all circumstances with files under
    50KB.
    [27.95]
    [27.276]
    other ways. Works well in all circumstances with files under 50KB.
  • replacement in README.md at line 83
    [27.1142][27.1142:1497](),[27.1497][26.20:62](),[26.62][27.1497:1528](),[27.1497][27.1497:1528]()
    Updates to lines.love can be downloaded from the following mirrors in addition
    to the website above:
    * https://github.com/akkartik/lines.love
    * https://repo.or.cz/lines.love.git
    * https://codeberg.org/akkartik/lines.love
    * https://tildegit.org/akkartik/lines.love
    * https://git.tilde.institute/akkartik/lines.love
    * https://git.sr.ht/~akkartik/lines.love
    * https://notabug.org/akkartik/lines.love
    * https://pagure.io/lines.love
    [27.1142]
    [27.1528]
    This repo is a fork of lines.love at [http://akkartik.name/lines.html](http://akkartik.name/lines.html).
    Updates to it can be downloaded from the following mirrors:
  • replacement in README.md at line 86
    [27.1529][4.18:96](),[4.96][27.1603:1609](),[27.1603][27.1603:1609](),[27.1609][27.18:197](),[27.197][9.19:40]()
    Forks of lines.love are encouraged. If you show me your fork, I'll link to it
    here.
    * https://github.com/akkartik/lines-polygon-experiment -- an experiment that
    uses separate shortcuts for regular polygons. `ctrl+3` for triangles,
    `ctrl+4` for squares, etc.
    ## Associated tools
    [27.1529]
    [27.1609]
    * https://codeberg.org/akkartik/text.love
    * https://repo.or.cz/text.love.git
    * https://tildegit.org/akkartik/text.love
    * https://git.tilde.institute/akkartik/text.love
    * https://git.sr.ht/~akkartik/text.love
    * https://notabug.org/akkartik/text.love
    * https://github.com/akkartik/text.love
    * https://pagure.io/text.love
  • replacement in README.md at line 95
    [27.1610][9.41:145]()
    * https://codeberg.org/akkartik/lines2md exports lines.love files to Markdown
    and (non-editable) SVG.
    [27.1610]
    [9.145]
    Further forks are encouraged. If you show me your fork, I'll link to it here.