experiment: new edit namespace

[?]
Jul 12, 2022, 5:14 AM
2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC

Dependencies

  • [2] QW5KQQTD fix a comment
  • [3] Q7BDB3XQ .
  • [4] SHEGBK4H while we're at it, undo naming points
  • [5] T7IWZFL4 more precise scroll on paste
  • [6] TC4HQILH when naming points, allow backspacing back to ''
  • [7] S7ZZA3YE ugh, handle absolute as well as relative paths
  • [8] 3EMBUWVW what should happen to selection while drawing?
  • [9] 6VXO3ZL3 just keep the cursor visible after any input events
  • [10] GK47BBCY start passing left/right margins everywhere
  • [11] 4CXVIEBS add args to some functions
  • [12] K4OBZSHE add args to some functions
  • [13] QCQTMUZ7 add args to some functions
  • [14] NX3DDSCZ fix a variable name
  • [15] 4CTZOJPC stop pretending globals are local
  • [16] 6LJZN727 handle chords
  • [17] S5VCAFKY couple of tests for cursor down
  • [18] 4RUI5X52 a few tests for pageup, and a bugfix
  • [19] HDC3AAQP silly reason my screenshots had an ugly black line down the left
  • [20] HJ3PM2VT .
  • [21] OAHNWDYG .
  • [22] 7SFHSB47 rename
  • [23] VXORMHME delete experimental REPL
  • [24] OGUV4HSA remove some memory leaks from rendered fragments
  • [25] 3CS5KKCI up/down cursor movement
  • [26] QU7NHFOV show cursor
  • [27] YKRF5V3Z starting to load/save
  • [28] 2C7CTIQY make space for multiple kinds of width
  • [29] DLQMM265 scroll past first page
  • [30] TNHZZYWP update some documentation
  • [31] KJKKASHZ reduce ambitions a bit: page up/down need not start screen from the middle of a line
  • [32] 252M2QMD forgot to move this special case out
  • [33] IZZVOCLB confirm that we have access to all of the love API
  • [34] WLHI7KD3 new globals: draw partial screen line up top
  • [35] NQWWTGXR switch undo/redo to ctrl- hotkeys
  • [36] TKFSYQ2Z up arrow to search previous
  • [37] EFMLTMZG bugfix: restrict strokes to the drawing they started in
  • [38] J2SVGR2E experiment: blinking cursor
  • [39] RMKMPFT5 fix a corner case when selecting text
  • [40] 3TDOZESE extract scrolling logic out of insert_return
  • [41] BOFNXP5G clicking now moves the cursor even on long, wrapped lines
  • [42] YTSPVDZH first successful pagedown test, first bug found by test
  • [43] SN2QONLI autosave on cut/paste
  • [44] SR7L4QPZ revert previous commit
  • [45] TVM2WIHH bugfix: autosave and undo in a couple of cases
  • [46] IDG26SXK bugfix in commit e51ce12969
  • [47] DRFE3B3Z mouse buttons are integers, not strings
  • [48] ZZ2B5RPQ extract variables for drawing padding
  • [49] MP2TBKU6 bugfix: crash in Text.up() after return
  • [50] 4WAFGF4Z selection bugfix
  • [51] AM42E4Y6 avoid redundant writes on exit
  • [52] MYC7XR5Q bugfix: lines that aren't drawn from the start
  • [53] H2DPLWMV snapshot: wrapping long lines at word boundaries
  • [54] 7IKRRESB longer names for indices in long loops
  • [55] TVCPXAAU rename
  • [56] DGK5BPVI bugfix: UTF-8 in compute_fragments
  • [57] AJB4LFRB try to maintain a reasonable line width
  • [58] J5IEBT64 enforce press/release state only processed once
  • [59] AMXTYDOF show the line width when dragging the slider
  • [60] XHLL3JQR show cursor immediately after a mouse click
  • [61] AOIRVVJA revert selection logic to before commit 3ffc2ed8f
  • [62] YGCT2D2O start loading settings as applicable
  • [63] VVXVV2D2 change data model; text can now have metadata
  • [64] EMRPLZPW drop an arg from a function
  • [65] WDWXNW7V slightly strange way to move points
  • [66] AVLAYODP much simpler
  • [67] FFBIY74N bugfix: 'escape' to cancel a stroke
  • [68] RJGZD4IN binary search to most natural up/down with proportional fonts
  • [69] XNFTJHC4 split keyboard handling between Text and Drawing
  • [70] MNWHXPBL more lightweight; select just the stroke at the mouse
  • [71] 3RGHOJ25 DRY some code
  • [72] CVGE3SIG I feel confident now that page-down is working.
  • [73] VHQCNMAR several more modules
  • [74] IRCKL6VN extract scrolling logic out of insert_at_cursor
  • [75] LAW2O3NW extract variable Margin_left
  • [76] K464QQR4 more defensive resize handling
  • [77] QCQHLMST always have a filename
  • [78] NZ7V4BVS note card
  • [79] DHI6IJCN selecting text and deleting selections
  • [80] HYEAFRZ2 split mouse_pressed events between Text and Drawing
  • [81] PESSMQBJ no, make sure to compute line width after screen dimensions
  • [82] ZUOL7X6V move
  • [83] AMSESRTH move some code
  • [84] OIB2QPRC start remembering where the cursor is drawn in px
  • [85] D2HYRJXI .
  • [86] VC2CU2GG faster paste
  • [87] FYS7TCDW bugfix
  • [88] 5FW7YOFT highlight selection while dragging
  • [89] EMHRPJ3R no, that's not right
  • [90] AD34IX2Z couple more tests
  • [91] U7M4M2F7 bugfix: don't rely on Screen_bottom1 while scrolling
  • [92] VJ3ODCHR assert for a bug I saw a while ago but can no longer reproduce
  • [93] M6TH7VSZ rip out notion of Line_width
  • [94] MGOQ5XAV start uppercasing globals
  • [95] ZNLTRNNK highlight another global
  • [96] BATTU6HW stop blanking screen while resizing
  • [97] FJ4L6N74 draw lines by default
  • [98] G3VLJLDH use the background color
  • [99] VCMS2CWT bugfix: escape key to hide online help
  • [100] QLTJG7Q3 indent
  • [101] BYG5CEMV support for naming points
  • [102] WOXIYUTL bugfix: manage screen_top and cursor when resizing
  • [103] JRLBUB6L more intuitive point delete from polygons
  • [104] JCSLDGAH beginnings of support for multiple shapes
  • [105] OTIBCAUJ love2d scaffold
  • [106] 4NDYV4WD fix 2 bugs in line selection
  • [107] XXI67EXR drop non-existent feature from comment
  • [108] AVQ5MC5D finish uppercasing all globals
  • [109] BLWAYPKV extract a module
  • [110] NQKFQSZE undo creating new drawings
  • [111] 2POFQQLW keep cursor on screen when pressing 'down'
  • [112] LFMI3D7D stop scanning from start of file on every App.draw
  • [113] VIU2FBNV make sure to save right when quitting
  • [114] D2GCFTTT clean up repl functionality
  • [115] BTKAW76L rename
  • [116] 3GFQP6IR stop saving the entire file when modifying drawings
  • [117] WIDXZBNW experiment: extremely precise scrolling on paste
  • [118] 4C375P53 this is a bit clearer
  • [119] 7Q6GKOOL .
  • [120] 7M7LS7I2 start saving some settings to disk on quit
  • [121] 42LVB4DE test: naming a point
  • [122] VG75U7IM bugfix: typing should delete highlighted text
  • [123] G3C4FKPR bugfix: missed fixing a callsite
  • [124] NP7PIUBT bugfix: restore state after C-f (find)
  • [125] BZRRUIFQ correct location of the line width slider
  • [126] YIQYNVD2 rip out the line-width slider
  • [127] ZLJGZYQG select text with shift + mouseclick
  • [128] ESETRNLB bugfix: printing the first part of a line at the bottom made it seem non-wrapping
  • [129] HOSPP2AN crisp font rendering
  • [130] CCYSVZA2 bugfix: BSOD in #4.
  • [131] IRV65LZP fold variables for screen dimensions into the app framework
  • [132] R6GUSTBY default font size and line-height
  • [133] YJJ4X4JG bugfix: avoid scrolling on 'end'
  • [134] YPHKZVWM extract a new variable
  • [135] V5TP27FP ctrl-+ and ctrl-- to adjust font size
  • [136] U76D4P36 fix a typo
  • [137] 3TFEAQSW start using some globals
  • [138] 3QNOKBFM beginnings of a test harness
  • [139] 4VKEE43Z bugfix
  • [140] PX7DDEMO autosave slightly less aggressively
  • [141] PGZJ6NAT ensure Filename is writable when opened outside a terminal
  • [142] CE4LZV4T drop last couple of manual tests
  • [143] 65XHTZEK regression: couldn't do many drawing operations because line.y was reset
  • [144] DHCLUDCW .
  • [145] 7EQLPB3O bugfix: don't delete selection when moving cursor
  • [146] OYXDYPGS get rid of debug variables
  • [147] 5ZFHMYQI .
  • [148] 2ENZW7TV select text using mouse drag
  • [149] QZH3PQFU .
  • [150] Z4XRNDTR find text
  • [151] JVRL5TWL store device-independent coordinates inside drawings
  • [152] EDY3RQUL gracefully handle a non-existent filename at the commandline
  • [153] A2QPFRFJ move
  • [154] 3TTAYXPP cleanup
  • [155] CIQN2MDE bugfix: typing a capital letter deletes selection
  • [156] G77XIN7M selecting a stroke
  • [157] RT6EV6OP delegate update events to drawings
  • [158] T7SJSJIH test: undo naming a point
  • [159] IYW7X3WL left/right cursor movement, deleting characters
  • [160] XX7G2FFJ intermingle freehand line drawings with text
  • [161] 2H67P75X switch arg for a function
  • [162] B3IWYWSR delete another arg that can be deduced
  • [163] KVHUFUFV reorg
  • [164] LUNH47XX make text and drawings the same width
  • [165] Y6FTGOHJ simpler
  • [166] 73OCE2MC after much struggle, a brute-force undo
  • [167] 5DOTWNVM right margin
  • [168] CTJ3IZGS add args to some functions
  • [169] BULPIBEG beginnings of a module for the text editor
  • [170] AVTNUQYR basic test-enabled framework
  • [171] 7RN3AETY bugfix: text sometimes getting colored like drawing borders
  • [172] 2INHXC3K position cursor by clicking on text
  • [173] JFFUF5AL override mouse state lookups in tests
  • [174] 2RXZ3PGO beginning of a new approach to scroll+wrap
  • [175] 6PUNJS5B backspace
  • [176] 6J3NXBYG affordance to adjust width for word wrap
  • [177] 242L3OQX bugfix: ensure Cursor_line is always on a text line
  • [178] HRWN5V6J Devine's suggestion to try to live with just freehand
  • [179] 6DE7RBZ6 move mouse_released events to Drawing
  • [180] UWNHC4AA redo y computations
  • [181] KMRJOSLY bugfix: delete selection before pasting
  • [182] AQQQNDTL yet another bugfix in selection management
  • [183] LS55YKGW switch copy/paste to ctrl- hotkeys
  • [184] DAENUOGV eliminate assumptions that line length == size in bytes
  • [*] R5QXEHUI somebody stop me
  • [*] BJ5X5O4A let's prevent the text cursor from ever getting on a drawing

Change contents

  • edit in main.lua at line 5
    [16.763][16.18:19](),[16.18][16.18:19]()
  • edit in main.lua at line 6
    [16.21][16.3:18](),[16.18][16.2:19](),[16.21][16.2:19](),[16.19][15.31:79](),[15.79][16.47:78](),[16.47][16.47:78]()
    require 'file'
    require 'button'
    require 'text'
    require 'drawing'
    require 'geom'
    require 'help'
    require 'icons'
  • edit in main.lua at line 8
    [16.1187]
    [16.21]
    -- delegate most business logic to a layer that can be reused by other projects
    require 'edit'
  • replacement in main.lua at line 14
    [16.914][16.1:66](),[16.1566][16.1:66](),[16.22][16.1:66](),[16.66][16.4:44](),[16.44][16.1880:2049](),[16.2049][16.105:375](),[16.105][16.105:375](),[16.104][16.104:134](),[16.375][16.104:134](),[16.1787][16.104:134](),[16.104][16.104:134](),[16.134][16.105:189](),[16.189][16.163:500](),[16.163][16.163:500](),[16.547][2.2:101](),[2.101][16.643:878](),[16.643][16.643:878](),[16.1237][16.2:35](),[16.35][16.8451:8975](),[16.8975][16.1167:1273](),[16.1273][16.9024:9108](),[16.9024][16.9024:9108](),[16.9108][3.4:53](),[3.53][16.9108:9198](),[16.9108][16.9108:9198](),[16.9198][16.10923:10940](),[16.10940][16.1217:1343](),[16.1343][16.1391:1485](),[16.1718][16.1391:1485](),[16.10940][16.1391:1485](),[16.204][16.54:95](),[16.1206][16.54:95](),[16.1485][16.54:95](),[16.4217][16.54:95](),[16.10940][16.54:95](),[16.114][16.54:95](),[16.95][16.3:4](),[16.1241][16.79:137](),[16.86][16.1:2](),[16.255][16.1:2](),[16.1318][16.1:2](),[16.4239][16.1:2](),[16.313][16.1:2](),[16.2][16.5:25](),[16.25][16.4127:4161](),[16.2][16.4127:4161](),[16.4161][16.7:89](),[16.89][16.26:27](),[16.4161][16.26:27](),[16.27][16.1462:1478](),[16.4161][16.1462:1478](),[16.1478][16.2595:2612](),[16.2612][16.4602:4662](),[16.4662][16.329:453](),[16.2612][16.329:453](),[16.13][16.3:4](),[16.341][16.3:4](),[16.453][16.3:4](),[16.1478][16.3:4](),[16.2612][16.3:4](),[16.4161][16.3:4](),[16.21][16.3:4](),[16.4][16.3:63](),[16.63][16.137:153](),[16.153][16.1567:1568](),[16.63][16.1567:1568](),[16.1568][16.8125:8163](),[16.8163][16.2543:2590](),[16.2590][16.391:455]()
    -- a line is either text or a drawing
    -- a text is a table with:
    -- mode = 'text',
    -- string data,
    -- startpos, the index of data the line starts rendering from (if currently on screen), can only be >1 for topmost line on screen
    -- starty, the y coord in pixels
    -- some cached data that's blown away and recomputed when data changes:
    -- fragments: snippets of rendered love.graphics.Text, guaranteed to not wrap
    -- screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line
    -- 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=''}}
    -- Lines can be too long to fit on screen, in which case they _wrap_ into
    -- multiple _screen lines_.
    --
    -- Therefore, any potential location for the cursor can be described in two ways:
    -- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)
    -- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.
    --
    -- Most of the time we'll only persist positions in schema 1, translating to
    -- schema 2 when that's convenient.
    --
    -- Make sure these coordinates are never aliased, so that changing one causes
    -- action at a distance.
    Screen_top1 = {line=1, pos=1} -- position of start of screen line at top of screen
    Cursor1 = {line=1, pos=1} -- position of cursor
    Screen_bottom1 = {line=1, pos=1} -- position of start of screen line at bottom of screen
    Selection1 = {}
    Old_cursor1, Old_selection1, Mousepress_shift = nil -- some extra state to compute selection between mouse press and release
    Recent_mouse = {} -- when selecting text, avoid recomputing some state on every single frame
    Cursor_x, Cursor_y = 0, 0 -- in pixels
    Current_drawing_mode = 'line'
    Previous_drawing_mode = nil
    -- values for tests
    Font_height = 14
    Line_height = 15
    -- widest possible character width
    Em = App.newText(love.graphics.getFont(), 'm')
    Margin_top = 15
    Margin_left = 25
    Margin_right = 25
    Margin_width = Margin_left + Margin_right
    Drawing_padding_top = 10
    Drawing_padding_bottom = 10
    Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom
    Filename = love.filesystem.getUserDirectory()..'/lines.txt'
    Next_save = nil
    -- undo
    History = {}
    Next_history = 1
    -- search
    Search_term = nil
    Search_text = nil
    Search_backup = nil -- stuff to restore when cancelling search
    [16.1566]
    [16.162]
    return edit.initialize_globals()
    end
  • edit in main.lua at line 17
    [16.163][16.163:196](),[16.196][16.204:240](),[16.55][16.8163:8164](),[16.196][16.8163:8164](),[16.240][16.8163:8164](),[16.455][16.8163:8164](),[16.2017][16.8163:8164](),[16.2680][16.8163:8164](),[16.8163][16.8163:8164](),[16.8164][16.1568:1600](),[16.1568][16.1568:1600]()
    -- resize
    Last_resize_time = nil
    -- blinking cursor
    Cursor_time = 0
    end -- App.initialize_globals
  • replacement in main.lua at line 139
    [16.1373][16.3:26](),[16.209][16.3:26](),[16.336][16.260:261](),[16.261][16.658:692](),[16.692][16.1144:1218](),[16.261][16.1144:1218](),[16.274][16.5:46](),[16.448][16.5:46](),[16.1218][16.5:46](),[16.545][16.5:46](),[16.46][5.8:24](),[5.24][16.1479:1502](),[16.46][16.1479:1502](),[16.1502][16.58:81](),[16.18][16.58:81](),[16.81][16.8:89](),[16.89][16.82:126](),[16.44][16.82:126](),[16.126][16.4162:4220](),[16.4220][16.90:679](),[16.679][16.306:331](),[16.394][16.306:331](),[16.331][16.680:976](),[16.976][16.2189:2201](),[16.2189][16.2189:2201](),[16.2201][16.977:1371](),[16.108][16.1574:1584](),[16.183][16.1574:1584](),[16.264][16.1574:1584](),[16.328][16.1574:1584](),[16.1371][16.1574:1584](),[16.2159][16.1574:1584](),[16.2447][16.1574:1584](),[16.1574][16.1574:1584](),[16.1584][16.1372:1416](),[16.1416][10.576:691](),[10.691][16.1474:1527](),[16.1474][16.1474:1527](),[16.421][16.1294:1302](),[16.1527][16.1294:1302](),[16.1577][16.1294:1302](),[16.1584][16.1294:1302](),[16.1294][16.1294:1302](),[16.1302][5.25:89](),[5.89][16.410:416](),[16.119][16.410:416](),[16.1302][16.410:416](),[16.410][16.410:416](),[16.2178][16.408:520](),[16.2469][16.408:520](),[16.416][16.408:520](),[16.520][16.2820:2875]()
    Button_handlers = {}
    love.graphics.setColor(0, 0, 0)
    --? print(Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos)
    assert(Text.le1(Screen_top1, Cursor1))
    Cursor_y = -1
    local y = Margin_top
    --? print('== draw')
    for line_index = Screen_top1.line,#Lines do
    local line = Lines[line_index]
    --? print('draw:', y, line_index, line)
    if y + Line_height > App.screen.height then break end
    Screen_bottom1.line = line_index
    if line.mode == 'text' and line.data == '' then
    line.starty = y
    line.startpos = 1
    -- 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(line_index-1, line_index)
    table.insert(Lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})
    if Cursor1.line >= line_index then
    Cursor1.line = Cursor1.line+1
    end
    schedule_save()
    record_undo_event({before=Drawing.before, after=snapshot(line_index-1, line_index+1)})
    end
    })
    if Search_term == nil then
    if line_index == Cursor1.line then
    Text.draw_cursor(Margin_left, y)
    end
    end
    Screen_bottom1.pos = Screen_top1.pos
    y = y + Line_height
    elseif line.mode == 'drawing' then
    y = y+Drawing_padding_top
    line.y = y
    Drawing.draw(line)
    y = y + Drawing.pixels(line.h) + Drawing_padding_bottom
    else
    line.starty = y
    line.startpos = 1
    if line_index == Screen_top1.line then
    line.startpos = Screen_top1.pos
    end
    --? print('text.draw', y, line_index)
    y, Screen_bottom1.pos = Text.draw(line, line_index, line.starty, Margin_left, App.screen.width-Margin_right)
    y = y + Line_height
    --? print('=> y', y)
    end
    end
    if Cursor_y == -1 then
    Cursor_y = App.screen.height
    end
    --? print('screen bottom: '..tostring(Screen_bottom1.pos)..' in '..tostring(Lines[Screen_bottom1.line].data))
    if Search_term then
    Text.draw_search_bar()
    end
    [16.1373]
    [16.416]
    edit.draw()
  • replacement in main.lua at line 143
    [16.1398][16.241:274](),[16.274][16.449:512](),[16.1398][16.449:512](),[16.512][16.282:333](),[16.333][16.570:621](),[16.570][16.570:621](),[16.706][16.621:635](),[16.621][16.621:635](),[16.635][16.3:24](),[16.1398][16.3:24](),[16.446][16.3:24](),[16.24][16.334:444]()
    Cursor_time = Cursor_time + dt
    -- some hysteresis while resizing
    if Last_resize_time then
    if App.getTime() - Last_resize_time < 0.1 then
    return
    else
    Last_resize_time = nil
    end
    end
    Drawing.update(dt)
    if Next_save and Next_save < App.getTime() then
    save_to_disk(Lines, Filename)
    Next_save = nil
    end
    [16.1398]
    [16.3]
    edit.update(dt)
  • edit in main.lua at line 146
    [16.8][16.445:609]()
    function schedule_save()
    if Next_save == nil then
    Next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you did
    end
    end
  • replacement in main.lua at line 147
    [16.66][16.8:47](),[16.47][16.8:68](),[16.66][16.8:68]()
    -- make sure to save before quitting
    if Next_save then
    save_to_disk(Lines, Filename)
    end
    [16.66]
    [16.48]
    edit.quit()
  • replacement in main.lua at line 164
    [16.1444][16.2876:2909](),[16.2909][9.8:83](),[9.83][16.8:62](),[16.2909][16.8:62](),[16.62][16.49:99](),[16.1402][16.49:99](),[16.1444][16.49:99](),[16.2909][16.49:99](),[16.49][16.49:99](),[16.99][16.1:2](),[16.47][16.1:2](),[16.877][16.304:346](),[16.2][16.304:346](),[16.346][16.176:208](),[16.536][16.176:208](),[16.176][16.176:208](),[16.208][16.143:225](),[16.119][16.119:218](),[16.225][16.119:218](),[16.156][16.119:218](),[16.218][16.1811:2346](),[16.1811][16.1811:2346](),[16.2346][16.153:205](),[16.205][12.233:326](),[12.326][16.254:264](),[16.254][16.254:264](),[16.264][16.63:141](),[16.2422][16.63:141](),[16.141][16.361:410](),[16.292][16.361:410](),[16.538][16.361:410](),[16.769][16.361:410](),[16.1528][16.361:410](),[16.1640][16.361:410](),[16.1837][16.361:410](),[16.2015][16.361:410](),[16.2422][16.361:410](),[16.361][16.361:410](),[16.410][16.539:584](),[16.584][16.41:173](),[16.173][14.8:63](),[14.63][16.142:156](),[16.633][16.142:156](),[16.156][16.999:1023](),[16.569][16.999:1023](),[16.633][16.999:1023](),[16.999][16.999:1023]()
    if Search_term then return end
    -- ensure cursor is visible immediately after it moves
    Cursor_time = 0
    --? print('press', Selection1.line, Selection1.pos)
    propagate_to_button_handlers(x,y, mouse_button)
    for line_index,line in ipairs(Lines) do
    if line.mode == 'text' then
    if Text.in_line(line, x,y, Margin_left, App.screen.width-Margin_right) 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. mousereleased should never look at shift state
    Old_cursor1 = Cursor1
    Old_selection1 = Selection1
    Mousepress_shift = App.shift_down()
    Selection1 = {
    line=line_index,
    pos=Text.to_pos_on_line(line, x, y, Margin_left, App.screen.width-Margin_right),
    }
    --? print('selection', Selection1.line, Selection1.pos)
    break
    end
    elseif line.mode == 'drawing' then
    if Drawing.in_drawing(line, x, y) then
    Lines.current_drawing_index = line_index
    Lines.current_drawing = line
    Drawing.before = snapshot(line_index)
    Drawing.mouse_pressed(line, x,y, mouse_button)
    break
    end
    end
    end
    [16.1444]
    [16.446]
    return edit.mouse_pressed(x,y, mouse_button)
  • replacement in main.lua at line 168
    [14.110][16.2910:2943](),[16.1485][16.2910:2943](),[16.2943][16.1403:1426](),[16.1426][9.84:159](),[9.159][16.293:325](),[16.1426][16.293:325](),[16.2943][16.293:325](),[16.325][14.111:157](),[14.157][16.610:630](),[16.365][16.610:630](),[16.630][16.208:364](),[16.208][16.208:364](),[16.364][16.365:450](),[16.365][16.365:450](),[16.450][16.226:310](),[16.160][16.770:809](),[16.310][16.770:809](),[16.208][16.770:809](),[16.809][16.265:318](),[16.318][12.327:422](),[12.422][16.369:381](),[16.369][16.369:381](),[16.381][16.157:214](),[16.157][16.157:214](),[16.84][16.2423:2619](),[16.214][16.2423:2619](),[16.2140][16.2423:2619](),[16.157][16.2423:2619](),[16.2619][16.617:631](),[16.617][16.617:631](),[16.631][16.2620:2682](),[16.2682][16.219:303](),[16.303][16.215:231](),[16.2682][16.215:231](),[16.231][16.631:661](),[16.2682][16.631:661](),[16.631][16.631:661](),[16.661][16.232:293](),[16.293][16.661:667](),[16.1661][16.661:667](),[16.661][16.661:667]()
    if Search_term then return end
    --? print('release')
    -- ensure cursor is visible immediately after it moves
    Cursor_time = 0
    if Lines.current_drawing then
    Drawing.mouse_released(x,y, mouse_button)
    schedule_save()
    if Drawing.before then
    record_undo_event({before=Drawing.before, after=snapshot(Lines.current_drawing_index)})
    Drawing.before = nil
    end
    else
    for line_index,line in ipairs(Lines) do
    if line.mode == 'text' then
    if Text.in_line(line, x,y, Margin_left, App.screen.width-Margin_right) then
    --? print('reset selection')
    Cursor1 = {
    line=line_index,
    pos=Text.to_pos_on_line(line, x, y, Margin_left, App.screen.width-Margin_right),
    }
    --? print('cursor', Cursor1.line, Cursor1.pos)
    if Mousepress_shift then
    if Old_selection1.line == nil then
    Selection1 = Old_cursor1
    else
    Selection1 = Old_selection1
    end
    end
    Old_cursor1, Old_selection1, Mousepress_shift = nil
    if eq(Cursor1, Selection1) then
    Selection1 = {}
    end
    break
    end
    end
    end
    --? print('selection:', Selection1.line, Selection1.pos)
    end
    [14.110]
    [16.130]
    return edit.mouse_released(x,y, mouse_button)
  • replacement in main.lua at line 172
    [16.1512][9.160:235](),[9.235][16.1668:1745](),[16.1512][16.1668:1745](),[16.1745][16.2944:3089](),[16.1512][16.2944:3089](),[16.3089][4.9:66](),[4.66][16.5:47](),[16.3089][16.5:47](),[16.47][16.237:319](),[16.237][16.237:319](),[16.319][4.67:151](),[4.151][16.319:354](),[16.319][16.319:354](),[16.354][16.631:649]()
    -- ensure cursor is visible immediately after it moves
    Cursor_time = 0
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    if Search_term then
    Search_term = Search_term..t
    Search_text = nil
    Text.search_next()
    elseif Current_drawing_mode == 'name' then
    local before = snapshot(Lines.current_drawing_index)
    local drawing = Lines.current_drawing
    local p = drawing.points[drawing.pending.target_point]
    p.name = p.name..t
    record_undo_event({before=before, after=snapshot(Lines.current_drawing_index)})
    else
    Text.textinput(t)
    end
    schedule_save()
    [16.1512]
    [16.3116]
    return edit.textinput(t)
  • replacement in main.lua at line 176
    [16.836][9.236:311](),[9.311][16.836:861](),[16.836][16.836:861](),[16.861][8.8:44](),[8.44][16.861:1225](),[16.861][16.861:1225](),[16.1225][11.741:811](),[11.811][16.1316:1322](),[16.1316][16.1316:1322](),[16.1322][16.3090:3190](),[16.1823][16.3090:3190](),[16.1550][16.3090:3190](),[16.3190][16.456:564](),[16.564][16.17591:17683](),[16.564][16.3262:3344](),[16.17683][16.3262:3344](),[16.3262][16.3262:3344](),[16.3344][16.565:591](),[16.591][16.3344:3421](),[16.3344][16.3344:3421](),[16.3421][16.1543:1599](),[16.1599][16.3477:3654](),[16.3477][16.3477:3654](),[16.3654][16.1244:1303](),[16.1303][16.3654:3723](),[16.3654][16.3654:3723](),[16.3723][16.592:715](),[16.715][16.3788:3819](),[16.3788][16.3788:3819](),[16.961][16.4279:4308](),[16.3819][16.4279:4308](),[16.4308][16.445:489](),[16.174][16.4448:4470](),[16.489][16.4448:4470](),[16.4448][16.4448:4470](),[16.982][16.4470:4499](),[16.4470][16.4470:4499](),[16.4499][16.490:534](),[16.534][16.4593:4615](),[16.4593][16.4593:4615](),[16.221][16.4661:4690](),[16.1003][16.4661:4690](),[16.4661][16.4661:4690](),[16.4690][16.535:568](),[16.568][16.4773:4795](),[16.4773][16.4773:4795](),[16.1024][16.71:100](),[16.265][16.71:100](),[16.100][16.26:105](),[16.105][16.100:351](),[16.100][16.100:351](),[16.351][16.17684:17776](),[16.17776][16.650:672](),[16.43][16.351:359](),[16.672][16.351:359](),[16.17776][16.351:359](),[16.351][16.351:359](),[16.1045][16.359:388](),[16.359][16.359:388](),[16.388][16.106:185](),[16.185][16.388:638](),[16.388][16.388:638](),[16.638][16.17777:17869](),[16.17869][16.673:695](),[16.80][16.38:46](),[16.695][16.38:46](),[16.17869][16.38:46](),[16.638][16.38:46](),[16.1066][16.46:90](),[16.46][16.46:90](),[16.90][16.186:265](),[16.265][16.90:173](),[16.90][16.90:173](),[16.1087][16.173:202](),[16.173][16.173:202](),[16.202][16.266:345](),[16.345][11.812:889](),[11.889][16.237:289](),[16.237][16.237:289](),[16.289][16.696:716](),[16.42][16.289:318](),[16.716][16.289:318](),[16.1108][16.289:318](),[16.289][16.289:318](),[16.318][16.346:425](),[16.425][16.318:582](),[16.318][16.318:582](),[16.80][16.620:753](),[16.125][16.620:753](),[16.620][16.620:753](),[16.791][16.791:802](),[16.942][16.8:41](),[16.802][16.8:41](),[16.41][16.849:859](),[16.1256][16.849:859](),[16.849][16.849:859](),[16.859][16.126:134](),[16.134][16.593:638](),[16.638][13.664:752](),[16.328][16.638:646](),[16.434][16.638:646](),[13.752][16.638:646](),[16.1876][16.638:646](),[16.638][16.638:646](),[16.646][16.717:737](),[16.77][16.1877:1959](),[16.737][16.1877:1959](),[16.646][16.1877:1959](),[16.1129][16.1959:1992](),[16.1959][16.1959:1992](),[16.1992][16.1344:1402](),[16.1402][16.426:457](),[16.1407][16.426:457](),[16.3882][16.426:457](),[16.457][16.8:69](),[16.69][16.8:189](),[16.189][16.738:760](),[16.760][16.225:233](),[16.225][16.225:233](),[16.11][16.1458:1516](),[16.1516][16.74:187](),[16.1525][16.74:187](),[16.74][16.74:187](),[16.187][16.1585:1593](),[16.155][16.1585:1593](),[16.1593][16.387:556](),[16.556][16.218:277](),[16.277][16.48:92](),[16.556][16.48:92](),[16.92][16.592:706](),[16.592][16.592:706](),[16.706][16.532:620](),[16.620][16.706:782](),[16.706][16.706:782](),[16.782][16.1600:1655](),[16.1655][6.8:53](),[6.53][16.837:889](),[16.1655][16.837:889](),[16.837][16.837:889](),[16.889][16.621:709](),[16.709][16.889:899](),[16.889][16.889:899](),[16.364][16.899:907](),[16.899][16.899:907](),[16.907][16.761:781](),[16.163][16.5015:5022](),[16.781][16.5015:5022](),[16.1180][16.5015:5022](),[16.2194][16.5015:5022](),[16.4892][16.5015:5022](),[16.5015][16.5015:5022](),[16.5022][16.458:537](),[16.537][16.5022:5055](),[16.5022][16.5022:5055](),[16.293][16.350:356](),[16.925][16.350:356](),[16.5055][16.350:356](),[16.350][16.350:356]()
    -- ensure cursor is visible immediately after it moves
    Cursor_time = 0
    if Selection1.line and
    not Lines.current_drawing and
    -- printable character created using shift key => delete selection
    -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
    (not App.shift_down() or utf8.len(key) == 1) and
    chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and backspace ~= 'delete' and not App.is_cursor_movement(chord) then
    Text.delete_selection(Margin_left, App.screen.width-Margin_right)
    end
    if Search_term then
    if chord == 'escape' then
    Search_term = nil
    Search_text = nil
    Cursor1 = Search_backup.cursor
    Screen_top1 = Search_backup.screen_top
    Search_backup = nil
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
    elseif chord == 'return' then
    Search_term = nil
    Search_text = nil
    Search_backup = nil
    elseif chord == 'backspace' then
    local len = utf8.len(Search_term)
    local byte_offset = Text.offset(Search_term, len)
    Search_term = string.sub(Search_term, 1, byte_offset-1)
    Search_text = nil
    elseif chord == 'down' then
    Cursor1.pos = Cursor1.pos+1
    Text.search_next()
    elseif chord == 'up' then
    Text.search_previous()
    end
    return
    elseif chord == 'C-f' then
    Search_term = ''
    Search_backup = {cursor={line=Cursor1.line, pos=Cursor1.pos}, screen_top={line=Screen_top1.line, pos=Screen_top1.pos}}
    assert(Search_text == nil)
    elseif chord == 'C-=' then
    initialize_font_settings(Font_height+2)
    Text.redraw_all()
    elseif chord == 'C--' then
    initialize_font_settings(Font_height-2)
    Text.redraw_all()
    elseif chord == 'C-0' then
    initialize_font_settings(20)
    Text.redraw_all()
    elseif chord == 'C-z' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    local event = undo_event()
    if event then
    local src = event.before
    Screen_top1 = deepcopy(src.screen_top)
    Cursor1 = deepcopy(src.cursor)
    Selection1 = deepcopy(src.selection)
    patch(Lines, event.after, event.before)
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
    schedule_save()
    end
    elseif chord == 'C-y' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    local event = redo_event()
    if event then
    local src = event.after
    Screen_top1 = deepcopy(src.screen_top)
    Cursor1 = deepcopy(src.cursor)
    Selection1 = deepcopy(src.selection)
    patch(Lines, event.before, event.after)
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
    schedule_save()
    end
    -- clipboard
    elseif chord == 'C-c' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    local s = Text.selection()
    if s then
    App.setClipboardText(s)
    end
    elseif chord == 'C-x' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    local s = Text.cut_selection(Margin_left, App.screen.width-Margin_right)
    if s then
    App.setClipboardText(s)
    end
    schedule_save()
    elseif chord == 'C-v' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    -- We don't have a good sense of when to scroll, so we'll be conservative
    -- and sometimes scroll when we didn't quite need to.
    local before_line = Cursor1.line
    local before = snapshot(before_line)
    local clipboard_data = App.getClipboardText()
    for _,code in utf8.codes(clipboard_data) do
    local c = utf8.char(code)
    if c == '\n' then
    Text.insert_return()
    else
    Text.insert_at_cursor(c)
    end
    end
    if Text.cursor_past_screen_bottom() then
    Text.snap_cursor_to_bottom_of_screen(Margin_left, App.screen.height-Margin_right)
    end
    schedule_save()
    record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})
    -- dispatch to drawing or text
    elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
    -- DON'T reset line.y here
    local drawing_index, drawing = Drawing.current_drawing()
    if drawing_index then
    local before = snapshot(drawing_index)
    Drawing.keychord_pressed(chord)
    record_undo_event({before=before, after=snapshot(drawing_index)})
    schedule_save()
    end
    elseif chord == 'escape' and not App.mouse_down(1) then
    for _,line in ipairs(Lines) do
    if line.mode == 'drawing' then
    line.show_help = false
    end
    end
    elseif Current_drawing_mode == 'name' then
    if chord == 'return' then
    Current_drawing_mode = Previous_drawing_mode
    Previous_drawing_mode = nil
    else
    local before = snapshot(Lines.current_drawing_index)
    local drawing = Lines.current_drawing
    local p = drawing.points[drawing.pending.target_point]
    if chord == 'escape' then
    p.name = nil
    record_undo_event({before=before, after=snapshot(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({before=before, after=snapshot(Lines.current_drawing_index)})
    end
    end
    schedule_save()
    else
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    Text.keychord_pressed(chord)
    end
    [16.836]
    [16.356]
    return edit.keychord_pressed(chord, key)
  • edit in main.lua at line 180
    [16.1591]
    [16.327]
    return edit.key_released(key, scancode)
  • file addition: edit.lua (----------)
    [186.2]
    utf8 = require 'utf8'
    require 'file'
    require 'button'
    require 'text'
    require 'drawing'
    require 'geom'
    require 'help'
    require 'icons'
    edit = {}
    -- run in both tests and a real run
    function edit.initialize_globals()
    -- a line is either text or a drawing
    -- a text is a table with:
    -- mode = 'text',
    -- string data,
    -- startpos, the index of data the line starts rendering from (if currently on screen), can only be >1 for topmost line on screen
    -- starty, the y coord in pixels
    -- some cached data that's blown away and recomputed when data changes:
    -- fragments: snippets of rendered love.graphics.Text, guaranteed to not wrap
    -- screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line
    -- 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=''}}
    -- Lines can be too long to fit on screen, in which case they _wrap_ into
    -- multiple _screen lines_.
    --
    -- Therefore, any potential location for the cursor can be described in two ways:
    -- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)
    -- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.
    --
    -- Most of the time we'll only persist positions in schema 1, translating to
    -- schema 2 when that's convenient.
    --
    -- Make sure these coordinates are never aliased, so that changing one causes
    -- action at a distance.
    Screen_top1 = {line=1, pos=1} -- position of start of screen line at top of screen
    Cursor1 = {line=1, pos=1} -- position of cursor
    Screen_bottom1 = {line=1, pos=1} -- position of start of screen line at bottom of screen
    Selection1 = {}
    Old_cursor1, Old_selection1, Mousepress_shift = nil -- some extra state to compute selection between mouse press and release
    Recent_mouse = {} -- when selecting text, avoid recomputing some state on every single frame
    Cursor_x, Cursor_y = 0, 0 -- in pixels
    Current_drawing_mode = 'line'
    Previous_drawing_mode = nil
    -- values for tests
    Font_height = 14
    Line_height = 15
    -- widest possible character width
    Em = App.newText(love.graphics.getFont(), 'm')
    Margin_top = 15
    Margin_left = 25
    Margin_right = 25
    Margin_width = Margin_left + Margin_right
    Drawing_padding_top = 10
    Drawing_padding_bottom = 10
    Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom
    Filename = love.filesystem.getUserDirectory()..'/lines.txt'
    Next_save = nil
    -- undo
    History = {}
    Next_history = 1
    -- search
    Search_term = nil
    Search_text = nil
    Search_backup = nil -- stuff to restore when cancelling search
    -- resize
    Last_resize_time = nil
    -- blinking cursor
    Cursor_time = 0
    end -- App.initialize_globals
    function edit.draw()
    Button_handlers = {}
    love.graphics.setColor(0, 0, 0)
    --? print(Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos)
    assert(Text.le1(Screen_top1, Cursor1))
    Cursor_y = -1
    local y = Margin_top
    --? print('== draw')
    for line_index = Screen_top1.line,#Lines do
    local line = Lines[line_index]
    --? print('draw:', y, line_index, line)
    if y + Line_height > App.screen.height then break end
    Screen_bottom1.line = line_index
    if line.mode == 'text' and line.data == '' then
    line.starty = y
    line.startpos = 1
    -- 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(line_index-1, line_index)
    table.insert(Lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})
    if Cursor1.line >= line_index then
    Cursor1.line = Cursor1.line+1
    end
    schedule_save()
    record_undo_event({before=Drawing.before, after=snapshot(line_index-1, line_index+1)})
    end
    })
    if Search_term == nil then
    if line_index == Cursor1.line then
    Text.draw_cursor(Margin_left, y)
    end
    end
    Screen_bottom1.pos = Screen_top1.pos
    y = y + Line_height
    elseif line.mode == 'drawing' then
    y = y+Drawing_padding_top
    line.y = y
    Drawing.draw(line)
    y = y + Drawing.pixels(line.h) + Drawing_padding_bottom
    else
    line.starty = y
    line.startpos = 1
    if line_index == Screen_top1.line then
    line.startpos = Screen_top1.pos
    end
    --? print('text.draw', y, line_index)
    y, Screen_bottom1.pos = Text.draw(line, line_index, line.starty, Margin_left, App.screen.width-Margin_right)
    y = y + Line_height
    --? print('=> y', y)
    end
    end
    if Cursor_y == -1 then
    Cursor_y = App.screen.height
    end
    --? print('screen bottom: '..tostring(Screen_bottom1.pos)..' in '..tostring(Lines[Screen_bottom1.line].data))
    if Search_term then
    Text.draw_search_bar()
    end
    end
    function edit.update(dt)
    Cursor_time = Cursor_time + dt
    -- some hysteresis while resizing
    if Last_resize_time then
    if App.getTime() - Last_resize_time < 0.1 then
    return
    else
    Last_resize_time = nil
    end
    end
    Drawing.update(dt)
    if Next_save and Next_save < App.getTime() then
    save_to_disk(Lines, Filename)
    Next_save = nil
    end
    end
    function schedule_save()
    if Next_save == nil then
    Next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you did
    end
    end
    function edit.quit()
    -- make sure to save before quitting
    if Next_save then
    save_to_disk(Lines, Filename)
    end
    end
    function edit.mouse_pressed(x,y, mouse_button)
    if Search_term then return end
    -- ensure cursor is visible immediately after it moves
    Cursor_time = 0
    --? print('press', Selection1.line, Selection1.pos)
    propagate_to_button_handlers(x,y, mouse_button)
    for line_index,line in ipairs(Lines) do
    if line.mode == 'text' then
    if Text.in_line(line, x,y, Margin_left, App.screen.width-Margin_right) 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. mousereleased should never look at shift state
    Old_cursor1 = Cursor1
    Old_selection1 = Selection1
    Mousepress_shift = App.shift_down()
    Selection1 = {
    line=line_index,
    pos=Text.to_pos_on_line(line, x, y, Margin_left, App.screen.width-Margin_right),
    }
    --? print('selection', Selection1.line, Selection1.pos)
    break
    end
    elseif line.mode == 'drawing' then
    if Drawing.in_drawing(line, x, y) then
    Lines.current_drawing_index = line_index
    Lines.current_drawing = line
    Drawing.before = snapshot(line_index)
    Drawing.mouse_pressed(line, x,y, mouse_button)
    break
    end
    end
    end
    end
    function edit.mouse_released(x,y, mouse_button)
    if Search_term then return end
    --? print('release')
    -- ensure cursor is visible immediately after it moves
    Cursor_time = 0
    if Lines.current_drawing then
    Drawing.mouse_released(x,y, mouse_button)
    schedule_save()
    if Drawing.before then
    record_undo_event({before=Drawing.before, after=snapshot(Lines.current_drawing_index)})
    Drawing.before = nil
    end
    else
    for line_index,line in ipairs(Lines) do
    if line.mode == 'text' then
    if Text.in_line(line, x,y, Margin_left, App.screen.width-Margin_right) then
    --? print('reset selection')
    Cursor1 = {
    line=line_index,
    pos=Text.to_pos_on_line(line, x, y, Margin_left, App.screen.width-Margin_right),
    }
    --? print('cursor', Cursor1.line, Cursor1.pos)
    if Mousepress_shift then
    if Old_selection1.line == nil then
    Selection1 = Old_cursor1
    else
    Selection1 = Old_selection1
    end
    end
    Old_cursor1, Old_selection1, Mousepress_shift = nil
    if eq(Cursor1, Selection1) then
    Selection1 = {}
    end
    break
    end
    end
    end
    --? print('selection:', Selection1.line, Selection1.pos)
    end
    end
    function edit.textinput(t)
    -- ensure cursor is visible immediately after it moves
    Cursor_time = 0
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    if Search_term then
    Search_term = Search_term..t
    Search_text = nil
    Text.search_next()
    elseif Current_drawing_mode == 'name' then
    local before = snapshot(Lines.current_drawing_index)
    local drawing = Lines.current_drawing
    local p = drawing.points[drawing.pending.target_point]
    p.name = p.name..t
    record_undo_event({before=before, after=snapshot(Lines.current_drawing_index)})
    else
    Text.textinput(t)
    end
    schedule_save()
    end
    function edit.keychord_pressed(chord, key)
    -- ensure cursor is visible immediately after it moves
    Cursor_time = 0
    if Selection1.line and
    not Lines.current_drawing and
    -- printable character created using shift key => delete selection
    -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
    (not App.shift_down() or utf8.len(key) == 1) and
    chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and backspace ~= 'delete' and not App.is_cursor_movement(chord) then
    Text.delete_selection(Margin_left, App.screen.width-Margin_right)
    end
    if Search_term then
    if chord == 'escape' then
    Search_term = nil
    Search_text = nil
    Cursor1 = Search_backup.cursor
    Screen_top1 = Search_backup.screen_top
    Search_backup = nil
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
    elseif chord == 'return' then
    Search_term = nil
    Search_text = nil
    Search_backup = nil
    elseif chord == 'backspace' then
    local len = utf8.len(Search_term)
    local byte_offset = Text.offset(Search_term, len)
    Search_term = string.sub(Search_term, 1, byte_offset-1)
    Search_text = nil
    elseif chord == 'down' then
    Cursor1.pos = Cursor1.pos+1
    Text.search_next()
    elseif chord == 'up' then
    Text.search_previous()
    end
    return
    elseif chord == 'C-f' then
    Search_term = ''
    Search_backup = {cursor={line=Cursor1.line, pos=Cursor1.pos}, screen_top={line=Screen_top1.line, pos=Screen_top1.pos}}
    assert(Search_text == nil)
    elseif chord == 'C-=' then
    initialize_font_settings(Font_height+2)
    Text.redraw_all()
    elseif chord == 'C--' then
    initialize_font_settings(Font_height-2)
    Text.redraw_all()
    elseif chord == 'C-0' then
    initialize_font_settings(20)
    Text.redraw_all()
    elseif chord == 'C-z' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    local event = undo_event()
    if event then
    local src = event.before
    Screen_top1 = deepcopy(src.screen_top)
    Cursor1 = deepcopy(src.cursor)
    Selection1 = deepcopy(src.selection)
    patch(Lines, event.after, event.before)
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
    schedule_save()
    end
    elseif chord == 'C-y' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    local event = redo_event()
    if event then
    local src = event.after
    Screen_top1 = deepcopy(src.screen_top)
    Cursor1 = deepcopy(src.cursor)
    Selection1 = deepcopy(src.selection)
    patch(Lines, event.before, event.after)
    Text.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leaks
    schedule_save()
    end
    -- clipboard
    elseif chord == 'C-c' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    local s = Text.selection()
    if s then
    App.setClipboardText(s)
    end
    elseif chord == 'C-x' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    local s = Text.cut_selection(Margin_left, App.screen.width-Margin_right)
    if s then
    App.setClipboardText(s)
    end
    schedule_save()
    elseif chord == 'C-v' then
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    -- We don't have a good sense of when to scroll, so we'll be conservative
    -- and sometimes scroll when we didn't quite need to.
    local before_line = Cursor1.line
    local before = snapshot(before_line)
    local clipboard_data = App.getClipboardText()
    for _,code in utf8.codes(clipboard_data) do
    local c = utf8.char(code)
    if c == '\n' then
    Text.insert_return()
    else
    Text.insert_at_cursor(c)
    end
    end
    if Text.cursor_past_screen_bottom() then
    Text.snap_cursor_to_bottom_of_screen(Margin_left, App.screen.height-Margin_right)
    end
    schedule_save()
    record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})
    -- dispatch to drawing or text
    elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
    -- DON'T reset line.y here
    local drawing_index, drawing = Drawing.current_drawing()
    if drawing_index then
    local before = snapshot(drawing_index)
    Drawing.keychord_pressed(chord)
    record_undo_event({before=before, after=snapshot(drawing_index)})
    schedule_save()
    end
    elseif chord == 'escape' and not App.mouse_down(1) then
    for _,line in ipairs(Lines) do
    if line.mode == 'drawing' then
    line.show_help = false
    end
    end
    elseif Current_drawing_mode == 'name' then
    if chord == 'return' then
    Current_drawing_mode = Previous_drawing_mode
    Previous_drawing_mode = nil
    else
    local before = snapshot(Lines.current_drawing_index)
    local drawing = Lines.current_drawing
    local p = drawing.points[drawing.pending.target_point]
    if chord == 'escape' then
    p.name = nil
    record_undo_event({before=before, after=snapshot(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({before=before, after=snapshot(Lines.current_drawing_index)})
    end
    end
    schedule_save()
    else
    for _,line in ipairs(Lines) do line.y = nil end -- just in case we scroll
    Text.keychord_pressed(chord)
    end
    end
    function edit.key_released(key, scancode)
    end
  • edit in Manual_tests.md at line 26
    [16.1589]
    [7.220]
    * My test harness automatically runs `test_*` methods -- but only at the
    top-level. I wish there was a way to raise warnings if someone defines such
    a function inside a dict somewhere.