streamline the interface for Text.draw
[view.love.git] / source.lua
blob3531bbf13aeeb92207e4a41d32cbbe94b55a53f7
1 source = {}
3 Editor_state = {}
5 -- called both in tests and real run
6 function source.initialize_globals()
7 -- tests currently mostly clear their own state
9 Show_log_browser_side = false
10 Focus = 'edit'
11 Show_file_navigator = false
12 File_navigation = {
13 all_candidates = {
14 'run',
15 'run_tests',
16 'log',
17 'edit',
18 'drawing',
19 'help',
20 'text',
21 'search',
22 'select',
23 'undo',
24 'text_tests',
25 'geom',
26 'drawing_tests',
27 'file',
28 'source',
29 'source_tests',
30 'commands',
31 'log_browser',
32 'source_edit',
33 'source_text',
34 'source_undo',
35 'colorize',
36 'source_text_tests',
37 'source_file',
38 'main',
39 'button',
40 'keychord',
41 'app',
42 'test',
43 'json',
45 index = 1,
46 filter = '',
47 cursors = {}, -- filename to cursor1, screen_top1
49 File_navigation.candidates = File_navigation.all_candidates -- modified with filter
51 Menu_status_bar_height = 5 + --[[line height in tests]] 15 + 5
53 -- a few text objects we can avoid recomputing unless the font changes
54 Text_cache = {}
56 -- blinking cursor
57 Cursor_time = 0
58 end
60 -- called only for real run
61 function source.initialize()
62 log_new('source')
63 if Settings and Settings.source then
64 source.load_settings()
65 else
66 source.initialize_default_settings()
67 end
69 source.initialize_edit_side()
70 source.initialize_log_browser_side()
72 Menu_status_bar_height = 5 + Editor_state.line_height + 5
73 Editor_state.top = Editor_state.top + Menu_status_bar_height
74 Log_browser_state.top = Log_browser_state.top + Menu_status_bar_height
78 -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
79 love.window.setTitle('lines.love - source')
83 end
85 -- environment for a mutable file
86 -- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up.
87 function source.initialize_edit_side()
88 load_from_disk(Editor_state)
89 Text.redraw_all(Editor_state)
90 if File_navigation.cursors[Editor_state.filename] then
91 Editor_state.screen_top1 = File_navigation.cursors[Editor_state.filename].screen_top1
92 Editor_state.cursor1 = File_navigation.cursors[Editor_state.filename].cursor1
93 else
94 Editor_state.screen_top1 = {line=1, pos=1}
95 Editor_state.cursor1 = {line=1, pos=1}
96 end
97 edit.check_locs(Editor_state)
99 if Editor_state.cursor1.line > #Editor_state.lines then
100 Editor_state.cursor1 = {line=1, pos=1}
102 if Editor_state.screen_top1.line > #Editor_state.lines then
103 Editor_state.screen_top1 = {line=1, pos=1}
106 if rawget(_G, 'jit') then
107 jit.off()
108 jit.flush()
112 function source.load_settings()
113 local settings = Settings.source
114 love.graphics.setFont(love.graphics.newFont(settings.font_height))
115 source.resize_window_from_settings(settings)
116 --? print('loading source position', settings.x, settings.y, settings.displayindex)
117 source.set_window_position_from_settings(settings)
118 Show_log_browser_side = settings.show_log_browser_side
119 local right = App.screen.width - Margin_right
120 if Show_log_browser_side then
121 right = App.screen.width/2 - Margin_right
123 Editor_state = edit.initialize_state(Margin_top, Margin_left, right, settings.font_height, math.floor(settings.font_height*1.3))
124 Editor_state.filename = settings.filename
125 Editor_state.filename = basename(Editor_state.filename) -- migrate settings that used full paths; we now support only relative paths within the app
126 if settings.cursors then
127 File_navigation.cursors = settings.cursors
128 Editor_state.screen_top1 = File_navigation.cursors[Editor_state.filename].screen_top1
129 Editor_state.cursor1 = File_navigation.cursors[Editor_state.filename].cursor1
130 else
131 -- migrate old settings
132 Editor_state.screen_top1 = {line=1, pos=1}
133 Editor_state.cursor1 = {line=1, pos=1}
137 function source.resize_window_from_settings(settings)
138 local os = love.system.getOS()
139 if os == 'Android' or os == 'iOS' then
140 -- maximizing on iOS breaks text rendering: https://github.com/deltadaedalus/vudu/issues/7
141 -- no point second-guessing window dimensions on mobile
142 App.screen.width, App.screen.height, App.screen.flags = App.screen.size()
143 return
145 -- maximize window to determine maximum allowable dimensions
146 App.screen.resize(0, 0) -- maximize
147 Display_width, Display_height, App.screen.flags = App.screen.size()
148 -- set up desired window dimensions
149 App.screen.flags.resizable = true
150 App.screen.flags.minwidth = math.min(Display_width, 200)
151 App.screen.flags.minheight = math.min(Display_height, 200)
152 App.screen.width, App.screen.height = settings.width, settings.height
153 --? print('setting window from settings:', App.screen.width, App.screen.height)
154 App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
157 function source.set_window_position_from_settings(settings)
158 -- love.window.setPosition doesn't quite seem to do what is asked of it on Linux.
159 App.screen.move(settings.x, settings.y-37, settings.displayindex)
162 function source.initialize_default_settings()
163 local font_height = 20
164 love.graphics.setFont(love.graphics.newFont(font_height))
165 source.initialize_window_geometry(App.width('m'))
166 Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)
167 Editor_state.filename = 'run.lua'
168 Editor_state.font_height = font_height
169 Editor_state.line_height = math.floor(font_height*1.3)
172 function source.initialize_window_geometry(em_width)
173 local os = love.system.getOS()
174 if os == 'Android' or os == 'iOS' then
175 -- maximizing on iOS breaks text rendering: https://github.com/deltadaedalus/vudu/issues/7
176 -- no point second-guessing window dimensions on mobile
177 App.screen.width, App.screen.height, App.screen.flags = App.screen.size()
178 return
180 -- maximize window
181 App.screen.resize(0, 0) -- maximize
182 Display_width, Display_height, App.screen.flags = App.screen.size()
183 -- shrink height slightly to account for window decoration
184 App.screen.height = Display_height-100
185 App.screen.width = 40*em_width
186 App.screen.flags.resizable = true
187 App.screen.flags.minwidth = math.min(App.screen.width, 200)
188 App.screen.flags.minheight = math.min(App.screen.height, 200)
189 App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
190 print('initializing source position')
191 if Settings == nil then Settings = {} end
192 if Settings.source == nil then Settings.source = {} end
193 Settings.source.x, Settings.source.y, Settings.source.displayindex = App.screen.position()
196 function source.resize(w, h)
197 --? print(("Window resized to width: %d and height: %d."):format(w, h))
198 App.screen.width, App.screen.height = w, h
199 Text.redraw_all(Editor_state)
200 Editor_state.selection1 = {} -- no support for shift drag while we're resizing
201 if Show_log_browser_side then
202 Editor_state.right = App.screen.width/2 - Margin_right
203 else
204 Editor_state.right = App.screen.width-Margin_right
206 Log_browser_state.left = App.screen.width/2 + Margin_right
207 Log_browser_state.right = App.screen.width-Margin_right
208 Editor_state.width = Editor_state.right-Editor_state.left
209 Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
210 --? print('end resize')
213 function source.file_drop(file)
214 -- first make sure to save edits on any existing file
215 if Editor_state.next_save then
216 save_to_disk(Editor_state)
218 -- clear the slate for the new file
219 Editor_state.filename = file:getFilename()
220 file:open('r')
221 Editor_state.lines = load_from_file(file)
222 file:close()
223 Text.redraw_all(Editor_state)
224 Editor_state.screen_top1 = {line=1, pos=1}
225 Editor_state.cursor1 = {line=1, pos=1}
229 -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
230 love.window.setTitle('lines.love - source')
236 -- a copy of source.file_drop when given a filename
237 function source.switch_to_file(filename)
238 -- first make sure to save edits on any existing file
239 if Editor_state.next_save then
240 save_to_disk(Editor_state)
242 -- save cursor position
243 File_navigation.cursors[Editor_state.filename] = {cursor1=Editor_state.cursor1, screen_top1=Editor_state.screen_top1}
244 -- clear the slate for the new file
245 Editor_state.filename = filename
246 load_from_disk(Editor_state)
247 Text.redraw_all(Editor_state)
248 if File_navigation.cursors[filename] then
249 Editor_state.screen_top1 = File_navigation.cursors[filename].screen_top1
250 Editor_state.cursor1 = File_navigation.cursors[filename].cursor1
251 else
252 Editor_state.screen_top1 = {line=1, pos=1}
253 Editor_state.cursor1 = {line=1, pos=1}
257 function source.draw()
258 edit.draw(Editor_state, --[[hide cursor?]] Show_file_navigator)
259 if Show_log_browser_side then
260 -- divider
261 App.color(Divider_color)
262 love.graphics.rectangle('fill', App.screen.width/2-1,Menu_status_bar_height, 3,App.screen.height)
264 log_browser.draw(Log_browser_state)
266 source.draw_menu_bar()
269 function source.update(dt)
270 Cursor_time = Cursor_time + dt
271 if App.mouse_x() < Editor_state.right then
272 edit.update(Editor_state, dt)
273 elseif Show_log_browser_side then
274 log_browser.update(Log_browser_state, dt)
278 function source.quit()
279 edit.quit(Editor_state)
280 log_browser.quit(Log_browser_state)
283 function source.settings()
284 if Current_app == 'source' then
285 --? print('reading source window position')
286 Settings.source.x, Settings.source.y, Settings.source.displayindex = App.screen.position()
288 --? print('saving source settings', Settings.source.x, Settings.source.y, Settings.source.displayindex)
289 File_navigation.cursors[Editor_state.filename] = {cursor1=Editor_state.cursor1, screen_top1=Editor_state.screen_top1}
290 return {
291 x=Settings.source.x, y=Settings.source.y, displayindex=Settings.source.displayindex,
292 width=App.screen.width, height=App.screen.height,
293 font_height=Editor_state.font_height,
294 filename=Editor_state.filename,
295 cursors=File_navigation.cursors,
296 show_log_browser_side=Show_log_browser_side,
297 focus=Focus,
301 function source.mouse_press(x,y, mouse_button)
302 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
303 --? print('mouse click', x, y)
304 --? print(Editor_state.left, Editor_state.right)
305 --? print(Log_browser_state.left, Log_browser_state.right)
306 if Show_file_navigator and y < Menu_status_bar_height + File_navigation.num_lines * Editor_state.line_height then
307 -- send click to buttons
308 edit.mouse_press(Editor_state, x,y, mouse_button)
309 return
311 if x < Editor_state.right + Margin_right then
312 --? print('click on edit side')
313 if Focus ~= 'edit' then
314 Focus = 'edit'
315 return
317 edit.mouse_press(Editor_state, x,y, mouse_button)
318 elseif Show_log_browser_side and Log_browser_state.left <= x and x < Log_browser_state.right then
319 --? print('click on log_browser side')
320 if Focus ~= 'log_browser' then
321 Focus = 'log_browser'
322 return
324 log_browser.mouse_press(Log_browser_state, x,y, mouse_button)
325 for _,line_cache in ipairs(Editor_state.line_cache) do line_cache.starty = nil end -- just in case we scroll
329 function source.mouse_release(x,y, mouse_button)
330 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
331 if Focus == 'edit' then
332 return edit.mouse_release(Editor_state, x,y, mouse_button)
333 else
334 return log_browser.mouse_release(Log_browser_state, x,y, mouse_button)
338 function source.mouse_wheel_move(dx,dy)
339 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
340 if Focus == 'edit' then
341 return edit.mouse_wheel_move(Editor_state, dx,dy)
342 else
343 return log_browser.mouse_wheel_move(Log_browser_state, dx,dy)
347 function source.text_input(t)
348 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
349 if Show_file_navigator then
350 text_input_on_file_navigator(t)
351 return
353 if Focus == 'edit' then
354 return edit.text_input(Editor_state, t)
355 else
356 return log_browser.text_input(Log_browser_state, t)
360 function source.keychord_press(chord, key)
361 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
362 --? print('source keychord')
363 if Show_file_navigator then
364 keychord_press_on_file_navigator(chord, key)
365 return
367 if chord == 'C-l' then
368 --? print('C-l')
369 Show_log_browser_side = not Show_log_browser_side
370 if Show_log_browser_side then
371 App.screen.width = math.min(Display_width, App.screen.width*2)
372 Editor_state.right = App.screen.width/2 - Margin_right
373 Log_browser_state.left = App.screen.width/2 + Margin_left
374 Log_browser_state.right = App.screen.width - Margin_right
375 else
376 App.screen.width = Editor_state.right + Margin_right
378 --? print('setting window:', App.screen.width, App.screen.height)
379 App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
380 --? print('done setting window')
381 -- try to restore position if possible
382 -- if the window gets wider the window manager may not respect this
383 if not App.run_tests then
384 source.set_window_position_from_settings(Settings.source)
386 return
388 if chord == 'C-k' then
389 -- clear logs
390 love.filesystem.remove('log')
391 -- restart to reload state of logs on screen
392 Settings.source = source.settings()
393 source.quit()
394 love.filesystem.write('config', json.encode(Settings))
395 load_file_from_source_or_save_directory('main.lua')
396 App.undo_initialize()
397 App.run_tests_and_initialize()
398 return
400 if chord == 'C-g' then
401 Show_file_navigator = true
402 return
404 if Focus == 'edit' then
405 return edit.keychord_press(Editor_state, chord, key)
406 else
407 return log_browser.keychord_press(Log_browser_state, chord, key)
411 function source.key_release(key, scancode)
412 Cursor_time = 0 -- ensure cursor is visible immediately after it moves
413 if Focus == 'edit' then
414 return edit.key_release(Editor_state, key, scancode)
415 else
416 return log_browser.keychord_press(Log_browser_state, chordkey, scancode)