Still lots to do, but the eventual hope is that this will make this project's code easier to reuse from other LÖVE projects.
One gotcha: even as we start putting code more aggressively into nested tables, tests must remain at the top-level. Otherwise they won't run.
2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC QW5KQQTDX5SRZI6EUHDZNQZMCTMEMBJHWQI4KGCLZG7SHLXMLKGAC Q7BDB3XQPTTM3YH3HAXATV7ENUTRXTYH55X3PPSXMQA2DO5UJHFAC SHEGBK4HP2KVIXQBVLD2E4XUEFEB3V7KFKVC7HY65TIUTL5YOH7QC T7IWZFL4NGMHUKNBXVXMC32AO4GC63WJDAOVOL6M6HIPANPNRCMAC TC4HQILHSPUPG6GXQCN2NRSOTLLXP3PICKPHRJXYSNSUYVP5WPFQC S7ZZA3YEKYGLBN6UC2N7WGUS43L6MX2KQQ2LBUZT4FQ7K7V5IQGQC 3EMBUWVW6X5NELBVPEGXLKU4YGOHAQRO5US323BI3OEGVHPXND7QC 6VXO3ZL3P7NCX2SEOUDCTTRB4J46G5RXVTUYXFJBLNZA23I2ZZYQC GK47BBCYVEZ3OEQ7ISE2WCJULAFZ35WC6EYJ5CTBYNM26RSAELOQC 4CXVIEBSQ5X62UYNJNSNMYKP24GE4IPO77T5ZWQW24QIK2BUQGWAC K4OBZSHEBIZBAKPH3F7ASDGCPLB7D5W5QLFJQYSM5XOYDPB4BUHAC QCQTMUZ7M3BKJFTKXTTXL4TS4CAQNIUNK3LR3WQIJDU3VVTOPS6AC NX3DDSCZM23ONUBXATHBM2DM3RL7YO7LDPXLI2UA6GQU2G3DKOTQC 4CTZOJPCTWYUSHLIZZJ2M5W7S4JZFZVT5MUU5XNSOIBS5L4UY5UQC AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC TVCPXAAU4P3K5MFYINH2MWDK3KGTQ2GE74TUNERYOONG2G5EYKMQC OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC 6LJZN727CRPYR34LV75CQF55YZI3E7MGESYZSFSYAE73SNEZE3FAC VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC XX7G2FFJ4QCGQGD4REAW5QFHVYAKCFUPGZCK7L6DFGS5ISVBYBQQC CE4LZV4TNXJT54CVGM3QANCBP42TMLMZWF2DBSMUYKAHILXIZEMQC VVXVV2D2F5Y6D6N5VVPUPK3N6GMDTG2YCYPQDYTYEKVKBYHRRYEAC AD34IX2ZSGYGU3LGY2IZOZNKD4HRQOYJVG5UWMWLXJZJSM62FFOAC TNHZZYWPVWKA3OEBRH3QQ64GW3D2BQL5JJAVIUKQM5WBTYWASMIQC MYC7XR5QOT2AXHF6UNGSNFFD5VL6UHGUZQBP7PWWLZ5NNXE7UMTAC JCSLDGAH2F6AIY4Z6XM6K4LOMW7EFY3E4NF5YXLMHLTYTX3A4Z3QC BOFNXP5GZDCUMQG3LQVTSSFEQP7REQ4RIRJLDLETFSAGFTVDVEKAC MGOQ5XAVFTWZPBG2O5ZTGSEKU6BRJKQZLDV6CM4737VD2FAEB5JQC 2RXZ3PGOTTZ6M4R372JXIKPLBQKPVBMAXNPIEO2HZDN4EMYW4GNAC WOXIYUTL4NU7ACHQYXEXJDSXCRDLQ2X457KO6C7GEXFQZ43F3L7QC DHI6IJCNSTHGED67T6H5X6Y636C7PIDGIJD32HBEKLT5WIMRS5MAC DRFE3B3ZKRG4RY2R5Q3SDFD3LH4EXUX3CZCDFBNAXVI2SLDS57PAC 5FW7YOFTLKHRND6IOR4HG4X3C5BO2WV5KTEUW3PPKCRU5L5GXKXQC RMKMPFT5L67WIFWIO4GTC6XESX6UPKNL4GPNQLOBC5CXSUZABEHQC IRV65LZPHFLLYPTMLTDO6OJDHVZQJ6MFXZ45IRXRDSRSEQNO5DIAC OIB2QPRCB4MAVZV5NCEKSAL45ITT6V4BYSET3Q2VCT3WBOIC4QVQC 4RUI5X52CSQODLT3WI4VBMXWZLACBYV5QANGDKRWS3VONZPVSEEQC AVQ5MC5DWNLI6LUUIPGBLGP4LKRPGWBY4THNY25OBT2FAVHC6MCAC A2QPFRFJNWDHBYRRLJFBK5BOTOWXDT5DYCKHRRKVBZNDA4NE3CHQC LUNH47XXUUITDLE6NBI3J7GJQWQ45OQAGXY2HJI4HRPOR2GUULPAC V5TP27FPD6GPATHPABRW2FKP7BIKY53KL47UVEU5DF4WXEZF7CKAC JVRL5TWLBTWMTHJDZSDN5XQDMEIIPVAZBKUP75HMO7JHURAYWG5QC R6GUSTBY5ZHR7E46DSIDQDNZDJI6QMZQDC7RPQMQWLGWQKXU6HVQC HOSPP2ANSW654DYRTC6CQUQA2GUKV6T2FI7QBKXD2DZS3R32IMGAC 42LVB4DEK3ILS3O5DHFMTJO5HNMJFDYA2WRCLUIOYFPA46MJFZTAC YPHKZVWM2FS7U3VNVDXFRJTBF4RLQ6K7ZWISLHOQJPYSKBELHFEAC LAW2O3NWVFTPBSKIMIXPAGYBDOCHYJNKCAVWKNKH62G42DIKZCYQC 5DOTWNVMOGN75GJMXXB4A56UAJYSNYC5WXPRT7QFMAAV27NWPP3AC ZZ2B5RPQKANSIWAZA4ATDXVBK3XLYIORJ7I4IH2WQOG5JAPJFZ4AC YKRF5V3ZZQIQ3UGAFYTQT5PUQVHCP2VHFDX77EY2C3X543HUDYKQC IZZVOCLB7KB4ZNQ35OL466MHWOK3XZMOS7ZPFLHUFQ47LJLQQQ3QC PGZJ6NATSMW4XH64XEPE5Q2EEYCMAMQIIP2OZXPNJ527234QPKMQC PX7DDEMOBGPVK3FXKK5XEPG24CJXZSVW67DLG2JZZ5E77NVEAA3AC 73OCE2MCBJJZZMN2KYPJTBOUCKBZAOQ2QIAMTGCNOOJ2AJAXFT2AC Z4XRNDTRTGSZHNB65WNHOVUBFW4QWQABLVSK4RM3QJHGK33DMRJAC NP7PIUBTR4K6SWJS46YZG3H2RYYNRGNEJMPV4I24TQXT5O3YT27QC K464QQR4FTXFUMHFWAGOD5DJ6YHUBUKRHLXF2ORE74DVT7TVQ35QC J2SVGR2EQEROXDDMYZOCELD2VDYQALGZYRSZ4WGMTACAGMRPJ7UAC 6J3NXBYGADKVHD53QKHUZNRO2B52DC66Y6GQT5KEH6YKVYNCCRTAC 4VKEE43Z7MUPNIAOCK36INVBNHRTSWRRN37TIKRPXPH3DRKGHHAQC ZNLTRNNKAKSMWOVZKYKFPSIOSRFS73YTFADWE4N5V3BT4UX57Q4AC 2H67P75X7FSSNZMV4TU5I46XLIYAJ5VT7H6MS7GYLUSNTA4IRDFQC YGCT2D2ORMLTBHANLGHZV3EBGGHD7ZK55UAM7HF2AVSHDXAAKK5QC DHCLUDCWEEPSOCVMSO7M6DDWESXWZ7PIZNXKG6TOT6TT6ODAPLBAC DLQMM2656JHXX3ONOEM6UIOXKFJFT5QT7RHWK7YS2W77PVZWHRSAC OYXDYPGSJK2QICJ6RBA7357WT4FSNAWRUT77YLQHT3F3VYMWGNFQC UWNHC4AAO3SPOYLPANTO4WKCTZL7KAYC73Q2YUZFFW7K26FVJ7FQC LFMI3D7D236VAARLPOF3OKWMFVL7KSCVEUBNGJKVOE75ST3AKZSQC 7IKRRESBHMYHHKW4XHUEEKHKPOBLAGZ7A7FJMRU32MTRKIV6S7GQC NQKFQSZEFIQTIJXEJ64KX46JXLWUUFXVRTQCPM7HF4DUHT2QHZAAC 4C375P53EXHUPXUFQSI3LA7THEP2WOKX5ZB57OQ5ZSM7LYOVW5HAC B3IWYWSRDSZ7AG5HDS3TELNTG2IKRZYPI25B6LJGVFAJYTHVXZZAC ESETRNLB3MIJ2SID6HJMMP52FEVUBLGK2HLWD75KDQZAKQMKSF2QC YTSPVDZHEN5LLNMGIBUBLPWFWSFM3SOHBRGWYSDEVFKRTH24ARRQC 3CS5KKCIZQ6J4SBILINYZSOM6V3U2LE7YIXOZVKXXNBROF6Z6JWAC BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC QU7NHFOVGFSKQ3CWG7EF2Q7GKP3Z6FHGTIDXFHHMSFL6XMUOHMEAC CVGE3SIGJRGCLY3A2RBPGFXAEKVZXUUIZQLRHJLM4VPUM4SHEZIAC RT6EV6OPUYCXYZOX2PHFXJ7KT77KHNEVINEGQXIQLHQVKPGTN6VQC KVHUFUFVOSY6GB4XI2QK4T4WCLIYOV3NZR67TX6AQHAQDWJMEOBQC VIU2FBNVHG5FV5AJLVPMGEUO5HCLJEGZTRWNY2C5XC4AKMQZZKVAC 7M7LS7I2QT6AFZ6RVK5KK2CZ6SNJAMQIWD7MX34F7MQ3MZKH72GAC AM42E4Y6RLS7QPWBMESL6H5RPFKG5LQYM6EFNB5UYSRSUASKLISQC J5IEBT64CTGVDGSIBE2MTXBTGOMFSSF62SVCFZL2SDDLE745MGDAC OAHNWDYG3V6EJQXJ333FD7M44E3VBZUWSKZ45SSFXVK5Q2HJUI4AC ZUOL7X6VIPRCMEZURYGNHTDEIP3ZCHZW4PKVKBNXVZL5V4VOE5ZQC U76D4P367BI2LN7UWQ5R26T6FPYL3RRZS4SBH3QZB4F57C7SFRWQC 2INHXC3KRJVZTX2BQ63ZQLHIC5SWPUG4PQKCMLC7SQKN5R7LJZ6QC CTJ3IZGSPY4DBHC6OYNL4DZE24MXYQBM3KVJZTQHM5DI5TED5ZLQC EMRPLZPWIJOEYBBSHFBRUYG4EFROTZ5VG7N3MTXFZFFPBTZ6ODGQC AQQQNDTL52Q2VO3XLEGYKHTU2YSRAB4ACEGVPSWYD2Z6WA6Z2YPAC CCYSVZA2ONWXB6XJXWSIEBY4CS2LGBEVV3RB6KZ6I4XYRXQLSTXQC QLTJG7Q33ABBTDJ55K3OPLNSYBFBIVRS3UABXEY73RHYMOOJ542QC 2ENZW7TVCS47BWCA4AIEVGKGMT4Y2TSM5IJ7O5K2VSWNXIN3SG4QC HYEAFRZ2UEKDYTAE2GDQLHEJBPQASP2NDLMXB7F6MTVK2BKOXKEAC FYS7TCDWKNRNOJSGRD2JMU4B2LHX5S63ZISM7YF7KZYEYLVCIKIAC ZLJGZYQGQ2S4UFWTVF4PQDSGMP6A4IS4GDHCMBAAA5SK2N2NWR3QC 3GFQP6IRHABYMDAEXEMM2HQNEUY4LT2P72PI3KXV4M6PSQT3SFLAC QZH3PQFUBL2ISJBJ4PBALVKUPKQ4QKGNKSFASEXWYB5J7ODL42KQC XHLL3JQRBGO3FVFRMEFHCFEHHRTEGSYPVW5PDBFXZO3XZAU2F7JAC BYG5CEMVXANDTBI2ORNVMEY6K3EBRIHZHS4QBK27VONJC5537COQC 7SFHSB47KC6YH737DJPYYVONCFGEHC2Z37RCVPJO6I5PHEOLK74AC CIQN2MDEMWAASJAHOHMUZTI5PF4JV5SZSOBYYDCIIFYO2VHWULKAC KMRJOSLYYHHPGMYXBSLUQTICP6F4LXRCGYSP55YTZQSX4SZISDEAC OGUV4HSA7XGSQLUVWBAE3AE263Z7Z6G3BZOB4CN2AOYD2DEJMOZAC MP2TBKU6CNDMZKENYMBV62F5KQ27ZWEVPVRFS2RESVDQQT2IRR4AC TKFSYQ2ZTEPN27IGKGEYSUS7FQ2AUIC7DJWFF2RJ53AW6QRPMXWQC EMHRPJ3RAVIVJEQIRXIVDGENV6QHUUGXXRWTJ3BXC7SZNC66VK5QC AJB4LFRBMIRBEDWJ3OW7GQIMD2BZBVQ62GH4TE2FISWZKSAHRF4QC NQWWTGXRLSBASOSP75FPOSVYP664VYRFQH7MY5LALLIP2VEBQMCQC 65XHTZEKUTGHMOIWAFRH7ZVGUP4DWBCUT2TN4Y3LHYILKWTTBLKAC T7SJSJIH3FUWK2TK6DNVLCNYL7ROJGFVMOOCIY3L46EX5T7M7VNAC LS55YKGWKICTQTAHR5KLMNDOL6CDI4ATT3NT5Z2YL5IM3CRQOONQC SN2QONLI6IAOW6UBIJIPWJVIC3HIYDPVWTT24V44CT5PK2RMJ3QAC WIDXZBNW4CCOGWHCQSICXRPSZ4MB24OJZLEC3RO3UTEZKEFUZBSQC VC2CU2GGRIWXIFJELD5NAELDUIRY5S5LEAFJCM2A5P3CUBYF3Z3AC IRCKL6VNSFB7TQEKPQUPJCN37N5QW7D54DSZMESVXGK7NEHGSIPAC 5ZFHMYQIX3BJ3OXWEAZHTXVZ4P6JJ7X34D372XGKBZWVYXLX75OQC YJJ4X4JGABMVA5JBQW5UAWI543P3Y7NDVFTOHA6LIDA5KSFGUFNQC BTKAW76LJFOXLINKJKOIK47MUDFHZKDMWX3NQODS2XUQLYGOZXUQC JFFUF5ALUWPDM7IEDEZVAYG2SVXO334STONRGKVB3QKY2TT5QGBQC TVM2WIHHCOTGYGJ4EL6MIUVH22Y4TU3HHHUFWAVVJQAT2ARCA2NQC IDG26SXKRKPRUQM3G2MMBQR3UZGNU7NCWJBBDKKIM5IGUNJQ2URQC VCMS2CWTJDOW2FYCDH4ZSMVJAP6W4ZV4VWHJPPGGNHRX47GWYAGAC 252M2QMDBMNWHBZY5MDSC7WVYO5JBLJYPVMW5W4IVJCZVYRQ5IQQC VG75U7IM2ZQTGM2QETDT6QQ4CSLQPB4APK436POAAQJWOMINPIJAC XNFTJHC4QSHNSIWNN7K6QZEZ37GTQYKHS4EPNSVPQCUSWREROGIQC 242L3OQXTU2TCAINRJXQEEDSXQXM7Y7USUPBK37ZNM3A7V5TUDSAC AVLAYODPMKCDBUFJSTGNUXIK74V3NDCBH55DBBFTNVBMFY6I7BCAC HRWN5V6J6VMXS7WNSRGI7WMUSZ2OI52JJ4IK352VVSDZI4EF5HHQC JRLBUB6LR2JIAKVQNKF3T4BDICUIJ3HEMRRHX56YP5M5SP7ZS3WAC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC BJ5X5O4ACBBJ56LRBBSTCW6IBQP4HAEOOOPNH3SKTA4F66YTOIDAC -- 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 screenCursor1 = {line=1, pos=1} -- position of cursorScreen_bottom1 = {line=1, pos=1} -- position of start of screen line at bottom of screenSelection1 = {}Old_cursor1, Old_selection1, Mousepress_shift = nil -- some extra state to compute selection between mouse press and releaseRecent_mouse = {} -- when selecting text, avoid recomputing some state on every single frameCursor_x, Cursor_y = 0, 0 -- in pixelsCurrent_drawing_mode = 'line'Previous_drawing_mode = nil-- values for testsFont_height = 14Line_height = 15-- widest possible character widthEm = App.newText(love.graphics.getFont(), 'm')Margin_top = 15Margin_left = 25Margin_right = 25Margin_width = Margin_left + Margin_rightDrawing_padding_top = 10Drawing_padding_bottom = 10Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottomFilename = love.filesystem.getUserDirectory()..'/lines.txt'Next_save = nil-- undoHistory = {}Next_history = 1-- searchSearch_term = nilSearch_text = nilSearch_backup = nil -- stuff to restore when cancelling search
return edit.initialize_globals()end
-- resizeLast_resize_time = nil-- blinking cursorCursor_time = 0end -- App.initialize_globals
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 = -1local y = Margin_top--? print('== draw')for line_index = Screen_top1.line,#Lines dolocal line = Lines[line_index]--? print('draw:', y, line_index, line)if y + Line_height > App.screen.height then break endScreen_bottom1.line = line_indexif line.mode == 'text' and line.data == '' thenline.starty = yline.startpos = 1-- insert new drawingbutton('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 thenCursor1.line = Cursor1.line+1endschedule_save()record_undo_event({before=Drawing.before, after=snapshot(line_index-1, line_index+1)})end})if Search_term == nil thenif line_index == Cursor1.line thenText.draw_cursor(Margin_left, y)endendScreen_bottom1.pos = Screen_top1.posy = y + Line_heightelseif line.mode == 'drawing' theny = y+Drawing_padding_topline.y = yDrawing.draw(line)y = y + Drawing.pixels(line.h) + Drawing_padding_bottomelseline.starty = yline.startpos = 1if line_index == Screen_top1.line thenline.startpos = Screen_top1.posend--? 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)endendif Cursor_y == -1 thenCursor_y = App.screen.heightend--? print('screen bottom: '..tostring(Screen_bottom1.pos)..' in '..tostring(Lines[Screen_bottom1.line].data))if Search_term thenText.draw_search_bar()end
edit.draw()
Cursor_time = Cursor_time + dt-- some hysteresis while resizingif Last_resize_time thenif App.getTime() - Last_resize_time < 0.1 thenreturnelseLast_resize_time = nilendendDrawing.update(dt)if Next_save and Next_save < App.getTime() thensave_to_disk(Lines, Filename)Next_save = nilend
edit.update(dt)
if Search_term then return end-- ensure cursor is visible immediately after it movesCursor_time = 0--? print('press', Selection1.line, Selection1.pos)propagate_to_button_handlers(x,y, mouse_button)for line_index,line in ipairs(Lines) doif line.mode == 'text' thenif 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 stateOld_cursor1 = Cursor1Old_selection1 = Selection1Mousepress_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)breakendelseif line.mode == 'drawing' thenif Drawing.in_drawing(line, x, y) thenLines.current_drawing_index = line_indexLines.current_drawing = lineDrawing.before = snapshot(line_index)Drawing.mouse_pressed(line, x,y, mouse_button)breakendendend
return edit.mouse_pressed(x,y, mouse_button)
if Search_term then return end--? print('release')-- ensure cursor is visible immediately after it movesCursor_time = 0if Lines.current_drawing thenDrawing.mouse_released(x,y, mouse_button)schedule_save()if Drawing.before thenrecord_undo_event({before=Drawing.before, after=snapshot(Lines.current_drawing_index)})Drawing.before = nilendelsefor line_index,line in ipairs(Lines) doif line.mode == 'text' thenif 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 thenif Old_selection1.line == nil thenSelection1 = Old_cursor1elseSelection1 = Old_selection1endendOld_cursor1, Old_selection1, Mousepress_shift = nilif eq(Cursor1, Selection1) thenSelection1 = {}endbreakendendend--? print('selection:', Selection1.line, Selection1.pos)end
return edit.mouse_released(x,y, mouse_button)
-- ensure cursor is visible immediately after it movesCursor_time = 0for _,line in ipairs(Lines) do line.y = nil end -- just in case we scrollif Search_term thenSearch_term = Search_term..tSearch_text = nilText.search_next()elseif Current_drawing_mode == 'name' thenlocal before = snapshot(Lines.current_drawing_index)local drawing = Lines.current_drawinglocal p = drawing.points[drawing.pending.target_point]p.name = p.name..trecord_undo_event({before=before, after=snapshot(Lines.current_drawing_index)})elseText.textinput(t)endschedule_save()
return edit.textinput(t)
-- ensure cursor is visible immediately after it movesCursor_time = 0if Selection1.line andnot 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) andchord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and backspace ~= 'delete' and not App.is_cursor_movement(chord) thenText.delete_selection(Margin_left, App.screen.width-Margin_right)endif Search_term thenif chord == 'escape' thenSearch_term = nilSearch_text = nilCursor1 = Search_backup.cursorScreen_top1 = Search_backup.screen_topSearch_backup = nilText.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leakselseif chord == 'return' thenSearch_term = nilSearch_text = nilSearch_backup = nilelseif chord == 'backspace' thenlocal 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 = nilelseif chord == 'down' thenCursor1.pos = Cursor1.pos+1Text.search_next()elseif chord == 'up' thenText.search_previous()endreturnelseif chord == 'C-f' thenSearch_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-=' theninitialize_font_settings(Font_height+2)Text.redraw_all()elseif chord == 'C--' theninitialize_font_settings(Font_height-2)Text.redraw_all()elseif chord == 'C-0' theninitialize_font_settings(20)Text.redraw_all()elseif chord == 'C-z' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal event = undo_event()if event thenlocal src = event.beforeScreen_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 leaksschedule_save()endelseif chord == 'C-y' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal event = redo_event()if event thenlocal src = event.afterScreen_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 leaksschedule_save()end-- clipboardelseif chord == 'C-c' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal s = Text.selection()if s thenApp.setClipboardText(s)endelseif chord == 'C-x' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal s = Text.cut_selection(Margin_left, App.screen.width-Margin_right)if s thenApp.setClipboardText(s)endschedule_save()elseif chord == 'C-v' thenfor _,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.linelocal before = snapshot(before_line)local clipboard_data = App.getClipboardText()for _,code in utf8.codes(clipboard_data) dolocal c = utf8.char(code)if c == '\n' thenText.insert_return()elseText.insert_at_cursor(c)endendif Text.cursor_past_screen_bottom() thenText.snap_cursor_to_bottom_of_screen(Margin_left, App.screen.height-Margin_right)endschedule_save()record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})-- dispatch to drawing or textelseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then-- DON'T reset line.y herelocal drawing_index, drawing = Drawing.current_drawing()if drawing_index thenlocal before = snapshot(drawing_index)Drawing.keychord_pressed(chord)record_undo_event({before=before, after=snapshot(drawing_index)})schedule_save()endelseif chord == 'escape' and not App.mouse_down(1) thenfor _,line in ipairs(Lines) doif line.mode == 'drawing' thenline.show_help = falseendendelseif Current_drawing_mode == 'name' thenif chord == 'return' thenCurrent_drawing_mode = Previous_drawing_modePrevious_drawing_mode = nilelselocal before = snapshot(Lines.current_drawing_index)local drawing = Lines.current_drawinglocal p = drawing.points[drawing.pending.target_point]if chord == 'escape' thenp.name = nilrecord_undo_event({before=before, after=snapshot(Lines.current_drawing_index)})elseif chord == 'backspace' thenlocal len = utf8.len(p.name)local byte_offset = Text.offset(p.name, len-1)if len == 1 then byte_offset = 0 endp.name = string.sub(p.name, 1, byte_offset)record_undo_event({before=before, after=snapshot(Lines.current_drawing_index)})endendschedule_save()elsefor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrollText.keychord_pressed(chord)end
return edit.keychord_pressed(chord, key)
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 runfunction 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 screenCursor1 = {line=1, pos=1} -- position of cursorScreen_bottom1 = {line=1, pos=1} -- position of start of screen line at bottom of screenSelection1 = {}Old_cursor1, Old_selection1, Mousepress_shift = nil -- some extra state to compute selection between mouse press and releaseRecent_mouse = {} -- when selecting text, avoid recomputing some state on every single frameCursor_x, Cursor_y = 0, 0 -- in pixelsCurrent_drawing_mode = 'line'Previous_drawing_mode = nil-- values for testsFont_height = 14Line_height = 15-- widest possible character widthEm = App.newText(love.graphics.getFont(), 'm')Margin_top = 15Margin_left = 25Margin_right = 25Margin_width = Margin_left + Margin_rightDrawing_padding_top = 10Drawing_padding_bottom = 10Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottomFilename = love.filesystem.getUserDirectory()..'/lines.txt'Next_save = nil-- undoHistory = {}Next_history = 1-- searchSearch_term = nilSearch_text = nilSearch_backup = nil -- stuff to restore when cancelling search-- resizeLast_resize_time = nil-- blinking cursorCursor_time = 0end -- App.initialize_globalsfunction 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 = -1local y = Margin_top--? print('== draw')for line_index = Screen_top1.line,#Lines dolocal line = Lines[line_index]--? print('draw:', y, line_index, line)if y + Line_height > App.screen.height then break endScreen_bottom1.line = line_indexif line.mode == 'text' and line.data == '' thenline.starty = yline.startpos = 1-- insert new drawingbutton('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 thenCursor1.line = Cursor1.line+1endschedule_save()record_undo_event({before=Drawing.before, after=snapshot(line_index-1, line_index+1)})end})if Search_term == nil thenif line_index == Cursor1.line thenText.draw_cursor(Margin_left, y)endendScreen_bottom1.pos = Screen_top1.posy = y + Line_heightelseif line.mode == 'drawing' theny = y+Drawing_padding_topline.y = yDrawing.draw(line)y = y + Drawing.pixels(line.h) + Drawing_padding_bottomelseline.starty = yline.startpos = 1if line_index == Screen_top1.line thenline.startpos = Screen_top1.posend--? 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)endendif Cursor_y == -1 thenCursor_y = App.screen.heightend--? print('screen bottom: '..tostring(Screen_bottom1.pos)..' in '..tostring(Lines[Screen_bottom1.line].data))if Search_term thenText.draw_search_bar()endendfunction edit.update(dt)Cursor_time = Cursor_time + dt-- some hysteresis while resizingif Last_resize_time thenif App.getTime() - Last_resize_time < 0.1 thenreturnelseLast_resize_time = nilendendDrawing.update(dt)if Next_save and Next_save < App.getTime() thensave_to_disk(Lines, Filename)Next_save = nilendendfunction schedule_save()if Next_save == nil thenNext_save = App.getTime() + 3 -- short enough that you're likely to still remember what you didendendfunction edit.quit()-- make sure to save before quittingif Next_save thensave_to_disk(Lines, Filename)endendfunction edit.mouse_pressed(x,y, mouse_button)if Search_term then return end-- ensure cursor is visible immediately after it movesCursor_time = 0--? print('press', Selection1.line, Selection1.pos)propagate_to_button_handlers(x,y, mouse_button)for line_index,line in ipairs(Lines) doif line.mode == 'text' thenif 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 stateOld_cursor1 = Cursor1Old_selection1 = Selection1Mousepress_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)breakendelseif line.mode == 'drawing' thenif Drawing.in_drawing(line, x, y) thenLines.current_drawing_index = line_indexLines.current_drawing = lineDrawing.before = snapshot(line_index)Drawing.mouse_pressed(line, x,y, mouse_button)breakendendendendfunction edit.mouse_released(x,y, mouse_button)if Search_term then return end--? print('release')-- ensure cursor is visible immediately after it movesCursor_time = 0if Lines.current_drawing thenDrawing.mouse_released(x,y, mouse_button)schedule_save()if Drawing.before thenrecord_undo_event({before=Drawing.before, after=snapshot(Lines.current_drawing_index)})Drawing.before = nilendelsefor line_index,line in ipairs(Lines) doif line.mode == 'text' thenif 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 thenif Old_selection1.line == nil thenSelection1 = Old_cursor1elseSelection1 = Old_selection1endendOld_cursor1, Old_selection1, Mousepress_shift = nilif eq(Cursor1, Selection1) thenSelection1 = {}endbreakendendend--? print('selection:', Selection1.line, Selection1.pos)endendfunction edit.textinput(t)-- ensure cursor is visible immediately after it movesCursor_time = 0for _,line in ipairs(Lines) do line.y = nil end -- just in case we scrollif Search_term thenSearch_term = Search_term..tSearch_text = nilText.search_next()elseif Current_drawing_mode == 'name' thenlocal before = snapshot(Lines.current_drawing_index)local drawing = Lines.current_drawinglocal p = drawing.points[drawing.pending.target_point]p.name = p.name..trecord_undo_event({before=before, after=snapshot(Lines.current_drawing_index)})elseText.textinput(t)endschedule_save()endfunction edit.keychord_pressed(chord, key)-- ensure cursor is visible immediately after it movesCursor_time = 0if Selection1.line andnot 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) andchord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and backspace ~= 'delete' and not App.is_cursor_movement(chord) thenText.delete_selection(Margin_left, App.screen.width-Margin_right)endif Search_term thenif chord == 'escape' thenSearch_term = nilSearch_text = nilCursor1 = Search_backup.cursorScreen_top1 = Search_backup.screen_topSearch_backup = nilText.redraw_all() -- if we're scrolling, reclaim all fragments to avoid memory leakselseif chord == 'return' thenSearch_term = nilSearch_text = nilSearch_backup = nilelseif chord == 'backspace' thenlocal 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 = nilelseif chord == 'down' thenCursor1.pos = Cursor1.pos+1Text.search_next()elseif chord == 'up' thenText.search_previous()endreturnelseif chord == 'C-f' thenSearch_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-=' theninitialize_font_settings(Font_height+2)Text.redraw_all()elseif chord == 'C--' theninitialize_font_settings(Font_height-2)Text.redraw_all()elseif chord == 'C-0' theninitialize_font_settings(20)Text.redraw_all()elseif chord == 'C-z' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal event = undo_event()if event thenlocal src = event.beforeScreen_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 leaksschedule_save()endelseif chord == 'C-y' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal event = redo_event()if event thenlocal src = event.afterScreen_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 leaksschedule_save()end-- clipboardelseif chord == 'C-c' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal s = Text.selection()if s thenApp.setClipboardText(s)endelseif chord == 'C-x' thenfor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrolllocal s = Text.cut_selection(Margin_left, App.screen.width-Margin_right)if s thenApp.setClipboardText(s)endschedule_save()elseif chord == 'C-v' thenfor _,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.linelocal before = snapshot(before_line)local clipboard_data = App.getClipboardText()for _,code in utf8.codes(clipboard_data) dolocal c = utf8.char(code)if c == '\n' thenText.insert_return()elseText.insert_at_cursor(c)endendif Text.cursor_past_screen_bottom() thenText.snap_cursor_to_bottom_of_screen(Margin_left, App.screen.height-Margin_right)endschedule_save()record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})-- dispatch to drawing or textelseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then-- DON'T reset line.y herelocal drawing_index, drawing = Drawing.current_drawing()if drawing_index thenlocal before = snapshot(drawing_index)Drawing.keychord_pressed(chord)record_undo_event({before=before, after=snapshot(drawing_index)})schedule_save()endelseif chord == 'escape' and not App.mouse_down(1) thenfor _,line in ipairs(Lines) doif line.mode == 'drawing' thenline.show_help = falseendendelseif Current_drawing_mode == 'name' thenif chord == 'return' thenCurrent_drawing_mode = Previous_drawing_modePrevious_drawing_mode = nilelselocal before = snapshot(Lines.current_drawing_index)local drawing = Lines.current_drawinglocal p = drawing.points[drawing.pending.target_point]if chord == 'escape' thenp.name = nilrecord_undo_event({before=before, after=snapshot(Lines.current_drawing_index)})elseif chord == 'backspace' thenlocal len = utf8.len(p.name)local byte_offset = Text.offset(p.name, len-1)if len == 1 then byte_offset = 0 endp.name = string.sub(p.name, 1, byte_offset)record_undo_event({before=before, after=snapshot(Lines.current_drawing_index)})endendschedule_save()elsefor _,line in ipairs(Lines) do line.y = nil end -- just in case we scrollText.keychord_pressed(chord)endendfunction edit.key_released(key, scancode)end