Merge lines.love

[?]
Sep 16, 2023, 6:44 AM
OWE64YJ27IFNSGLLWIGYBXRNJ4XU55UYUINAHLOWG2LFEYJM73QAC

Dependencies

  • [2] 6ECYOEHY bugfix: obsolete location for attribute
  • [3] ELJNEPW2 simplify cursor-on-screen check
  • [4] PV2YA7KS subsection headings in a long switch
  • [5] APX2PY6G stop tracking wallclock time
  • [6] RRDO6H7H bugfix
  • [7] 5ITAXPEP wait a little to flush disk before quitting
  • [8] YRJFJNUD bugfix
  • [9] O4RRXNOK bugfix: disallow font size of 0
  • [10] TXI6GSQD some minor cleanup
  • [11] ZWDTEUH7 clean up some absolute coordinates
  • [12] WK6UK5AJ enhance bugfix of commit a9aa3436f (Dec 2024)
  • [13] 6XCJX4DZ bugfix: inscript's bug
  • [14] 5SM6DRHK port inscript's bugfix to source editor
  • [15] 2QQNDEWT Merge lines.love
  • [16] NPXYCSEA Merge lines.love
  • [17] FPY4LO2W make a few names consistent with snake_case
  • [18] NVSC4N4K change a helper slightly
  • [19] 2MGBV7NP bugfix: crash when using mouse wheel
  • [20] J5NTUS2G source: show file being edited in window title bar
  • [21] MRA2Y3EE idea: set recent_mouse on mouse events
  • [22] QQBP3G6W return height of editor widget after drawing
  • [23] TGZAJUEF bring back a set of constants
  • [24] 5BMR5HRT click to the left of a line
  • [25] SPNMXTYR have file API operate on state object
  • [26] UH4YWHW5 button framework is at the app level
  • [27] FKNXK2OA switch to line index in a function
  • [28] 4KC7I3E2 make colors easier to edit
  • [29] PTDO2SOT add state arg to schedule_save
  • [30] 3MAZEQK5 add state arg to Text.textinput
  • [31] 52ZZ5TIE switch to line index in a function
  • [32] LF7BWEG4 group all editor globals
  • [33] KKMFQDR4 editing source code from within the app
  • [34] UHB4GARJ left/right margin -> left/right coordinates
  • [35] X3CQLBTR set window title within each app
  • [36] X3F7ECSL add state arg to some functions
  • [37] OGD5RAQK bugfix: naming points in drawings
  • [38] C4VTBATA .
  • [39] Z3BQO2RK typo
  • [40] GNKUD23I get rid of recent_mouse
  • [41] 2Y7YH7UP infrastructure for caching LÖVE text objects
  • [42] 6RYLD5ON change how we handle clicks above top margin
  • [43] 2WGHUWE6 self-documenting 0 Test_right_margin
  • [44] ILOA5BYF separate data structure for each line's cache data
  • [45] PP2IIHL6 stop putting button state in a global
  • [46] 7JH2ZT3F add state arg to Drawing.draw
  • [47] KYNGDE2C consistent names in a few more places
  • [48] AU424KN7 Merge lines.love
  • [49] 7VGDIPLC more robust state validation
  • [50] GN3C6AGM bugfix in changing shape mid-stroke
  • [51] H6QZ7GRR more precise name
  • [52] QXVD2RIF add state arg to Drawing.mouse_released
  • [53] 2L5MEZV3 experiment: new edit namespace
  • [54] PK5U572C drop some extra args
  • [55] BH7BT36L ctrl+a: select entire buffer
  • [56] 6D5MOJS4 allow buttons to interrupt events
  • [57] GZ5WULJV switch source side to new screen-line-based render
  • [58] YFW4MNNP handle wrapping lines
  • [59] LDFXFRUO bring a few things in sync between run and source
  • [60] OMLASW7K experiment at avoiding some merge conflicts
  • [61] TGHAJBES use line cache for drawings as well
  • [62] PJEQCTBL add state arg to Drawing.update
  • [63] MTJEVRJR add state arg to a few functions
  • [64] SPSW74Y5 add state arg to Text.keychord_pressed
  • [65] IFTYOERM line.y -> line_cache.starty in a few more places
  • [66] PWDBOOWJ copying to clipboard can never scroll
  • [67] 3QQZ7W4E bring couple more globals back to the app level
  • [68] APYPFFS3 call edit rather than App callbacks in tests
  • [69] DLQAEAC7 add state arg to Drawing.mouse_pressed
  • [70] BW2IUB3K keep all text cache writes inside text.lua
  • [71] WJBZZQE4 fold together two largely similar cases
  • [72] WLJCIXYM add state arg to a few functions
  • [73] ERQKFTPV extract method
  • [74] OWYI7OJT Merge upstream
  • [75] 2CK5QI7W make love event names consistent
  • [76] WLWNS6FB a bug I've never run into
  • [77] KTZQ57HV replace globals with args in a few functions
  • [78] CNCYMM6A make test initializations a little more obvious
  • [79] Z3IQ6A4R bugfix
  • [80] Z5WOBP27 Merge lines.love
  • [81] FTB4YNQU Merge lines.love
  • [82] QJISOCHJ some temporary logging to catch a bug
  • [83] 7FPELAZB ah, I see the problem
  • [84] 3OTESDW6 move drawing.starty into line cache
  • [85] 4GYPLUDY streamline the interface for Text.draw
  • [86] S2MISTTM add state arg to a few functions
  • [87] OI4FPFIN support drawings in the source editor
  • [88] VN3MTXFK Merge lines.love
  • [89] ISOFHXB2 App.width can no longer take a Text
  • [90] Z5HLXU4P add state arg to a few functions
  • [91] K6DTOGOQ flip return value of button handlers
  • [92] LNUHQOGH start passing in Editor_state explicitly
  • [93] GFXWHTE6 mouse wheel support
  • [94] 5UKUADTW distinguish consistently between mouse buttons and other buttons
  • [95] 4EGQRXDA bugfix: naming points
  • [96] SW7BSBMJ several bugfixes in saving/loading cursor position
  • [97] QCPXQ2E3 add state arg to a few functions
  • [98] 3HVBAZPA add state arg to a few functions
  • [99] 5Y24ZDZI bugfix
  • [100] 23MA4T3G add state arg to Drawing.keychord_pressed
  • [101] 5ZA3BRNY add state arg to a few functions
  • [102] AF253GHL bugfix

Change contents

  • edit in source.lua at line 77
    [21.104][21.12:60](),[21.104][20.13:85]()
    love.window.setTitle('capture.love - source')
    love.window.setTitle('lines.love - source - '..Editor_state.filename)
  • resurrect zombie in source.lua at line 77
    [21.60][21.105:108](),[20.85][21.105:108](),[21.57][21.105:108](),[21.57][21.105:108]()
  • edit in source.lua at line 77
    [21.104]
    [21.105]
    love.window.setTitle('capture.love - source - '..Editor_state.filename)
  • edit in edit.lua at line 109
    [21.472][18.345:390](),[21.472][18.345:390]()
    edit.put_cursor_on_next_text_line(State)
  • replacement in edit.lua at line 118
    [21.406][21.406:452]()
    edit.put_cursor_on_first_text_line(State)
    [21.406]
    [21.452]
    edit.put_cursor_on_next_text_line(State)
  • edit in edit.lua at line 142
    [21.642]
    [21.642]
    end
    function edit.put_cursor_on_next_text_line(State)
    while true do
    if State.cursor1.line >= #State.lines then
    break
    end
    if State.lines[State.cursor1.line].mode == 'text' then
    break
    end
    State.cursor1.line = State.cursor1.line+1
    State.cursor1.pos = 1
    end
  • replacement in edit.lua at line 157
    [21.647][21.647:737]()
    function edit.put_cursor_on_first_text_line(State)
    for i,line in ipairs(State.lines) do
    [21.647]
    [21.737]
    -- return y drawn until
    function edit.draw(State)
    State.button_handlers = {}
    App.color(Text_color)
    if #State.lines ~= #State.line_cache then
    print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
    assert(false)
    end
    if not Text.le1(State.screen_top1, State.cursor1) then
    print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
    assert(false)
    end
    State.cursor_x = nil
    State.cursor_y = nil
    local y = State.top
    local screen_bottom1 = {line=nil, pos=nil}
    --? print('== draw')
    for line_index = State.screen_top1.line,#State.lines do
    local line = State.lines[line_index]
    --? print('draw:', y, line_index, line)
    if y + State.line_height > App.screen.height then break end
    screen_bottom1.line = line_index
    if line.mode == 'text' then
    --? print('text.draw', y, line_index)
    local startpos = 1
    if line_index == State.screen_top1.line then
    startpos = State.screen_top1.pos
    end
    if line.data == '' then
    -- button to insert new drawing
    button(State, 'draw', {x=State.left-Margin_left+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, screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
    --? print('=> y', y)
    elseif line.mode == 'drawing' then
    y = y+Drawing_padding_top
    Drawing.draw(State, line_index, y)
    y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom
    else
    print(line.mode)
    assert(false)
    end
    end
    State.screen_bottom1 = screen_bottom1
    if State.search_term then
    Text.draw_search_bar(State)
    end
    return y
    end
    function edit.update(State, dt)
    Drawing.update(State, dt)
    if State.next_save and State.next_save < Current_time then
    save_to_disk(State)
    State.next_save = nil
    end
    end
    function schedule_save(State)
    if State.next_save == nil then
    State.next_save = Current_time + 3 -- short enough that you're likely to still remember what you did
    end
    end
    function edit.quit(State)
    -- make sure to save before quitting
    if State.next_save then
    save_to_disk(State)
    -- give some time for the OS to flush everything to disk
    love.timer.sleep(0.1)
    end
    end
    function edit.mouse_press(State, x,y, mouse_button)
    if State.search_term then return end
    --? print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then
    -- press on a button and it returned 'true' to short-circuit
    return
    end
    if y < State.top then
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=State.screen_top1.line,
    pos=State.screen_top1.pos,
    }
    return
    end
    for line_index,line in ipairs(State.lines) do
  • replacement in edit.lua at line 263
    [21.769][21.769:819]()
    State.cursor1 = {line=i, pos=1}
    break
    [21.769]
    [21.819]
    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_release should never look at shift state
    --? print_and_log(('edit.mouse_press: in line %d'):format(line_index))
    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),
    }
    return
    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_press(State, line_index, x,y, mouse_button)
    return
    end
    end
    end
    -- still here? click is below all screen lines
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=State.screen_bottom1.line,
    pos=Text.pos_at_end_of_screen_line(State, State.screen_bottom1),
    }
    end
    function edit.mouse_release(State, x,y, mouse_button)
    if State.search_term then return end
    --? print_and_log(('edit.mouse_release: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    if State.lines.current_drawing then
    Drawing.mouse_release(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
    --? print_and_log('edit.mouse_release: no current drawing')
    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_and_log(('edit.mouse_release: in line %d'):format(line_index))
    State.cursor1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    --? print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    if State.mousepress_shift then
    if State.old_selection1.line == nil then
    State.selection1 = State.old_cursor1
    else
    State.selection1 = State.old_selection1
    end
    end
    State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
    if eq(State.cursor1, State.selection1) then
    State.selection1 = {}
    end
    break
    end
    end
    end
    --? print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos))
    end
    end
    function edit.mouse_wheel_move(State, dx,dy)
    if dy > 0 then
    State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
    edit.put_cursor_on_next_text_line(State)
    for i=1,math.floor(dy) do
    Text.up(State)
    end
    elseif dy < 0 then
    State.cursor1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
    edit.put_cursor_on_next_text_line(State)
    for i=1,math.floor(-dy) do
    Text.down(State)
    end
    end
    end
    function edit.text_input(State, t)
    --? print('text input', t)
    if State.search_term then
    State.search_term = State.search_term..t
    Text.search_next(State)
    elseif State.lines.current_drawing and State.current_drawing_mode == 'name' then
    local before = snapshot(State, State.lines.current_drawing_index)
    local drawing = State.lines.current_drawing
    local p = drawing.points[drawing.pending.target_point]
    p.name = p.name..t
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    else
    local drawing_index, drawing = Drawing.current_drawing(State)
    if drawing_index == nil then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    Text.text_input(State, t)
    end
    end
    schedule_save(State)
    end
    function edit.keychord_press(State, chord, key)
    if State.selection1.line and
    not State.lines.current_drawing and
    -- printable character created using shift key => delete selection
    -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
    (not App.shift_down() or utf8.len(key) == 1) and
    chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and chord ~= 'C-y' and not App.is_cursor_movement(chord) then
    Text.delete_selection(State, State.left, State.right)
    end
    if State.search_term then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    if chord == 'escape' then
    State.search_term = nil
    State.cursor1 = State.search_backup.cursor
    State.screen_top1 = State.search_backup.screen_top
    State.search_backup = nil
    Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
    elseif chord == 'return' then
    State.search_term = nil
    State.search_backup = nil
    elseif chord == 'backspace' then
    local len = utf8.len(State.search_term)
    local byte_offset = Text.offset(State.search_term, len)
    State.search_term = string.sub(State.search_term, 1, byte_offset-1)
    elseif chord == 'down' then
    State.cursor1.pos = State.cursor1.pos+1
    Text.search_next(State)
    elseif chord == 'up' then
    Text.search_previous(State)
    end
    return
    elseif chord == 'C-f' then
    State.search_term = ''
    State.search_backup = {
    cursor={line=State.cursor1.line, pos=State.cursor1.pos},
    screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},
    }
    -- zoom
    elseif chord == 'C-=' then
    edit.update_font_settings(State, State.font_height+2)
    Text.redraw_all(State)
    elseif chord == 'C--' then
    if State.font_height > 2 then
    edit.update_font_settings(State, State.font_height-2)
    Text.redraw_all(State)
    end
    elseif chord == 'C-0' then
    edit.update_font_settings(State, 20)
    Text.redraw_all(State)
    -- undo
    elseif chord == 'C-z' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local event = undo_event(State)
    if event then
    local src = event.before
    State.screen_top1 = deepcopy(src.screen_top)
    State.cursor1 = deepcopy(src.cursor)
    State.selection1 = deepcopy(src.selection)
    patch(State.lines, event.after, event.before)
    patch_placeholders(State.line_cache, event.after, event.before)
    -- invalidate various cached bits of lines
    State.lines.current_drawing = nil
    -- if we're scrolling, reclaim all fragments to avoid memory leaks
    Text.redraw_all(State)
    schedule_save(State)
    end
    elseif chord == 'C-y' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local event = redo_event(State)
    if event then
    local src = event.after
    State.screen_top1 = deepcopy(src.screen_top)
    State.cursor1 = deepcopy(src.cursor)
    State.selection1 = deepcopy(src.selection)
    patch(State.lines, event.before, event.after)
    -- invalidate various cached bits of lines
    State.lines.current_drawing = nil
    -- if we're scrolling, reclaim all fragments to avoid memory leaks
    Text.redraw_all(State)
    schedule_save(State)
    end
    -- clipboard
    elseif chord == 'C-a' then
    State.selection1 = {line=1, pos=1}
    State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1}
    elseif chord == 'C-c' then
    local s = Text.selection(State)
    if s then
    App.set_clipboard(s)
    end
    elseif chord == 'C-x' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local s = Text.cut_selection(State, State.left, State.right)
    if s then
    App.set_clipboard(s)
    end
    schedule_save(State)
    elseif chord == 'C-v' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = 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 = State.cursor1.line
    local before = snapshot(State, before_line)
    local clipboard_data = App.get_clipboard()
    for _,code in utf8.codes(clipboard_data) do
    local c = utf8.char(code)
    if c == '\n' then
    Text.insert_return(State)
    else
    Text.insert_at_cursor(State, c)
    end
    end
    if Text.cursor_out_of_screen(State) then
    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
    end
    schedule_save(State)
    record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
    -- dispatch to drawing or text
    elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
    -- DON'T reset line_cache.starty here
    local drawing_index, drawing = Drawing.current_drawing(State)
    if drawing_index then
    local before = snapshot(State, drawing_index)
    Drawing.keychord_press(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
  • edit in edit.lua at line 515
    [21.827]
    [21.827]
    elseif State.lines.current_drawing and 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)
    if len > 0 then
    local byte_offset = Text.offset(p.name, len-1)
    if len == 1 then byte_offset = 0 end
    p.name = string.sub(p.name, 1, byte_offset)
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    end
    end
    end
    schedule_save(State)
    else
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    Text.keychord_press(State, chord)
  • edit in edit.lua at line 541
    [21.833]
    [21.833]
    end
    function edit.key_release(State, key, scancode)
    end
    function edit.update_font_settings(State, font_height)
    State.font_height = font_height
    love.graphics.setFont(love.graphics.newFont(State.font_height))
    State.line_height = math.floor(font_height*1.3)
    end
    --== some methods for tests
    -- Insulate tests from some key globals so I don't have to change the vast
    -- majority of tests when they're modified for the real app.
    Test_margin_left = 25
    Test_margin_right = 0
    function edit.initialize_test_state()
    -- if you change these values, tests will start failing
    return edit.initialize_state(
    15, -- top margin
    Test_margin_left,
    App.screen.width - Test_margin_right,
    14, -- font height assuming default LÖVE font
    15) -- line height
  • replacement in edit.lua at line 569
    [21.838][15.29:29](),[21.2727][21.15:44](),[21.22445][13.1951:1996](),[21.609][21.1896:1966](),[21.470][7.26:113](),[21.5449][14.6169:6248](),[21.5593][21.170:199](),[21.6985][8.134:241](),[21.6870][4.26:36](),[21.8139][21.1967:2037](),[15.29][21.28:52](),[21.366][21.28:52](),[21.361][18.513:660](),[21.361][18.513:660](),[21.795][18.391:512](),[21.795][18.391:512](),[21.795][18.391:512](),[16.498][21.364:368](),[21.258][21.364:368](),[21.7283][16.498:498](),[21.258][21.7264:7283](),[21.7264][21.7264:7283](),[21.7264][21.234:258](),[21.567][21.7237:7264](),[21.6186][21.7237:7264](),[21.7237][21.7237:7264](),[21.519][21.6139:6186](),[21.7099][21.411:519](),[21.233][21.7075:7099](),[21.7075][21.7075:7099](),[21.7075][21.209:233](),[21.410][21.7048:7075](),[21.880][21.7048:7075](),[21.7048][21.7048:7075](),[21.363][21.835:880](),[21.6915][21.259:363](),[21.208][21.6891:6915](),[21.6891][21.6891:6915](),[21.6891][21.184:208](),[21.258][21.6864:6891](),[21.6138][21.6864:6891](),[21.6864][21.6864:6891](),[21.210][21.6091:6138](),[21.834][21.166:210](),[21.166][21.166:210](),[21.119][21.789:834](),[21.6651][21.15:119](),[21.183][21.6627:6651](),[21.6627][21.6627:6651](),[21.6627][21.159:183](),[21.6090][21.6600:6627](),[21.6600][21.6600:6627](),[21.6528][21.6021:6090](),[21.6020][21.6481:6528](),[21.6481][21.6481:6528](),[21.6451][21.5989:6020](),[21.16047][21.6450:6451](),[21.6449][21.16043:16047](),[21.9642][21.16043:16047](),[21.16043][21.16043:16047](),[21.158][21.6430:6449](),[21.6430][21.6430:6449](),[21.6430][21.134:158](),[21.5988][21.6403:6430](),[21.6403][21.6403:6430](),[21.6268][21.5854:5988](),[21.5853][21.6228:6268](),[21.6228][21.6228:6268](),[21.6184][21.5808:5853](),[21.5729][21.6183:6184](),[21.6183][21.6183:6184](),[21.1119][21.5645:5729](),[21.5645][21.5645:5729](),[21.5600][21.1075:1119](),[21.1074][21.5422:5600](),[21.5422][21.5422:5600](),[21.5422][21.1052:1074](),[21.1051][21.5400:5422](),[21.5400][21.5400:5422](),[21.5400][21.915:1051](),[21.447][21.5371:5400](),[21.6146][21.5371:5400](),[21.329][21.442:447](),[21.442][21.442:447](),[6.92][21.337:387](),[21.337][21.337:387](),[21.264][6.26:92](),[21.6146][21.175:264](),[21.5807][21.6141:6146](),[21.9642][21.6141:6146](),[21.16001][21.5759:5807](),[21.5758][21.15990:16001](),[21.7139][21.15990:16001](),[21.15990][21.15990:16001](),[21.1400][21.5720:5758](),[21.15878][21.1293:1400](),[21.631][21.15871:15878](),[21.15871][21.15871:15878](),[21.15851][21.606:631](),[21.330][21.15833:15851](),[21.3243][21.15833:15851](),[21.9506][21.15833:15851](),[21.105844][21.15833:15851](),[21.15833][21.15833:15851](),[21.15593][21.26:330](),[21.3134][21.15517:15593](),[21.9411][21.15517:15593](),[21.105742][21.15517:15593](),[21.15517][21.15517:15593](),[21.15429][21.3026:3134](),[21.9316][21.15315:15429](),[21.105640][21.15315:15429](),[21.15315][21.15315:15429](),[21.3025][21.9266:9316](),[21.9266][21.9266:9316](),[21.15212][21.2953:3025](),[21.9200][21.15203:15212](),[21.105510][21.15203:15212](),[21.15203][21.15203:15212](),[21.15118][21.9097:9200](),[21.367][21.15088:15118](),[21.9096][21.15088:15118](),[21.105385][21.15088:15118](),[21.15088][21.15088:15118](),[21.15043][21.284:367](),[21.9044][21.14957:15043](),[21.105326][21.14957:15043](),[21.14957][21.14957:15043](),[21.14922][21.9003:9044](),[21.605][21.14856:14922](),[21.14856][21.14856:14922](),[21.2952][21.578:605](),[21.14834][21.578:605](),[21.5719][21.2866:2952](),[21.60][21.2866:2952](),[21.2865][21.5676:5719](),[21.14679][21.2813:2865](),[21.81][21.14653:14679](),[21.14653][21.14653:14679](),[21.1292][21.15:81](),[21.14592][21.15:81](),[21.14561][21.1250:1292](),[21.2812][21.14470:14561](),[21.9002][21.14470:14561](),[21.105277][21.14470:14561](),[21.14470][21.14470:14561](),[21.577][21.2710:2812](),[21.14368][21.552:577](),[21.1438][21.14360:14368](),[21.8913][21.14360:14368](),[21.23116][21.14360:14368](),[21.105181][21.14360:14368](),[21.14360][21.14360:14368](),[3.360][21.23041:23116](),[21.1331][21.23041:23116](),[21.14227][3.315:360](),[21.8706][21.14209:14227](),[21.14209][21.14209:14227](),[21.14176][21.8666:8706](),[21.8665][21.14165:14176](),[21.14165][21.14165:14176](),[21.14136][21.8631:8665](),[17.454][21.14032:14136](),[21.14032][21.14032:14136](),[21.2709][17.407:454](),[21.8812][21.2661:2709](),[21.13904][21.8769:8812](),[2.230][21.13768:13904](),[21.1249][21.13768:13904](),[21.8768][21.13768:13904](),[21.105015][21.13768:13904](),[21.13768][21.13768:13904](),[21.13689][2.123:230](),[21.551][21.13660:13689](),[21.13660][21.13660:13689](),[21.13640][21.526:551](),[17.406][21.13632:13640](),[21.13632][21.13632:13640](),[21.13602][17.379:406](),[21.3962][21.13588:13602](),[21.8682][21.13588:13602](),[21.23040][21.13588:13602](),[21.104922][21.13588:13602](),[21.13588][21.13588:13602](),[21.1158][21.22975:23040](),[21.8593][21.22975:23040](),[21.13432][21.1051:1158](),[17.378][21.13395:13432](),[21.13395][21.13395:13432](),[21.13365][17.351:378](),[21.3865][21.13351:13365](),[21.13351][21.13351:13365](),[21.1050][21.3829:3865](),[21.8507][21.3829:3865](),[21.657][21.13212:13241](),[21.13212][21.13212:13241](),[21.13212][21.501:657](),[21.525][21.13189:13212](),[21.13189][21.13189:13212](),[21.398][21.498:525](),[21.7066][21.498:525](),[21.13167][21.498:525](),[21.8421][21.207:398](),[21.12904][21.8226:8421](),[21.2660][21.12856:12904](),[21.12856][21.12856:12904](),[21.942][21.2624:2660](),[21.8225][21.2624:2660](),[21.12746][21.835:942](),[21.497][21.12709:12746](),[21.12709][21.12709:12746](),[21.206][21.470:497](),[21.6968][21.470:497](),[21.12687][21.470:497](),[21.2037][21.15:206](),[21.8570][21.15:206](),[21.8139][21.15:206](),[21.12424][21.7944:8139](),[21.2623][21.12375:12424](),[21.12375][21.12375:12424](),[2.122][21.2587:2623](),[21.834][21.2587:2623](),[21.7943][21.2587:2623](),[21.12265][2.15:122](),[4.36][21.12236:12265](),[21.6870][21.12236:12265](),[21.12236][21.12236:12265](),[21.174][21.6843:6870](),[21.12214][21.6843:6870](),[21.12181][21.133:174](),[9.289][21.12152:12181](),[21.6842][21.12152:12181](),[21.12152][21.12152:12181](),[21.12086][9.158:289](),[21.6814][21.12057:12086](),[21.12057][21.12057:12086](),[21.73][21.6787:6814](),[21.7806][21.6787:6814](),[21.11991][21.15:73](),[4.25][21.11962:11991](),[21.7755][21.11962:11991](),[21.103883][21.11962:11991](),[21.11962][21.11962:11991](),[21.7755][4.15:25](),[21.7565][21.25240:25412](),[21.11787][21.7538:7565](),[21.2183][21.11739:11787](),[21.11739][21.11739:11787](),[21.11710][21.2149:2183](),[21.2148][21.11680:11710](),[21.11680][21.11680:11710](),[21.7537][21.2118:2148](),[21.11621][21.7491:7537](),[21.7490][21.11589:11621](),[21.103555][21.11589:11621](),[21.11589][21.11589:11621](),[21.11407][21.7278:7460](),[21.7277][21.11370:11407](),[21.103307][21.11370:11407](),[21.11370][21.11370:11407](),[21.7245][21.7245:7277](),[21.11296][21.7185:7215](),[21.6786][21.11262:11296](),[21.11262][21.11262:11296](),[21.7184][21.6689:6786](),[21.7046][21.7046:7184](),[21.11014][21.6986:7016](),[8.241][21.10984:11014](),[21.6985][21.10984:11014](),[21.102945][21.10984:11014](),[21.10984][21.10984:11014](),[21.10962][21.6957:6985](),[21.3828][21.10956:10962](),[21.6956][21.10956:10962](),[21.22974][21.10956:10962](),[21.102909][21.10956:10962](),[21.10956][21.10956:10962](),[21.325][21.22916:22974](),[21.363][21.22916:22974](),[12.401][21.22916:22974](),[21.500][21.22916:22974](),[21.10886][21.22916:22974](),[21.10752][12.214:401](),[21.6873][21.10522:10752](),[21.102812][21.10522:10752](),[21.10522][21.10522:10752](),[21.5675][21.6800:6873](),[21.6800][21.6800:6873](),[21.10343][21.5627:5675](),[21.469][21.10338:10343](),[21.10338][21.10338:10343](),[21.10320][21.446:469](),[21.273][21.10314:10320](),[21.806][21.10314:10320](),[21.10314][21.10314:10320](),[21.5626][21.265:273](),[21.265][21.265:273](),[21.234][21.5594:5626](),[21.10292][21.26:234](),[21.2586][21.10285:10292](),[21.6749][21.10285:10292](),[21.102724][21.10285:10292](),[21.10285][21.10285:10292](),[21.10201][21.2482:2586](),[21.6658][21.10119:10201](),[21.102626][21.10119:10201](),[21.10119][21.10119:10201](),[21.2481][21.6610:6658](),[21.6610][21.6610:6658](),[21.283][21.2411:2481](),[21.6547][21.2411:2481](),[21.2117][21.200:283](),[21.6495][21.2089:2117](),[21.199][21.6394:6467](),[21.743][21.6394:6467](),[21.5593][21.6394:6467](),[21.6394][21.6394:6467](),[21.9696][21.5558:5593](),[21.2225][21.9685:9696](),[21.6276][21.9685:9696](),[21.102222][21.9685:9696](),[21.9685][21.9685:9696](),[19.212][21.2163:2225](),[21.2163][21.2163:2225](),[21.2163][19.167:212](),[10.182][21.2080:2163](),[21.2080][21.2080:2163](),[21.2073][10.161:182](),[19.166][21.2014:2073](),[21.2014][21.2014:2073](),[21.2014][19.121:166](),[21.936][21.1864:2014](),[21.1241][21.1864:2014](),[13.3165][21.1864:2014](),[21.6276][21.1864:2014](),[21.9624][13.2970:3165](),[21.6202][21.9564:9624](),[21.102134][21.9564:9624](),[21.9564][21.9564:9624](),[21.9432][21.6034:6202](),[21.6033][21.9402:9432](),[21.101923][21.9402:9432](),[21.9402][21.9402:9432](),[21.9360][21.5979:6033](),[21.5978][21.9343:9360](),[21.101854][21.9343:9360](),[21.9343][21.9343:9360](),[21.354][21.5833:5978](),[21.744][21.5833:5978](),[21.1049][21.5833:5978](),[13.2969][21.5833:5978](),[21.5833][21.5833:5978](),[21.9165][13.2853:2969](),[21.224][21.9153:9165](),[21.421][21.9153:9165](),[21.5763][21.9153:9165](),[21.6688][21.9153:9165](),[21.22915][21.9153:9165](),[21.101597][21.9153:9165](),[21.9153][21.9153:9165](),[21.9058][21.357:421](),[21.5655][21.9027:9058](),[21.101475][21.9027:9058](),[21.9027][21.9027:9058](),[21.631][21.5627:5655](),[21.936][21.5627:5655](),[13.2852][21.5627:5655](),[21.9005][21.5627:5655](),[21.258][13.2769:2852](),[21.8882][21.205:258](),[21.5529][21.8848:8882](),[21.101328][21.8848:8882](),[21.8848][21.8848:8882](),[21.856][21.5479:5529](),[13.2768][21.5479:5529](),[21.8804][21.5479:5529](),[21.8804][13.2704:2768](),[21.2410][21.8762:8804](),[21.5478][21.8762:8804](),[21.101270][21.8762:8804](),[21.8762][21.8762:8804](),[21.8668][21.2296:2410](),[21.445][21.8641:8668](),[21.8641][21.8641:8668](),[21.5557][21.420:445](),[21.68][21.420:445](),[21.5377][21.5505:5557](),[21.169][21.5339:5377](),[21.551][21.5339:5377](),[21.795][21.5339:5377](),[13.2703][21.5339:5377](),[21.8468][21.5339:5377](),[21.5338][13.2596:2703](),[21.5504][21.5299:5338](),[21.5299][21.5299:5338](),[21.8364][21.5450:5504](),[13.2595][21.8359:8364](),[21.8359][21.8359:8364](),[21.8359][13.2287:2595](),[13.2286][21.8335:8359](),[21.8335][21.8335:8359](),[21.788][13.2271:2286](),[21.2295][21.722:788](),[21.5243][21.2242:2295](),[21.192][21.5145:5243](),[21.754][21.5145:5243](),[21.8134][21.5145:5243](),[21.8089][21.57:192](),[13.2270][21.8040:8089](),[21.8040][21.8040:8089](),[21.7962][13.2255:2270](),[21.117][21.7952:7962](),[21.356][21.7952:7962](),[21.5067][21.7952:7962](),[21.6469][21.7952:7962](),[21.22758][21.7952:7962](),[21.100865][21.7952:7962](),[21.7952][21.7952:7962](),[21.7859][21.294:356](),[21.4961][21.7830:7859](),[21.100745][21.7830:7859](),[21.7830][21.7830:7859](),[21.82][21.4792:4961](),[21.329][21.4792:4961](),[21.573][21.4792:4961](),[21.5449][21.4792:4961](),[14.6248][21.4792:4961](),[21.7697][21.4792:4961](),[21.7631][21.5383:5449](),[21.60][21.7173:7631](),[21.204][21.7173:7631](),[21.4791][21.7173:7631](),[21.6356][21.7173:7631](),[21.22676][21.7173:7631](),[21.100533][21.7173:7631](),[21.7173][21.7173:7631](),[21.7091][21.153:204](),[21.4696][21.7059:7091](),[21.100424][21.7059:7091](),[21.7059][21.7059:7091](),[21.1281][21.4648:4696](),[21.7017][21.4648:4696](),[21.865][21.1257:1281](),[21.1257][21.1257:1281](),[21.1226][21.793:865](),[21.7017][21.1049:1226](),[21.254][21.7016:7017](),[21.7016][21.7016:7017](),[21.94][21.172:254](),[21.179][21.172:254](),[21.172][21.172:254](),[21.125][21.15:94](),[21.253][21.15:94](),[21.497][21.15:94](),[13.2254][21.15:94](),[21.4647][21.15:94](),[21.4581][13.2149:2254](),[21.721][21.4542:4581](),[21.4542][21.4542:4581](),[21.6757][21.669:721](),[7.113][21.6746:6757](),[21.470][21.6746:6757](),[21.4487][21.6746:6757](),[21.100241][21.6746:6757](),[21.6746][21.6746:6757](),[21.4441][21.446:470](),[21.6692][21.4415:4441](),[21.4414][21.6653:6692](),[21.6653][21.6653:6692](),[21.6632][21.4388:4414](),[21.419][21.6621:6632](),[5.895][21.6621:6632](),[21.100147][21.6621:6632](),[21.6621][21.6621:6632](),[21.312][5.789:895](),[21.6468][21.249:312](),[21.4387][21.6457:6468](),[21.99992][21.6457:6468](),[21.6457][21.6457:6468](),[21.445][21.4361:4387](),[21.4361][21.4361:4387](),[5.788][21.421:445](),[21.4315][21.421:445](),[21.43][5.727:788](),[21.4252][21.15:43](),[21.6095][21.4220:4252](),[21.64][21.6090:6095](),[21.6090][21.6090:6095](),[21.6090][21.53:64](),[21.2101][21.6084:6090](),[21.6084][21.6084:6090](),[21.4219][21.2069:2101](),[13.2148][21.4191:4219](),[21.4191][21.4191:4219](),[21.5923][13.2108:2148](),[21.4060][21.5917:5923](),[21.99635][21.5917:5923](),[21.5917][21.5917:5923](),[21.1110][21.5845:5853](),[21.5845][21.5845:5853](),[21.5492][21.1067:1110](),[21.633][21.5483:5492](),[21.684][21.5483:5492](),[21.3725][21.5483:5492](),[21.99244][21.5483:5492](),[21.5483][21.5483:5492](),[21.56][21.558:633](),[21.47][21.558:633](),[21.557][21.15:56](),[21.5347][21.525:557](),[21.1066][21.5308:5347](),[21.3617][21.5308:5347](),[21.99122][21.5308:5347](),[21.5308][21.5308:5347](),[13.2107][21.1039:1066](),[21.3617][21.1039:1066](),[21.5239][13.2035:2107](),[21.972][21.5229:5239](),[21.5229][21.5229:5239](),[21.113][21.935:972](),[21.935][21.935:972](),[21.1966][21.609:935](),[21.8494][21.609:935](),[21.609][21.609:935](),[21.113][21.330:609](),[11.195][21.330:609](),[21.330][21.330:609](),[21.269][11.103:195](),[21.340][21.189:269](),[21.189][21.189:269](),[21.143][21.299:340](),[21.298][21.92:143](),[21.4442][21.92:143](),[21.91][21.273:298](),[13.2034][21.15:91](),[21.25239][21.15:91](),[21.3138][21.15:91](),[21.3095][13.1997:2034](),[21.4249][21.3031:3095](),[21.3030][21.4205:4249](),[21.98451][21.4205:4249](),[21.4205][21.4205:4249](),[21.4124][21.2931:3030](),[13.1996][21.4101:4124](),[21.2930][21.4101:4124](),[21.22445][21.4101:4124](),[21.98330][21.4101:4124](),[21.4101][21.4101:4124](),[3.314][21.22423:22445](),[21.2901][21.22423:22445](),[21.1165][3.268:314](),[21.816][21.988:1165](),[21.1895][21.988:1165](),[21.1023][21.642:816](),[21.44][21.999:1023](),[21.2727][21.999:1023](),[21.3913][21.999:1023](),[21.52][21.2701:2727](),[21.366][21.2701:2727](),[21.838][21.2701:2727](),[21.3868][21.2701:2727]()
    State.button_handlers = {}
    local screen_bottom1 = {line=nil, pos=nil}
    table.insert(State.line_cache, line_index, {})
    -- give some time for the OS to flush everything to disk
    love.timer.sleep(0.1)
    --? print_and_log(('edit.mouse_press: in line %d'):format(line_index))
    --? print('text input', t)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    -- undo
    patch_placeholders(State.line_cache, event.after, event.before)
    -- return y drawn until
    if State.lines[State.cursor1.line].mode == 'text' then
    break
    end
    State.cursor1.line = State.cursor1.line+1
    State.cursor1.pos = 1
    end
    function edit.put_cursor_on_next_text_line(State)
    while true do
    if State.cursor1.line >= #State.lines then
    break
    end
    edit.draw(State)
    edit.update(State, 0)
    App.screen.contents = {}
    edit.mouse_release(State, x,y, mouse_button)
    function edit.run_after_mouse_release(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.draw(State)
    end
    edit.update(State, 0)
    App.screen.contents = {}
    edit.mouse_press(State, x,y, mouse_button)
    function edit.run_after_mouse_press(State, x,y, mouse_button)
    App.fake_mouse_press(x,y, mouse_button)
    edit.draw(State)
    end
    edit.update(State, 0)
    App.screen.contents = {}
    edit.mouse_release(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.mouse_press(State, x,y, mouse_button)
    function edit.run_after_mouse_click(State, x,y, mouse_button)
    App.fake_mouse_press(x,y, mouse_button)
    edit.draw(State)
    end
    edit.update(State, 0)
    App.screen.contents = {}
    edit.keychord_press(State, chord)
    edit.key_release(State, chord)
    function edit.run_after_keychord(State, chord)
    -- not all keys are text_input
    end
    edit.draw(State)
    edit.update(State, 0)
    App.screen.contents = {}
    function edit.run_after_text_input(State, t)
    edit.keychord_press(State, t)
    edit.text_input(State, t)
    edit.key_release(State, t)
    -- TODO: handle chords of multiple keys
    -- all text_input events are also keypresses
    14, -- font height assuming default LÖVE font
    15) -- line height
    end
    App.screen.width - Test_margin_right,
    function edit.initialize_test_state()
    -- if you change these values, tests will start failing
    return edit.initialize_state(
    15, -- top margin
    Test_margin_left,
    Test_margin_right = 0
    Test_margin_left = 25
    -- Insulate tests from some key globals so I don't have to change the vast
    -- majority of tests when they're modified for the real app.
    --== some methods for tests
    end
    State.line_height = math.floor(font_height*1.3)
    love.graphics.setFont(love.graphics.newFont(State.font_height))
    function edit.update_font_settings(State, font_height)
    State.font_height = font_height
    end
    function edit.key_release(State, key, scancode)
    end
    end
    Text.keychord_press(State, chord)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    else
    schedule_save(State)
    end
    end
    if len > 0 then
    local byte_offset = Text.offset(p.name, len-1)
    if len == 1 then byte_offset = 0 end
    p.name = string.sub(p.name, 1, byte_offset)
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    end
    elseif chord == 'backspace' then
    local len = utf8.len(p.name)
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    local p = drawing.points[drawing.pending.target_point]
    if chord == 'escape' then
    p.name = nil
    local drawing = State.lines.current_drawing
    local before = snapshot(State, State.lines.current_drawing_index)
    else
    State.current_drawing_mode = State.previous_drawing_mode
    State.previous_drawing_mode = nil
    if chord == 'return' then
    elseif State.lines.current_drawing and State.current_drawing_mode == 'name' then
    if line.mode == 'drawing' then
    line.show_help = false
    end
    end
    for _,line in ipairs(State.lines) do
    end
    elseif chord == 'escape' and not App.mouse_down(1) then
    schedule_save(State)
    record_undo_event(State, {before=before, after=snapshot(State, drawing_index)})
    Drawing.keychord_press(State, chord)
    local before = snapshot(State, drawing_index)
    if drawing_index then
    local drawing_index, drawing = Drawing.current_drawing(State)
    -- DON'T reset line_cache.starty here
    -- dispatch to drawing or text
    elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
    record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
    schedule_save(State)
    end
    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
    if Text.cursor_out_of_screen(State) then
    end
    end
    Text.insert_at_cursor(State, c)
    else
    Text.insert_return(State)
    for _,code in utf8.codes(clipboard_data) do
    local c = utf8.char(code)
    if c == '\n' then
    local clipboard_data = App.get_clipboard()
    local before = snapshot(State, before_line)
    local before_line = State.cursor1.line
    -- 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.
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    elseif chord == 'C-v' then
    schedule_save(State)
    end
    App.set_clipboard(s)
    if s then
    local s = Text.cut_selection(State, State.left, State.right)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    end
    elseif chord == 'C-x' then
    App.set_clipboard(s)
    if s then
    local s = Text.selection(State)
    elseif chord == 'C-c' then
    elseif chord == 'C-a' then
    State.selection1 = {line=1, pos=1}
    State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1}
    end
    -- clipboard
    schedule_save(State)
    -- invalidate various cached bits of lines
    State.lines.current_drawing = nil
    -- if we're scrolling, reclaim all fragments to avoid memory leaks
    Text.redraw_all(State)
    State.screen_top1 = deepcopy(src.screen_top)
    State.cursor1 = deepcopy(src.cursor)
    State.selection1 = deepcopy(src.selection)
    patch(State.lines, event.before, event.after)
    if event then
    local src = event.after
    local event = redo_event(State)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    end
    elseif chord == 'C-y' then
    schedule_save(State)
    -- invalidate various cached bits of lines
    State.lines.current_drawing = nil
    -- if we're scrolling, reclaim all fragments to avoid memory leaks
    Text.redraw_all(State)
    State.screen_top1 = deepcopy(src.screen_top)
    State.cursor1 = deepcopy(src.cursor)
    State.selection1 = deepcopy(src.selection)
    patch(State.lines, event.after, event.before)
    if event then
    local src = event.before
    local event = undo_event(State)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    elseif chord == 'C-z' then
    Text.redraw_all(State)
    edit.update_font_settings(State, 20)
    elseif chord == 'C-0' then
    if State.font_height > 2 then
    edit.update_font_settings(State, State.font_height-2)
    Text.redraw_all(State)
    end
    elseif chord == 'C--' then
    Text.redraw_all(State)
    edit.update_font_settings(State, State.font_height+2)
    elseif chord == 'C-=' then
    -- zoom
    State.search_backup = {
    cursor={line=State.cursor1.line, pos=State.cursor1.pos},
    screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},
    }
    State.search_term = ''
    end
    return
    elseif chord == 'C-f' then
    Text.search_previous(State)
    elseif chord == 'up' then
    Text.search_next(State)
    State.cursor1.pos = State.cursor1.pos+1
    elseif chord == 'down' then
    local len = utf8.len(State.search_term)
    local byte_offset = Text.offset(State.search_term, len)
    State.search_term = string.sub(State.search_term, 1, byte_offset-1)
    elseif chord == 'backspace' then
    State.search_backup = nil
    State.search_term = nil
    elseif chord == 'return' then
    Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
    State.cursor1 = State.search_backup.cursor
    State.screen_top1 = State.search_backup.screen_top
    State.search_backup = nil
    State.search_term = nil
    if chord == 'escape' then
    if State.search_term then
    end
    Text.delete_selection(State, State.left, State.right)
    chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and chord ~= 'C-y' and not App.is_cursor_movement(chord) then
    -- 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
    if State.selection1.line and
    not State.lines.current_drawing and
    function edit.keychord_press(State, chord, key)
    end
    schedule_save(State)
    end
    end
    Text.text_input(State, t)
    local drawing_index, drawing = Drawing.current_drawing(State)
    if drawing_index == nil then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    else
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    local p = drawing.points[drawing.pending.target_point]
    p.name = p.name..t
    local drawing = State.lines.current_drawing
    local before = snapshot(State, State.lines.current_drawing_index)
    elseif State.lines.current_drawing and State.current_drawing_mode == 'name' then
    Text.search_next(State)
    if State.search_term then
    State.search_term = State.search_term..t
    function edit.text_input(State, t)
    end
    end
    for i=1,math.floor(-dy) do
    Text.down(State)
    end
    edit.put_cursor_on_next_text_line(State)
    State.cursor1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
    elseif dy < 0 then
    for i=1,math.floor(dy) do
    Text.up(State)
    end
    edit.put_cursor_on_next_text_line(State)
    end
    end
    function edit.mouse_wheel_move(State, dx,dy)
    if dy > 0 then
    State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
    --? print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos))
    end
    break
    end
    end
    end
    State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
    if eq(State.cursor1, State.selection1) then
    State.selection1 = {}
    end
    end
    State.selection1 = State.old_selection1
    else
    if State.mousepress_shift then
    if State.old_selection1.line == nil then
    State.selection1 = State.old_cursor1
    --? print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    }
    pos=Text.to_pos_on_line(State, line_index, x, y),
    line=line_index,
    State.cursor1 = {
    --? print_and_log(('edit.mouse_release: in line %d'):format(line_index))
    if Text.in_line(State, line_index, x,y) then
    if line.mode == 'text' then
    for line_index,line in ipairs(State.lines) do
    --? print_and_log('edit.mouse_release: no current drawing')
    Drawing.before = nil
    end
    else
    record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
    if Drawing.before then
    schedule_save(State)
    Drawing.mouse_release(State, x,y, mouse_button)
    if State.lines.current_drawing then
    --? print_and_log(('edit.mouse_release: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    if State.search_term then return end
    function edit.mouse_release(State, x,y, mouse_button)
    end
    -- still here? click is below all screen lines
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=State.screen_bottom1.line,
    pos=Text.pos_at_end_of_screen_line(State, State.screen_bottom1),
    }
    end
    end
    end
    return
    Drawing.mouse_press(State, line_index, x,y, mouse_button)
    Drawing.before = snapshot(State, line_index)
    State.lines.current_drawing_index = line_index
    State.lines.current_drawing = line
    local line_cache = State.line_cache[line_index]
    if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) then
    end
    elseif line.mode == 'drawing' then
    return
    }
    pos=Text.to_pos_on_line(State, line_index, x, y),
    line=line_index,
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    -- i.e. mouse_release should never look at shift state
    -- 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
    if Text.in_line(State, line_index, x,y) then
    if line.mode == 'text' then
    for line_index,line in ipairs(State.lines) do
    }
    return
    end
    line=State.screen_top1.line,
    pos=State.screen_top1.pos,
    if y < State.top then
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    -- press on a button and it returned 'true' to short-circuit
    return
    end
    if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then
    --? print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    if State.search_term then return end
    function edit.mouse_press(State, x,y, mouse_button)
    end
    end
    save_to_disk(State)
    if State.next_save then
    -- make sure to save before quitting
    function edit.quit(State)
    end
    end
    State.next_save = Current_time + 3 -- short enough that you're likely to still remember what you did
    function schedule_save(State)
    if State.next_save == nil then
    end
    end
    State.next_save = nil
    save_to_disk(State)
    if State.next_save and State.next_save < Current_time then
    Drawing.update(State, dt)
    function edit.update(State, dt)
    end
    return y
    end
    Text.draw_search_bar(State)
    if State.search_term then
    State.screen_bottom1 = screen_bottom1
    end
    end
    print(line.mode)
    assert(false)
    else
    y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom
    Drawing.draw(State, line_index, y)
    y = y+Drawing_padding_top
    elseif line.mode == 'drawing' then
    --? print('=> y', y)
    y, screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
    end
    end,
    })
    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)})
    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={}})
    button(State, 'draw', {x=State.left-Margin_left+4, y=y+4, w=12,h=12, color={1,1,0},
    end
    if line.data == '' then
    -- button to insert new drawing
    startpos = State.screen_top1.pos
    if line_index == State.screen_top1.line then
    local startpos = 1
    if line.mode == 'text' then
    --? print('text.draw', y, line_index)
    screen_bottom1.line = line_index
    if y + State.line_height > App.screen.height then break end
    --? print('draw:', y, line_index, line)
    for line_index = State.screen_top1.line,#State.lines do
    local line = State.lines[line_index]
    --? print('== draw')
    local y = State.top
    State.cursor_x = nil
    State.cursor_y = nil
    if not Text.le1(State.screen_top1, State.cursor1) then
    print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
    assert(false)
    end
    if #State.lines ~= #State.line_cache then
    print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
    assert(false)
    end
    App.color(Text_color)
    function edit.draw(State)
    [21.838]
    -- all text_input events are also keypresses
    -- TODO: handle chords of multiple keys
    function edit.run_after_text_input(State, t)
    edit.keychord_press(State, t)
    edit.text_input(State, t)
    edit.key_release(State, t)
    App.screen.contents = {}
    edit.update(State, 0)
    edit.draw(State)
    end
    -- not all keys are text_input
    function edit.run_after_keychord(State, chord)
    edit.keychord_press(State, chord)
    edit.key_release(State, chord)
    App.screen.contents = {}
    edit.update(State, 0)
    edit.draw(State)
    end
    function edit.run_after_mouse_click(State, x,y, mouse_button)
    App.fake_mouse_press(x,y, mouse_button)
    edit.mouse_press(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.mouse_release(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.update(State, 0)
    edit.draw(State)
    end
    function edit.run_after_mouse_press(State, x,y, mouse_button)
    App.fake_mouse_press(x,y, mouse_button)
    edit.mouse_press(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.update(State, 0)
    edit.draw(State)
    end
    function edit.run_after_mouse_release(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.mouse_release(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.update(State, 0)
    edit.draw(State)
    end