source.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. source = {}
  2. Editor_state = {}
  3. Line_number_width = 3 -- in ems
  4. -- called both in tests and real run
  5. function source.initialize_globals()
  6. -- tests currently mostly clear their own state
  7. Show_log_browser_side = false
  8. Focus = 'edit'
  9. Show_file_navigator = false
  10. File_navigation = {
  11. all_candidates = {
  12. 'run',
  13. 'run_tests',
  14. 'log',
  15. 'edit',
  16. 'drawing',
  17. 'help',
  18. 'text',
  19. 'search',
  20. 'select',
  21. 'undo',
  22. 'text_tests',
  23. 'geom',
  24. 'drawing_tests',
  25. 'file',
  26. 'source',
  27. 'source_tests',
  28. 'commands',
  29. 'log_browser',
  30. 'source_edit',
  31. 'source_text',
  32. 'source_undo',
  33. 'colorize',
  34. 'source_text_tests',
  35. 'source_file',
  36. 'main',
  37. 'button',
  38. 'keychord',
  39. 'app',
  40. 'test',
  41. 'json',
  42. },
  43. index = 1,
  44. filter = '',
  45. cursors = {}, -- filename to cursor1, screen_top1
  46. }
  47. File_navigation.candidates = File_navigation.all_candidates -- modified with filter
  48. Menu_status_bar_height = 5 + --[[line height in tests]] 15 + 5
  49. -- blinking cursor
  50. Cursor_time = 0
  51. end
  52. -- called only for real run
  53. function source.initialize()
  54. log_new('source')
  55. if Settings and Settings.source then
  56. source.load_settings()
  57. else
  58. source.initialize_default_settings()
  59. end
  60. source.initialize_edit_side()
  61. source.initialize_log_browser_side()
  62. Menu_status_bar_height = 5 + Editor_state.line_height + 5
  63. Editor_state.top = Editor_state.top + Menu_status_bar_height
  64. Log_browser_state.top = Log_browser_state.top + Menu_status_bar_height
  65. -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
  66. love.window.setTitle('text.love - source - '..Editor_state.filename)
  67. end
  68. -- environment for a mutable file
  69. -- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up.
  70. function source.initialize_edit_side()
  71. load_from_disk(Editor_state)
  72. Text.redraw_all(Editor_state)
  73. if File_navigation.cursors[Editor_state.filename] then
  74. Editor_state.screen_top1 = File_navigation.cursors[Editor_state.filename].screen_top1
  75. Editor_state.cursor1 = File_navigation.cursors[Editor_state.filename].cursor1
  76. else
  77. Editor_state.screen_top1 = {line=1, pos=1}
  78. Editor_state.cursor1 = {line=1, pos=1}
  79. end
  80. edit.check_locs(Editor_state)
  81. if Editor_state.cursor1.line > #Editor_state.lines then
  82. Editor_state.cursor1 = {line=1, pos=1}
  83. end
  84. if Editor_state.screen_top1.line > #Editor_state.lines then
  85. Editor_state.screen_top1 = {line=1, pos=1}
  86. end
  87. if rawget(_G, 'jit') then
  88. jit.off()
  89. jit.flush()
  90. end
  91. end
  92. function print_and_log(s)
  93. print(s)
  94. log(3, s)
  95. end
  96. function source.load_settings()
  97. local settings = Settings.source
  98. local font = love.graphics.newFont(settings.font_height)
  99. -- set up desired window dimensions and make window resizable
  100. _, _, App.screen.flags = App.screen.size()
  101. App.screen.flags.resizable = true
  102. App.screen.width, App.screen.height = settings.width, settings.height
  103. App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
  104. source.set_window_position_from_settings(settings)
  105. Show_log_browser_side = settings.show_log_browser_side
  106. local right = App.screen.width - Margin_right
  107. if Show_log_browser_side then
  108. right = App.screen.width/2 - Margin_right
  109. end
  110. Editor_state = edit.initialize_state(Margin_top, Margin_left + Line_number_width*font:getWidth('m'), right, font, settings.font_height, math.floor(settings.font_height*1.3))
  111. Editor_state.filename = settings.filename
  112. Editor_state.filename = basename(Editor_state.filename) -- migrate settings that used full paths; we now support only relative paths within the app
  113. if settings.cursors then
  114. File_navigation.cursors = settings.cursors
  115. Editor_state.screen_top1 = File_navigation.cursors[Editor_state.filename].screen_top1
  116. Editor_state.cursor1 = File_navigation.cursors[Editor_state.filename].cursor1
  117. else
  118. -- migrate old settings
  119. Editor_state.screen_top1 = {line=1, pos=1}
  120. Editor_state.cursor1 = {line=1, pos=1}
  121. end
  122. end
  123. function source.set_window_position_from_settings(settings)
  124. local os = love.system.getOS()
  125. if os == 'Linux' then
  126. -- love.window.setPosition doesn't quite seem to do what is asked of it on Linux.
  127. App.screen.move(settings.x, settings.y-37, settings.displayindex)
  128. else
  129. App.screen.move(settings.x, settings.y, settings.displayindex)
  130. end
  131. end
  132. function source.initialize_default_settings()
  133. local font_height = 20
  134. local font = love.graphics.newFont(font_height)
  135. source.initialize_window_geometry()
  136. Editor_state = edit.initialize_state(Margin_top, Margin_left + Line_number_width*font:getWidth('m'), App.screen.width-Margin_right, font, font_height, math.floor(font_height*1.3))
  137. Editor_state.filename = 'run.lua'
  138. end
  139. function source.initialize_window_geometry()
  140. -- Initialize window width/height and make window resizable.
  141. --
  142. -- I get tempted to have opinions about window dimensions here, but they're
  143. -- non-portable:
  144. -- - maximizing doesn't work on mobile and messes things up
  145. -- - maximizing keeps the title bar on screen in Linux, but off screen on
  146. -- Windows. And there's no way to get the height of the title bar.
  147. -- It seems more robust to just follow LÖVE's default window size until
  148. -- someone overrides it.
  149. App.screen.width, App.screen.height, App.screen.flags = App.screen.size()
  150. App.screen.flags.resizable = true
  151. App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
  152. end
  153. function source.resize(w, h)
  154. --? print(("Window resized to width: %d and height: %d."):format(w, h))
  155. App.screen.width, App.screen.height = w, h
  156. Text.redraw_all(Editor_state)
  157. Editor_state.selection1 = {} -- no support for shift drag while we're resizing
  158. if Show_log_browser_side then
  159. Editor_state.right = App.screen.width/2 - Margin_right
  160. else
  161. Editor_state.right = App.screen.width-Margin_right
  162. end
  163. Log_browser_state.left = App.screen.width/2 + Margin_right
  164. Log_browser_state.right = App.screen.width-Margin_right
  165. Editor_state.width = Editor_state.right-Editor_state.left
  166. Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
  167. --? print('end resize')
  168. end
  169. function source.file_drop(file)
  170. -- first make sure to save edits on any existing file
  171. if Editor_state.next_save then
  172. save_to_disk(Editor_state)
  173. end
  174. -- clear the slate for the new file
  175. Editor_state.filename = file:getFilename()
  176. file:open('r')
  177. Editor_state.lines = load_from_file(file)
  178. file:close()
  179. Text.redraw_all(Editor_state)
  180. Editor_state.screen_top1 = {line=1, pos=1}
  181. Editor_state.cursor1 = {line=1, pos=1}
  182. -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
  183. love.window.setTitle('text.love - source')
  184. end
  185. -- a copy of source.file_drop when given a filename
  186. function source.switch_to_file(filename)
  187. -- first make sure to save edits on any existing file
  188. if Editor_state.next_save then
  189. save_to_disk(Editor_state)
  190. end
  191. -- save cursor position
  192. File_navigation.cursors[Editor_state.filename] = {cursor1=Editor_state.cursor1, screen_top1=Editor_state.screen_top1}
  193. -- clear the slate for the new file
  194. Editor_state.filename = filename
  195. load_from_disk(Editor_state)
  196. Text.redraw_all(Editor_state)
  197. if File_navigation.cursors[filename] then
  198. Editor_state.screen_top1 = File_navigation.cursors[filename].screen_top1
  199. Editor_state.cursor1 = File_navigation.cursors[filename].cursor1
  200. else
  201. Editor_state.screen_top1 = {line=1, pos=1}
  202. Editor_state.cursor1 = {line=1, pos=1}
  203. end
  204. end
  205. function source.draw()
  206. edit.draw(Editor_state, --[[hide cursor?]] Show_file_navigator, --[[show line numbers]] true)
  207. if Show_log_browser_side then
  208. -- divider
  209. App.color(Divider_color)
  210. love.graphics.rectangle('fill', App.screen.width/2-1,Menu_status_bar_height, 3,App.screen.height)
  211. --
  212. log_browser.draw(Log_browser_state, --[[hide_cursor]] Focus ~= 'log_browser')
  213. end
  214. source.draw_menu_bar()
  215. if Error_message then
  216. local height = math.min(20*Editor_state.line_height, App.screen.height*0.2)
  217. App.color{r=0.8,g=0,b=0}
  218. love.graphics.rectangle('fill', 150, App.screen.height - height-10, App.screen.width, height+10)
  219. App.color{r=0,g=0,b=0}
  220. love.graphics.print(Error_message, 150+10, App.screen.height - height)
  221. end
  222. end
  223. function source.update(dt)
  224. Cursor_time = Cursor_time + dt
  225. if App.mouse_x() < Editor_state.right then
  226. edit.update(Editor_state, dt)
  227. elseif Show_log_browser_side then
  228. log_browser.update(Log_browser_state, dt)
  229. end
  230. end
  231. function source.quit()
  232. edit.quit(Editor_state)
  233. log_browser.quit(Log_browser_state)
  234. end
  235. function source.settings()
  236. if Settings == nil then Settings = {} end
  237. if Settings.source == nil then Settings.source = {} end
  238. Settings.source.x, Settings.source.y, Settings.source.displayindex = App.screen.position()
  239. File_navigation.cursors[Editor_state.filename] = {cursor1=Editor_state.cursor1, screen_top1=Editor_state.screen_top1}
  240. return {
  241. x=Settings.source.x, y=Settings.source.y, displayindex=Settings.source.displayindex,
  242. width=App.screen.width, height=App.screen.height,
  243. font_height=Editor_state.font_height,
  244. filename=Editor_state.filename,
  245. cursors=File_navigation.cursors,
  246. show_log_browser_side=Show_log_browser_side,
  247. focus=Focus,
  248. }
  249. end
  250. function source.mouse_press(x,y, mouse_button)
  251. Cursor_time = 0 -- ensure cursor is visible immediately after it moves
  252. love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
  253. --? print('mouse click', x, y)
  254. --? print(Editor_state.left, Editor_state.right)
  255. --? print(Log_browser_state.left, Log_browser_state.right)
  256. if Show_file_navigator and y < Menu_status_bar_height + File_navigation.num_lines * Editor_state.line_height then
  257. -- send click to buttons
  258. edit.mouse_press(Editor_state, x,y, mouse_button)
  259. return
  260. end
  261. if x < Editor_state.right + Margin_right then
  262. --? print('click on edit side')
  263. if Focus ~= 'edit' then
  264. Focus = 'edit'
  265. return
  266. end
  267. edit.mouse_press(Editor_state, x,y, mouse_button)
  268. elseif Show_log_browser_side and Log_browser_state.left <= x and x < Log_browser_state.right then
  269. --? print('click on log_browser side')
  270. if Focus ~= 'log_browser' then
  271. Focus = 'log_browser'
  272. return
  273. end
  274. log_browser.mouse_press(Log_browser_state, x,y, mouse_button)
  275. end
  276. end
  277. function source.mouse_release(x,y, mouse_button)
  278. Cursor_time = 0 -- ensure cursor is visible immediately after it moves
  279. if Focus == 'edit' then
  280. return edit.mouse_release(Editor_state, x,y, mouse_button)
  281. else
  282. return log_browser.mouse_release(Log_browser_state, x,y, mouse_button)
  283. end
  284. end
  285. function source.mouse_wheel_move(dx,dy)
  286. Cursor_time = 0 -- ensure cursor is visible immediately after it moves
  287. if Focus == 'edit' then
  288. return edit.mouse_wheel_move(Editor_state, dx,dy)
  289. else
  290. return log_browser.mouse_wheel_move(Log_browser_state, dx,dy)
  291. end
  292. end
  293. function source.text_input(t)
  294. Cursor_time = 0 -- ensure cursor is visible immediately after it moves
  295. if Show_file_navigator then
  296. text_input_on_file_navigator(t)
  297. return
  298. end
  299. if Focus == 'edit' then
  300. return edit.text_input(Editor_state, t)
  301. else
  302. return log_browser.text_input(Log_browser_state, t)
  303. end
  304. end
  305. function source.keychord_press(chord, key)
  306. Cursor_time = 0 -- ensure cursor is visible immediately after it moves
  307. --? print('source keychord')
  308. if Show_file_navigator then
  309. keychord_press_on_file_navigator(chord, key)
  310. return
  311. end
  312. if chord == 'C-l' then
  313. --? print('C-l')
  314. Show_log_browser_side = not Show_log_browser_side
  315. if Show_log_browser_side then
  316. Editor_state.right = App.screen.width/2 - Margin_right
  317. Editor_state.width = Editor_state.right-Editor_state.left
  318. Text.redraw_all(Editor_state)
  319. Log_browser_state.left = App.screen.width/2 + Margin_left
  320. Log_browser_state.right = App.screen.width - Margin_right
  321. else
  322. Editor_state.right = App.screen.width - Margin_right
  323. Editor_state.width = Editor_state.right-Editor_state.left
  324. Text.redraw_all(Editor_state)
  325. end
  326. return
  327. end
  328. if chord == 'C-k' then
  329. -- clear logs
  330. love.filesystem.remove('log')
  331. -- restart to reload state of logs on screen
  332. Settings.source = source.settings()
  333. source.quit()
  334. love.filesystem.write('config', json.encode(Settings))
  335. load_file_from_source_or_save_directory('main.lua')
  336. App.undo_initialize()
  337. App.run_tests_and_initialize()
  338. return
  339. end
  340. if chord == 'C-g' then
  341. Show_file_navigator = true
  342. return
  343. end
  344. if Focus == 'edit' then
  345. return edit.keychord_press(Editor_state, chord, key)
  346. else
  347. return log_browser.keychord_press(Log_browser_state, chord, key)
  348. end
  349. end
  350. function source.key_release(key, scancode)
  351. Cursor_time = 0 -- ensure cursor is visible immediately after it moves
  352. if Focus == 'edit' then
  353. return edit.key_release(Editor_state, key, scancode)
  354. else
  355. return log_browser.keychord_press(Log_browser_state, chordkey, scancode)
  356. end
  357. end