2022-09-04 01:10:12 +00:00
Column_header_color = { r = 0.7 , g = 0.7 , b = 0.7 }
Pane_title_color = { r = 0.5 , g = 0.5 , b = 0.5 }
Pane_title_background_color = { r = 0 , g = 0 , b = 0 , a = 0.1 }
Pane_background_color = { r = 0.7 , g = 0.7 , b = 0.7 , a = 0.1 }
Grab_background_color = { r = 0.7 , g = 0.7 , b = 0.7 }
Cursor_pane_background_color = { r = 0.7 , g = 0.7 , b = 0 , a = 0.1 }
Menu_background_color = { r = 0.6 , g = 0.8 , b = 0.6 }
Menu_border_color = { r = 0.6 , g = 0.7 , b = 0.6 }
Menu_command_color = { r = 0.2 , g = 0.2 , b = 0.2 }
Command_palette_background_color = Menu_background_color
Command_palette_border_color = Menu_border_color
Command_palette_command_color = Menu_command_color
Command_palette_alternatives_background_color = Menu_background_color
Command_palette_highlighted_alternative_background_color = { r = 0.5 , g = 0.7 , b = 0.3 }
Command_palette_alternatives_color = { r = 0.3 , g = 0.5 , b = 0.3 }
Crosslink_color = { r = 0 , g = 0.7 , b = 0.7 }
Crosslink_background_color = { r = 0 , g = 0 , b = 0 , a = 0.1 }
2022-09-03 21:13:22 +00:00
run = { }
2022-12-11 23:58:38 +00:00
profile = require ' profile '
2022-09-04 01:10:12 +00:00
-- The note-taking app has a few differences with the baseline editor it's
-- forked from:
-- - most notes are read-only
-- - the editor operates entirely in viewport-relative coordinates; 0,0 is
-- the top-left corner of the window. However the note-taking app in
-- read-only mode largely operates in absolute coordinates; a potentially
-- large 2D space that the window is just a peephole into.
--
-- We'll use the rendering logic in the editor, but only use its event loop
-- when a window is being edited (there can only be one all over the entire
-- surface)
--
-- Most of the time the viewport affects each pane's top and screen_top. An
-- exception is when you're editing a pane and you scroll the cursor inside
-- it. In that case we want to affect the viewport (for all panes) based on
-- the editable pane's screen_top.
2022-09-03 21:13:22 +00:00
Editor_state = { }
-- called both in tests and real run
function run . initialize_globals ( )
2022-09-04 01:10:12 +00:00
-- stuff we paginate over is organized as follows:
-- - there are multiple columns
-- - each column contains panes
-- - each pane contains editor state as in lines.love
Surface = { }
2022-11-23 05:49:11 +00:00
-- The surface may show the same file in multiple panes. Share links between
-- files (which will never go in Surface).
Links = { }
2022-09-04 01:10:12 +00:00
-- LÖVE renders N frames per second like any game engine, but we don't
-- really need that. The only thing that animates in this app is the cursor.
--
-- Until I fix that, the architecture of this app will be to plan what to
-- draw only when something changes. That way we minimize the amount of
-- computation/power wasted on each of those frames.
Panes_to_draw = { } -- array of panes from surface
Column_headers_to_draw = { } -- strings with x coordinates
Display_settings = {
mode = ' normal ' ,
-- valid modes:
-- normal (show full surface)
-- maximize (show just a single note; focus mode)
-- search (notes currently on surface)
-- search_all (notes in directory)
-- searching_all (search in progress)
x = 0 , y = 0 , -- <==== Top-left corner of the viewport into the surface
column_width = 400 ,
2022-11-16 07:44:54 +00:00
show_debug = false ,
2022-09-04 01:10:12 +00:00
show_palette = false ,
palette_command = ' ' ,
palette_command_text = App.newText ( love.graphics . getFont ( ) , ' ' ) ,
palette_alternative_index = 1 , palette_candidates = nil ,
search_term = ' ' , search_text = nil ,
search_backup_x = nil , search_backup_y = nil , search_backup_cursor_pane = nil ,
search_all_query = nil , search_all_query_text = nil , search_all_terms = nil ,
search_all_progress_indicator = nil ,
search_all_pane = nil , search_all_state = nil ,
}
-- display settings that are constants
Font_height = 20
Line_height = math.floor ( Font_height * 1.3 )
-- space saved for headers
-- this is only on the screen, not used on the surface itself
Menu_status_bar_height = 5 + Line_height + 5
--? print('menu height', Menu_status_bar_height)
Column_header_height = 5 + Line_height + 5
--? print('column header height', Column_header_height)
Header_height = Menu_status_bar_height + Column_header_height
-- padding is the space between panes on the surface
Padding_vertical = 20 -- space between panes
Padding_horizontal = 20
-- margins are extra space inside the borders of panes on the surface
Margin_above = 10
Margin_below = 10
Pan_step = 10
Pan = { }
Cursor_pane = { col = 0 , row = 1 } -- surface column and row index, along with some cached data
-- occasional secondary cursor
Grab_pane = nil
-- where we store our notes (pane id is also a relative path under there)
Directory = ' data/ '
2022-09-03 21:13:22 +00:00
-- a few text objects we can avoid recomputing unless the font changes
Text_cache = { }
-- blinking cursor
Cursor_time = 0
2022-10-27 06:13:22 +00:00
2022-11-20 21:40:22 +00:00
-- a read-only buffer for errors
2022-12-10 06:15:16 +00:00
Error_log = edit.initialize_state ( 0 , 0 , Display_settings.column_width , Font_height , Line_height )
2022-12-02 20:36:57 +00:00
Error_log.id = ' errors '
Error_log.filename = Directory .. ' errors '
Error_log.editable = false
Text.redraw_all ( Error_log )
2022-10-27 06:13:22 +00:00
Current_error = nil
Current_error_text = nil
Current_error_time = nil
2022-09-03 21:13:22 +00:00
end
-- called only for real run
function run . initialize ( arg )
2022-09-18 03:37:40 +00:00
log_new ( ' run ' )
2022-09-03 21:13:22 +00:00
love.keyboard . setTextInput ( true ) -- bring up keyboard on touch screen
love.keyboard . setKeyRepeat ( true )
2022-09-04 01:10:12 +00:00
Editor_state = nil -- not used outside editor tests
2022-09-03 21:13:22 +00:00
love.graphics . setBackgroundColor ( 1 , 1 , 1 )
2022-09-04 01:10:12 +00:00
assert ( # arg <= 1 )
2022-09-10 05:28:52 +00:00
if # arg == 1 then
2022-09-04 01:10:12 +00:00
Directory = ' data. ' .. arg [ 1 ] .. ' / '
2022-09-03 21:13:22 +00:00
end
2022-09-04 01:10:12 +00:00
love.window . setTitle ( ' pensieve.love ' )
print ( ' reading notes from ' .. love.filesystem . getSaveDirectory ( ) .. ' / ' .. Directory )
print ( ' put any notes there (and make frequent backups) ' )
2022-09-10 05:31:27 +00:00
if Settings.width then
2022-09-04 01:10:12 +00:00
load_settings ( )
2022-09-03 21:13:22 +00:00
else
2022-09-04 01:10:12 +00:00
initialize_default_settings ( )
2022-09-03 21:13:22 +00:00
end
2022-09-04 01:10:12 +00:00
if Display_settings.column_width > App.screen . width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal then
Display_settings.column_width = math.max ( 200 , App.screen . width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal )
2022-09-03 21:13:22 +00:00
end
2022-09-04 01:10:12 +00:00
Cursor_pane.col = math.min ( Cursor_pane.col , # Surface )
if Cursor_pane.col >= 1 then
Cursor_pane.row = math.min ( Cursor_pane.row , # Surface [ Cursor_pane.col ] )
end
plan_draw ( )
2022-09-03 21:13:22 +00:00
if rawget ( _G , ' jit ' ) then
jit.off ( )
jit.flush ( )
end
end
2022-09-04 01:10:12 +00:00
function load_settings ( )
2022-09-03 21:13:22 +00:00
-- maximize window to determine maximum allowable dimensions
2022-09-04 01:10:12 +00:00
love.window . setMode ( 0 , 0 ) -- maximize
2022-09-03 21:13:22 +00:00
App.screen . width , App.screen . height , App.screen . flags = love.window . getMode ( )
2022-09-04 01:10:12 +00:00
--? print('max height', App.screen.height)
2022-09-03 21:13:22 +00:00
-- set up desired window dimensions
App.screen . flags.resizable = true
App.screen . flags.minwidth = math.min ( App.screen . width , 200 )
App.screen . flags.minheight = math.min ( App.screen . width , 200 )
2022-09-10 05:31:27 +00:00
App.screen . width , App.screen . height = Settings.width , Settings.height
2022-09-03 21:13:22 +00:00
love.window . setMode ( App.screen . width , App.screen . height , App.screen . flags )
2022-09-10 05:31:27 +00:00
love.window . setPosition ( Settings.x , Settings.y , Settings.displayindex )
Font_height = Settings.font_height
2022-09-04 01:10:12 +00:00
Line_height = math.floor ( Font_height * 1.3 )
love.graphics . setFont ( love.graphics . newFont ( Font_height ) )
Em = App.newText ( love.graphics . getFont ( ) , ' m ' )
2022-09-10 05:31:27 +00:00
Display_settings.column_width = Settings.column_width
for _ , column_name in ipairs ( Settings.columns ) do
2022-09-04 01:10:12 +00:00
create_column ( column_name )
end
2022-09-10 05:31:27 +00:00
Cursor_pane.col = Settings.cursor_col
Cursor_pane.row = Settings.cursor_row
Display_settings.x = Settings.surface_x
Display_settings.y = Settings.surface_y
2022-09-04 01:10:12 +00:00
end
function initialize_default_settings ( )
initialize_window_geometry ( )
love.graphics . setFont ( love.graphics . newFont ( Font_height ) )
Em = App.newText ( love.graphics . getFont ( ) , ' m ' )
Display_settings.column_width = 40 * App.width ( Em )
-- initialize surface with a single column
command.recently_modified ( )
end
function initialize_window_geometry ( )
2022-09-03 21:13:22 +00:00
-- maximize window
love.window . setMode ( 0 , 0 ) -- maximize
App.screen . width , App.screen . height , App.screen . flags = love.window . getMode ( )
-- shrink height slightly to account for window decoration
App.screen . height = App.screen . height - 100
2022-09-04 01:10:12 +00:00
App.screen . width = App.screen . width - 100
2022-09-03 21:13:22 +00:00
App.screen . flags.resizable = true
App.screen . flags.minwidth = math.min ( App.screen . width , 200 )
App.screen . flags.minheight = math.min ( App.screen . width , 200 )
love.window . setMode ( App.screen . width , App.screen . height , App.screen . flags )
end
function run . resize ( w , h )
App.screen . width , App.screen . height = w , h
2022-09-04 01:10:12 +00:00
--? print('resize:', App.screen.width, App.screen.height)
plan_draw ( )
end
function load_pane ( id )
--? print('load pane from file', id)
local result = edit.initialize_state ( 0 , 0 , math.min ( Display_settings.column_width , App.screen . width - Margin_right ) , Font_height , Line_height )
result.id = id
result.filename = Directory .. id
2022-11-23 06:22:38 +00:00
load_from_disk ( result )
Links [ id ] = load_links ( id )
2022-09-04 01:10:12 +00:00
result.font_height = Font_height
result.line_height = Line_height
result.em = Em
result.editable = false
edit.fixup_cursor ( result )
2022-11-23 06:22:38 +00:00
Text.redraw_all ( result )
2022-09-04 01:10:12 +00:00
return result
end
function height ( pane )
if pane._height == nil then
refresh_pane_height ( pane )
end
return pane._height
end
-- keep the structure of this function sync'd with plan_draw
function refresh_pane_height ( pane )
--? print('refresh pane height')
local y = 0
if pane.title then
y = y + 5 + Line_height + 5
end
2022-12-18 20:43:10 +00:00
rehydrate_pane ( pane )
2022-09-04 01:10:12 +00:00
for i = 1 , # pane.lines do
local line = pane.lines [ i ]
if line.mode == ' text ' then
Text.compute_fragments ( pane , i )
Text.populate_screen_line_starting_pos ( pane , i )
y = y + Line_height *# pane.line_cache [ i ] . screen_line_starting_pos
Text.clear_screen_line_cache ( pane , i )
elseif line.mode == ' drawing ' then
-- nothing
y = y + Drawing.pixels ( line.h , Display_settings.column_width ) + Drawing_padding_height
else
print ( line.mode )
assert ( false )
end
end
2022-11-23 05:49:11 +00:00
if Links [ pane.id ] and not empty ( Links [ pane.id ] ) then
2022-09-04 01:10:12 +00:00
y = y + 5 + Line_height + 5 -- for crosslinks
end
pane._height = y
2022-09-03 21:13:22 +00:00
end
2022-09-04 01:10:12 +00:00
-- titles are optional and so affect the height of the pane
function add_title ( pane , title )
pane.title = title
pane._height = nil
end
-- keep the structure of this function sync'd with refresh_pane_height
2022-12-11 09:04:47 +00:00
function plan_draw ( options )
2022-12-18 20:43:10 +00:00
--? print('plan draw')
2022-12-10 04:43:03 +00:00
--? print(#Surface, 'columns;', num_panes(), 'panes')
Panes_to_draw = { }
Column_headers_to_draw = { }
local sx = Padding_horizontal + Margin_left
2022-12-18 20:49:34 +00:00
if Grab_pane then rehydrate_pane ( Grab_pane ) end
2022-12-10 04:43:03 +00:00
for column_index , column in ipairs ( Surface ) do
if should_show_column ( sx ) then
table.insert ( Column_headers_to_draw , { name = ( ' %d. %s ' ) : format ( column_index , column.name ) , x = sx - Display_settings.x } )
local sy = Padding_vertical
for pane_index , pane in ipairs ( column ) do
if sy > Display_settings.y + App.screen . height - Header_height then
break
end
--? print('bounds:', column_index, pane_index, sx,sy)
if should_show_pane ( pane , sy ) then
table.insert ( Panes_to_draw , pane )
-- stash some short-lived variables
pane.column_index = column_index
pane.pane_index = pane_index
local y_offset = 0
local body_sy = sy
if column [ pane_index ] . title then
body_sy = body_sy + 5 + Line_height + 5
end
2022-12-11 09:04:47 +00:00
if should_update_screen_top ( column_index , pane_index , pane , options ) then
2022-09-04 01:10:12 +00:00
if body_sy < Display_settings.y then
pane.screen_top1 , y_offset = schema1_of_y ( pane , Display_settings.y - body_sy )
else
pane.screen_top1 = { line = 1 , pos = 1 }
end
end
if body_sy < Display_settings.y then
pane.top = Margin_above
else
pane.top = body_sy - Display_settings.y + Margin_above
end
pane.top = Header_height + pane.top - y_offset
--? print('bounds: =>', pane.top)
pane.left = sx - Display_settings.x
pane.right = pane.left + Display_settings.column_width
pane.width = pane.right - pane.left
else
-- clear bounds to catch issues early
pane.top = nil
--? print('bounds: =>', pane.top)
end
sy = sy + Margin_above + height ( pane ) + Margin_below + Padding_vertical
end
else
-- clear bounds to catch issues early
for _ , pane in ipairs ( column ) do
pane.top = nil
end
end
sx = sx + Margin_right + Display_settings.column_width + Padding_horizontal + Margin_left
2022-09-03 21:13:22 +00:00
end
2022-09-04 01:10:12 +00:00
end
2022-12-11 09:04:47 +00:00
function should_update_screen_top ( column_index , pane_index , pane , options )
2022-12-21 10:19:02 +00:00
if options == nil then return true end
2022-09-04 01:10:12 +00:00
if column_index ~= Cursor_pane.col then return true end
if pane_index ~= Cursor_pane.row then return true end
2022-12-11 09:04:47 +00:00
-- update the cursor pane either if it's not editable, or
-- if it was explicitly requested
2022-12-21 10:19:02 +00:00
return not options.ignore_cursor_pane
2022-09-03 21:13:22 +00:00
end
function run . draw ( )
2022-12-11 08:25:41 +00:00
--? print('draw')
2022-09-04 01:10:12 +00:00
if Display_settings.mode == ' normal ' then
draw_normal_mode ( )
elseif Display_settings.mode == ' search ' then
draw_normal_mode ( )
-- hack: pass in an unexpected object and pun some attributes
Text.draw_search_bar ( Display_settings , --[[force show cursor]] true )
elseif Display_settings.mode == ' search_all ' then
draw_normal_mode ( )
-- only difference is in command palette below
elseif Display_settings.mode == ' searching_all ' then
draw_normal_mode ( )
-- only difference is in command palette below
elseif Display_settings.mode == ' maximize ' then
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
pane.top = Header_height + Margin_above
pane.left = App.screen . width / 2 - 20 * App.width ( Em )
pane.right = App.screen . width / 2 + 20 * App.width ( Em )
pane.width = pane.right - pane.left
edit.draw ( pane )
end
end
else
print ( Display_settings.mode )
assert ( false )
end
if Grab_pane then
local old_top , old_left , old_right = Grab_pane.top , Grab_pane.left , Grab_pane.right
local old_screen_top = Grab_pane.screen_top1
Grab_pane.screen_top1 = { line = 1 , pos = 1 }
Grab_pane.top = App.screen . height - 10 * Line_height
Grab_pane.left = App.screen . width - Display_settings.column_width - Margin_right - Padding_horizontal
Grab_pane.right = Grab_pane.left + Display_settings.column_width
Grab_pane.width = Grab_pane.right - Grab_pane.left
App.color ( Grab_background_color )
love.graphics . rectangle ( ' fill ' , Grab_pane.left - Margin_left , Grab_pane.top - Margin_above , Grab_pane.width + Margin_left + Margin_right , App.screen . height - Grab_pane.top + Margin_above )
edit.draw ( Grab_pane )
Grab_pane.top , Grab_pane.left , Grab_pane.right = old_top , old_left , old_right
Grab_pane.screen_top1 = old_screen_top
end
draw_menu_bar ( )
if Display_settings.mode == ' search_all ' or Display_settings.mode == ' searching_all ' then
draw_command_palette_for_search_all ( )
2022-10-27 06:13:22 +00:00
else
if Display_settings.show_palette then
draw_command_palette ( )
else
show_error ( )
end
2022-09-04 01:10:12 +00:00
end
2022-11-16 07:39:01 +00:00
draw_debug ( )
2022-09-04 01:10:12 +00:00
end
function draw_normal_mode ( )
assert ( Cursor_pane.col )
assert ( Cursor_pane.row )
--? print('draw', Display_settings.x, Display_settings.y)
for _ , pane in ipairs ( Panes_to_draw ) do
assert ( pane.top )
--? if Surface[pane.column_index].name == 'search: donate' then
--? print('draw: search: donate', pane, Display_settings.search_all_pane)
--? print(#pane.lines, #pane.line_cache, pane._height)
--? print(pane.lines[1].data)
--? end
if pane.title and eq ( pane.screen_top1 , { line = 1 , pos = 1 } ) then
draw_title ( pane )
end
edit.draw ( pane )
if pane_drew_to_bottom ( pane ) then
draw_links ( pane )
end
if pane.column_index == Cursor_pane.col and pane.pane_index == Cursor_pane.row then
App.color ( Cursor_pane_background_color )
if pane.editable and Surface.cursor_on_screen_check then
assert ( pane.cursor_y , ' cursor went off screen; this should never happen ' )
Surface.cursor_on_screen_check = false
end
else
App.color ( Pane_background_color )
end
love.graphics . rectangle ( ' fill ' , pane.left - Margin_left , pane.top - Margin_above , pane.width + Margin_left + Margin_right , pane.bottom - pane.top + Margin_above + Margin_below )
end
for _ , header in ipairs ( Column_headers_to_draw ) do
-- column header
App.color ( Column_header_color )
love.graphics . rectangle ( ' fill ' , header.x - Margin_left , Menu_status_bar_height , Margin_left + Display_settings.column_width + Margin_right , Column_header_height )
App.color ( Text_color )
love.graphics . print ( header.name , header.x , Menu_status_bar_height + 5 )
end
end
function pane_drew_to_bottom ( pane )
return pane.bottom < App.screen . height - Line_height
end
function should_show_column ( sx )
return overlap ( sx - Margin_left , sx + Display_settings.column_width + Margin_right , Display_settings.x , Display_settings.x + App.screen . width )
end
function should_show_pane ( pane , sy )
return overlap ( sy , sy + Margin_above + height ( pane ) + Margin_below , Display_settings.y , Display_settings.y + App.screen . height - Header_height )
end
function draw_title ( pane )
assert ( pane.title )
if Text_cache [ pane.title ] == nil then
Text_cache [ pane.title ] = App.newText ( love.graphics . getFont ( ) , pane.title )
end
App.color ( Pane_title_color )
App.screen . draw ( Text_cache [ pane.title ] , pane.left , pane.top - Margin_above - 5 - Line_height )
App.color ( Pane_title_background_color )
love.graphics . rectangle ( ' fill ' , pane.left - Margin_left , pane.top - Margin_above - 5 - Line_height - 5 , Margin_left + Display_settings.column_width + Margin_right , 5 + Line_height + 5 )
end
function draw_links ( pane )
2022-11-23 05:49:11 +00:00
local links = Links [ pane.id ]
2022-09-04 01:10:12 +00:00
if links == nil then return end
if empty ( links ) then return end
local x = pane.left
for _ , label in ipairs ( Edge_list ) do
if links [ label ] then
draw_link ( label , x , pane.bottom )
end
2022-10-29 20:43:36 +00:00
x = x + App.width ( to_text ( label ) ) + 10 + 10
2022-09-04 01:10:12 +00:00
end
-- links we don't know about, just in case
for link , _ in pairs ( links ) do
if not Opposite [ link ] then
draw_link ( link , x , pane.bottom )
2022-10-29 20:43:36 +00:00
x = x + App.width ( to_text ( link ) ) + 10 + 10
2022-09-04 01:10:12 +00:00
end
end
pane.bottom = pane.bottom + 5 + Line_height + 5
end
function draw_link ( label , x , y )
2022-10-29 20:43:36 +00:00
local label_text = to_text ( label )
2022-09-04 01:10:12 +00:00
App.color ( Crosslink_color )
2022-10-29 20:43:36 +00:00
love.graphics . draw ( label_text , x , y + 5 )
2022-09-04 01:10:12 +00:00
App.color ( Crosslink_background_color )
2022-10-29 20:43:36 +00:00
love.graphics . rectangle ( ' fill ' , x - 5 , y + 3 , App.width ( label_text ) + 10 , 2 + Line_height + 2 )
2022-09-04 01:10:12 +00:00
end
-- assumes intervals are half-open: [lo, hi)
-- https://en.wikipedia.org/wiki/Interval_(mathematics)
function overlap ( lo1 , hi1 , lo2 , hi2 )
-- lo2 hi2
-- | |
-- | |
-- | |
if lo1 <= lo2 and hi1 > lo2 then
return true
end
-- lo2 hi2
-- | |
-- | |
if lo1 < hi2 and hi1 >= hi2 then
return true
end
-- lo2 hi2
-- | |
-- | |
return lo1 >= lo2 and hi1 <= hi2
2022-09-03 21:13:22 +00:00
end
function run . update ( dt )
2022-11-16 07:39:01 +00:00
update_footprint ( )
2022-09-03 21:13:22 +00:00
Cursor_time = Cursor_time + dt
2022-09-04 01:10:12 +00:00
if App.mouse_y ( ) < Header_height then
-- column header
love.mouse . setCursor ( love.mouse . getSystemCursor ( ' arrow ' ) )
elseif in_pane ( App.mouse_x ( ) , App.mouse_y ( ) ) then
love.mouse . setCursor ( love.mouse . getSystemCursor ( ' arrow ' ) )
else
love.mouse . setCursor ( love.mouse . getSystemCursor ( ' hand ' ) )
end
if Pan.x then
Display_settings.x = math.max ( Pan.x - App.mouse_x ( ) , 0 )
Display_settings.y = math.max ( Pan.y - ( App.mouse_y ( ) - Header_height ) , 0 )
end
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane and pane.editable then
edit.update ( pane , dt )
end
end
if not Display_settings.show_palette and ( Display_settings.mode == ' normal ' or Display_settings.mode == ' search ' ) and App.mouse_down ( 1 ) then
-- pan the surface by dragging
plan_draw ( )
end
if Display_settings.mode == ' searching_all ' then
resume_search_all ( )
end
2022-10-27 06:13:22 +00:00
if Current_error then
if App.getTime ( ) - Current_error_time > 3 then
Current_error = nil
Current_error_text = nil
Current_error_time = nil
end
end
2022-09-04 01:10:12 +00:00
end
function in_pane ( x , y )
-- duplicate some logic from App.draw
local sx , sy = to_surface ( x , y )
local x = Padding_horizontal
for column_idx , column in ipairs ( Surface ) do
if sx < x then
return false
end
if sx < x + Margin_left + Display_settings.column_width + Margin_right then
local y = Padding_vertical
for pane_idx , pane in ipairs ( column ) do
if sy < y then
return false
end
if sy < y + Margin_above + height ( pane ) + Margin_below then
return true
end
y = y + Margin_above + height ( pane ) + Margin_below + Padding_vertical
end
end
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
end
return false
end
function to_pane ( sx , sy )
-- duplicate some logic from App.draw
local x = Padding_horizontal
for column_idx , column in ipairs ( Surface ) do
if sx < x then
return nil
end
if sx < x + Margin_left + Display_settings.column_width + Margin_right then
local y = Padding_vertical
for pane_idx , pane in ipairs ( column ) do
if sy < y then
return nil
end
if sy < y + Margin_above + height ( pane ) + Margin_below then
return { col = column_idx , row = pane_idx }
end
y = y + Margin_above + height ( pane ) + Margin_below + Padding_vertical
end
end
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
end
return nil
end
function to_surface ( x , y )
return x + Display_settings.x , y + Display_settings.y - Header_height
2022-09-03 21:13:22 +00:00
end
function run . quit ( )
2022-09-04 01:10:12 +00:00
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane and pane.editable then
edit.quit ( pane )
end
end
2022-09-03 21:13:22 +00:00
end
2022-09-10 05:30:37 +00:00
function run . settings ( )
2022-09-10 05:31:27 +00:00
assert ( Settings )
2022-09-04 00:00:58 +00:00
if Current_app == ' run ' then
Settings.x , Settings.y , Settings.displayindex = love.window . getPosition ( )
end
2022-09-04 01:10:12 +00:00
local column_names = { }
for _ , column in ipairs ( Surface ) do
table.insert ( column_names , column.name )
2022-09-03 21:13:22 +00:00
end
return {
2022-09-04 00:00:58 +00:00
x = Settings.x , y = Settings.y , displayindex = Settings.displayindex ,
2022-09-03 21:13:22 +00:00
width = App.screen . width , height = App.screen . height ,
2022-09-04 01:10:12 +00:00
font_height = Font_height ,
column_width = Display_settings.column_width ,
surface_x = Display_settings.x ,
surface_y = Display_settings.y ,
cursor_col = Cursor_pane.col ,
cursor_row = Cursor_pane.row ,
columns = column_names ,
2022-09-03 21:13:22 +00:00
}
end
2022-12-24 03:30:06 +00:00
function run . mouse_press ( x , y , mouse_button )
2022-09-04 01:10:12 +00:00
--? print('app mouse pressed', x,y)
2022-09-03 21:13:22 +00:00
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
2022-09-04 01:10:12 +00:00
clear_selections ( )
if Display_settings.mode == ' normal ' or Display_settings.mode == ' search ' or Display_settings.mode == ' search_all ' or Display_settings.mode == ' searching_all ' then
2022-12-24 03:30:06 +00:00
mouse_press_in_normal_mode ( x , y , mouse_button )
2022-09-04 01:10:12 +00:00
elseif Display_settings.mode == ' maximize ' then
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
2022-12-24 03:30:06 +00:00
edit.mouse_press ( pane , x , y , mouse_button )
2022-09-04 01:10:12 +00:00
end
end
else
print ( Display_settings.mode )
assert ( false )
end
end
function clear_selections ( )
for _ , column in ipairs ( Surface ) do
for _ , pane in ipairs ( column ) do
pane.selection1 = { }
end
end
end
2022-12-24 03:30:06 +00:00
function mouse_press_in_normal_mode ( x , y , mouse_button )
2022-09-04 01:10:12 +00:00
Pan = { }
if y < Header_height then
-- column headers currently not interactable
return
end
local sx , sy = to_surface ( x , y )
if in_pane ( x , y ) then
Cursor_pane = to_pane ( sx , sy )
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
2022-12-24 03:30:06 +00:00
edit.mouse_press ( pane , x , y , mouse_button )
2022-09-04 01:10:12 +00:00
pane._height = nil
end
end
else
Pan = { x = sx , y = sy }
end
2022-09-03 21:13:22 +00:00
end
2022-12-24 01:16:19 +00:00
function run . mouse_release ( x , y , mouse_button )
2022-09-04 01:10:12 +00:00
--? print('app mouse released')
2022-09-03 21:13:22 +00:00
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
2022-12-13 07:05:33 +00:00
if in_pane ( x , y ) or Display_settings.mode == ' maximize ' then
2022-11-28 20:01:43 +00:00
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
2022-12-24 03:30:06 +00:00
edit.mouse_release ( pane , x , y , mouse_button )
2022-11-28 20:01:43 +00:00
end
end
2022-09-04 01:10:12 +00:00
end
Pan = { }
2022-09-03 21:13:22 +00:00
end
2022-12-24 01:16:19 +00:00
function run . text_input ( t )
2022-09-03 21:13:22 +00:00
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
2022-09-04 01:10:12 +00:00
--? print('textinput', t)
-- hotkeys operating on the cursor pane
if Display_settings.show_palette then
Display_settings.palette_command = Display_settings.palette_command .. t
Display_settings.palette_command_text = App.newText ( love.graphics . getFont ( ) , Display_settings.palette_command )
Display_settings.palette_alternative_index = 1
Display_settings.palette_candidates = candidates ( )
elseif Display_settings.mode == ' normal ' then
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
if not pane.editable then
-- global hotkeys for normal mode
if t == ' X ' then
command.wider_columns ( )
return
elseif t == ' x ' then
command.narrower_columns ( )
return
end
-- send keys to the current pane
else
2022-11-29 17:42:56 +00:00
if pane.cursor_x and pane.cursor_x >= 0 and pane.cursor_x < App.screen . width then
if pane.cursor_y and pane.cursor_y >= Header_height and pane.cursor_y < App.screen . height then
2022-09-04 01:10:12 +00:00
--? print(('%s typed in editor pane'):format(t))
local old_top = { line = pane.screen_top1 . line , pos = pane.screen_top1 . pos }
2022-12-24 03:30:06 +00:00
edit.text_input ( pane , t )
2022-09-04 01:10:12 +00:00
maybe_update_screen_top_of_cursor_pane ( pane , old_top )
pane._height = nil
2022-12-21 10:19:02 +00:00
plan_draw { ignore_cursor_pane = true }
2022-09-04 01:10:12 +00:00
end
end
end
end
end
elseif Display_settings.mode == ' search ' then
--? print('insert', t)
Display_settings.search_term = Display_settings.search_term .. t
Display_settings.search_text = nil
-- reset search state
clear_selections ( )
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next ( )
bring_cursor_of_cursor_pane_in_view ( ' down ' )
Surface.cursor_on_screen_check = true
2022-12-10 19:28:34 +00:00
plan_draw ( )
2022-09-04 01:10:12 +00:00
elseif Display_settings.mode == ' search_all ' then
Display_settings.search_all_query = Display_settings.search_all_query .. t
Display_settings.search_all_query_text = nil
elseif Display_settings.mode == ' searching_all ' then
Display_settings.mode = ' normal '
Display_settings.search_all_query_text = nil
elseif Display_settings.mode == ' maximize ' then
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
if pane.editable then
2022-12-24 03:30:06 +00:00
edit.text_input ( pane , t )
2022-09-04 01:10:12 +00:00
end
end
end
else
print ( Display_settings.mode )
assert ( false )
end
2022-09-03 21:13:22 +00:00
end
2022-12-24 01:16:19 +00:00
function run . keychord_press ( chord , key )
2022-09-04 01:10:12 +00:00
--? print('keychord press', chord)
2022-09-03 21:13:22 +00:00
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
2022-09-04 01:10:12 +00:00
-- global hotkeys
if chord == ' C-= ' then
update_font_settings ( Font_height + 2 )
elseif chord == ' C-- ' then
update_font_settings ( Font_height - 2 )
elseif chord == ' C-0 ' then
update_font_settings ( 20 )
-- mode-specific hotkeys
elseif Display_settings.show_palette then
2022-12-24 03:30:06 +00:00
keychord_press_on_command_palette ( chord , key )
2022-09-04 01:10:12 +00:00
elseif Display_settings.mode == ' normal ' then
if chord == ' C-return ' then
Display_settings.show_palette = true
Display_settings.palette_candidates = candidates ( )
elseif chord == ' C-f ' then
command.commence_find_on_surface ( )
elseif Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
2022-10-18 17:00:09 +00:00
if pane then
if pane.editable then
2022-12-10 19:28:34 +00:00
if chord == ' C-e ' then
2022-12-11 23:58:38 +00:00
--? profile.stop()
--? print(profile.report())
2022-12-10 19:28:34 +00:00
command.exit_editing ( )
elseif pane.cursor_x == nil then
-- ignore if cursor is not visible on screen
assert ( pane.cursor_y == nil )
2022-12-24 03:30:06 +00:00
panning_keychord_press ( chord , key )
2022-12-10 19:28:34 +00:00
plan_draw ( )
else
local old_top = { line = pane.screen_top1 . line , pos = pane.screen_top1 . pos }
2022-12-24 03:30:06 +00:00
edit.keychord_press ( pane , chord , key )
2022-12-11 09:04:47 +00:00
maybe_update_screen_top_of_cursor_pane ( pane , old_top )
pane._height = nil
2022-12-21 10:19:02 +00:00
plan_draw { ignore_cursor_pane = true }
2022-12-10 19:28:34 +00:00
end
2022-10-18 17:00:09 +00:00
else
2022-12-24 03:30:06 +00:00
keychord_press_in_normal_mode_with_immutable_pane ( pane , chord , key )
2022-12-10 19:28:34 +00:00
plan_draw ( )
2022-10-18 17:00:09 +00:00
end
2022-09-04 01:10:12 +00:00
end
end
elseif Display_settings.mode == ' search ' then
2022-12-24 03:30:06 +00:00
keychord_press_in_search_mode ( chord , key )
2022-09-04 01:10:12 +00:00
elseif Display_settings.mode == ' search_all ' then
2022-12-24 03:30:06 +00:00
keychord_press_in_search_all_mode ( chord , key )
2022-09-04 01:10:12 +00:00
elseif Display_settings.mode == ' searching_all ' then
interrupt_search_all ( )
elseif Display_settings.mode == ' maximize ' then
if chord == ' C-return ' then
Display_settings.show_palette = true
Display_settings.palette_candidates = candidates ( )
else
2022-12-24 03:30:06 +00:00
keychord_press_in_maximize_mode ( chord , key )
2022-09-04 01:10:12 +00:00
end
else
print ( Display_settings.mode )
assert ( false )
end
end
function update_font_settings ( font_height )
local column_width_in_ems = Display_settings.column_width / App.width ( Em )
Font_height = font_height
love.graphics . setFont ( love.graphics . newFont ( Font_height ) )
Line_height = math.floor ( font_height * 1.3 )
Em = App.newText ( love.graphics . getFont ( ) , ' m ' )
Display_settings.column_width = column_width_in_ems * App.width ( Em )
for _ , column in ipairs ( Surface ) do
for _ , pane in ipairs ( column ) do
pane.font_height = Font_height
pane.line_height = Line_height
pane.em = Em
pane.left = 0
pane.right = Display_settings.column_width
end
end
clear_all_pane_heights ( )
plan_draw ( )
end
-- Scan all panes, while delegating as much work as possible to lines.love search.
-- * Text.search_next in lines.love scans from cursor while wrapping around
-- within the pane, so we need to work around that.
-- * Each pane's search_term field influences whether the search term at
-- cursor is highlighted, so we need to manage that as well. At any moment
-- we want the search_term and search_text to be set for at most a single
-- pane.
--
-- Side-effect: we perturb the cursor of panes as we scan them.
function search_next ( )
if Cursor_pane.col < 1 then return end
clear_all_search_terms ( )
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane == nil then
return
end
--? print('search next', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
local old_cursor_in_cursor_pane = { line = pane.cursor1 . line , pos = pane.cursor1 . pos }
-- scan current pane down from cursor
2022-11-24 00:02:29 +00:00
if search_next_in_pane ( pane ) then
2022-09-04 01:10:12 +00:00
--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)
return
end
pane.cursor1 = old_cursor_in_cursor_pane
-- scan current column down from current pane
for current_pane_index = Cursor_pane.row + 1 , # Surface [ Cursor_pane.col ] do
local pane = Surface [ Cursor_pane.col ] [ current_pane_index ]
pane.cursor1 = { line = 1 , pos = 1 }
edit.fixup_cursor ( pane )
pane.screen_top1 = { line = 1 , pos = 1 }
if search_next_in_pane ( pane ) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
local current_column_index = 1 + Cursor_pane.col %# Surface -- (i+1)%#Surface in the presence of 1-indexing
-- scan columns past current, looping around
while true do
for current_pane_index , pane in ipairs ( Surface [ current_column_index ] ) do
pane.cursor1 = { line = 1 , pos = 1 }
edit.fixup_cursor ( pane )
pane.screen_top1 = { line = 1 , pos = 1 }
if search_next_in_pane ( pane ) then
Cursor_pane = { col = current_column_index , row = current_pane_index }
--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- loop update
current_column_index = 1 + current_column_index %# Surface -- i = (i+1)%#Surface in the presence of 1-indexing
-- termination check
if current_column_index == Cursor_pane.col then
break
end
end
-- scan current column until current pane
for current_pane_index = 1 , Cursor_pane.row - 1 do
2022-11-24 00:02:29 +00:00
local pane = Surface [ Cursor_pane.col ] [ current_pane_index ]
2022-11-25 01:37:21 +00:00
if search_next_in_pane ( pane ) then
2022-09-04 01:10:12 +00:00
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- finally, scan the cursor pane until the cursor
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
local old_cursor = pane.cursor1
pane.cursor1 = { line = 1 , pos = 1 }
edit.fixup_cursor ( pane )
pane.screen_top1 = { line = 1 , pos = 1 }
if search_next_in_pane ( pane ) then
if Text.lt1 ( pane.cursor1 , old_cursor ) then
return
end
end
-- nothing found
pane.cursor1 = old_cursor_in_cursor_pane
end
-- returns whether it found an occurrence
function search_next_in_pane ( pane )
pane.search_term = Display_settings.search_term
pane.search_text = Display_settings.search_text
2022-11-24 07:45:07 +00:00
pane.search_backup = { cursor = { line = pane.cursor1 . line , pos = pane.cursor1 . pos } , screen_top = { line = pane.screen_top1 . line , pos = pane.screen_top1 . pos } }
2022-09-04 01:10:12 +00:00
for i = 1 , # pane.lines do
if pane.line_cache [ i ] == nil then
pane.line_cache [ i ] = { }
end
end
if Text.search_next ( pane ) then
2022-11-24 07:45:07 +00:00
if Text.le1 ( pane.search_backup . cursor , pane.cursor1 ) then
2022-09-04 01:10:12 +00:00
-- select this occurrence
return true
end
-- Otherwise cursor wrapped around. Skip this pane.
end
-- Clean up this pane before moving on to the next one.
pane.search_term = nil
pane.search_text = nil
2022-11-24 07:45:07 +00:00
pane.cursor1 . line = pane.search_backup . cursor.line
pane.cursor1 . pos = pane.search_backup . cursor.pos
pane.screen_top1 . line = pane.search_backup . screen_top.line
pane.screen_top1 . pos = pane.search_backup . screen_top.pos
pane.search_backup = nil
2022-09-04 01:10:12 +00:00
end
-- Scan all panes, while delegating as much work as possible to lines.love search.
function search_previous ( )
if Cursor_pane.col < 1 then return end
clear_all_search_terms ( )
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane == nil then
return
end
--? print('search previous', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
local old_cursor_in_cursor_pane = { line = pane.cursor1 . line , pos = pane.cursor1 . pos }
-- scan current pane up from cursor
2022-11-24 00:02:29 +00:00
if search_previous_in_pane ( pane ) then
2022-09-04 01:10:12 +00:00
--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)
return
end
pane.cursor1 = old_cursor_in_cursor_pane
-- scan current column down from current pane
for current_pane_index = Cursor_pane.row - 1 , 1 , - 1 do
local pane = Surface [ Cursor_pane.col ] [ current_pane_index ]
pane.cursor1 = edit.final_cursor ( pane )
if search_previous_in_pane ( pane ) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
local current_column_index = 1 + ( Cursor_pane.col - 2 ) %# Surface -- (i-1)%#Surface in the presence of 1-indexing
-- scan columns past current, looping around
while true do
for current_pane_index = # Surface [ current_column_index ] , 1 , - 1 do
local pane = Surface [ current_column_index ] [ current_pane_index ]
pane.cursor1 = edit.final_cursor ( pane )
if search_previous_in_pane ( pane ) then
Cursor_pane = { col = current_column_index , row = current_pane_index }
--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- loop update
current_column_index = 1 + ( current_column_index - 2 ) %# Surface -- i = (i-1)%#Surface in the presence of 1-indexing
-- termination check
if current_column_index == Cursor_pane.col then
break
end
end
-- scan current column from bottom current pane
for current_pane_index =# Surface [ Cursor_pane.col ] , Cursor_pane.row + 1 , - 1 do
--? print('same column', current_pane_index)
2022-11-24 00:02:29 +00:00
local pane = Surface [ Cursor_pane.col ] [ current_pane_index ]
2022-11-25 01:37:21 +00:00
if search_previous_in_pane ( pane ) then
2022-09-04 01:10:12 +00:00
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- finally, scan the cursor pane from bottom until cursor
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
local old_cursor = pane.cursor1
pane.cursor1 = edit.final_cursor ( pane )
if search_previous_in_pane ( pane ) then
if Text.lt1 ( old_cursor , pane.cursor1 ) then
return
end
end
-- nothing found
pane.cursor1 = old_cursor_in_cursor_pane
end
-- returns whether it found an occurrence
function search_previous_in_pane ( pane )
pane.search_term = Display_settings.search_term
pane.search_text = Display_settings.search_text
2022-11-24 07:45:07 +00:00
pane.search_backup = { cursor = { line = pane.cursor1 . line , pos = pane.cursor1 . pos } , screen_top = { line = pane.screen_top1 . line , pos = pane.screen_top1 . pos } }
2022-09-04 01:10:12 +00:00
for i = 1 , # pane.lines do
if pane.line_cache [ i ] == nil then
pane.line_cache [ i ] = { }
end
end
if Text.search_previous ( pane ) then
2022-11-24 07:45:07 +00:00
if Text.lt1 ( pane.cursor1 , pane.search_backup . cursor ) then
2022-09-04 01:10:12 +00:00
-- select this occurrence
return true
end
-- Otherwise cursor wrapped around. Skip this pane.
end
-- Clean up this pane before moving on to the previous one.
pane.search_term = nil
pane.search_text = nil
2022-11-24 07:45:07 +00:00
pane.cursor1 . line = pane.search_backup . cursor.line
pane.cursor1 . pos = pane.search_backup . cursor.pos
pane.screen_top1 . line = pane.search_backup . screen_top.line
pane.screen_top1 . pos = pane.search_backup . screen_top.pos
pane.search_backup = nil
2022-09-04 01:10:12 +00:00
end
function bring_cursor_of_cursor_pane_in_view ( dir )
if Cursor_pane.col < 1 then
return
end
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane == nil then
return
end
--? print('viewport before', Display_settings.x, Display_settings.y)
local left_edge_sx = left_edge_sx ( Cursor_pane.col )
local cursor_sx = left_edge_sx + Text.x_of_schema1 ( pane , pane.cursor1 )
local vertically_ok = cursor_sx > Display_settings.x and cursor_sx < Display_settings.x + App.screen . width - App.width ( Em )
--? print(y_of_schema1(pane, pane.cursor1))
--? print('viewport starts at', Display_settings.y)
--? print('pane starts at', up_edge_sy(Cursor_pane.col, Cursor_pane.row))
--? print('cursor line contains ^'..pane.lines[pane.cursor1.line].data..'$')
--? print('cursor is at', y_of_schema1(pane, pane.cursor1), 'from top of pane')
local cursor_sy = up_edge_sy ( Cursor_pane.col , Cursor_pane.row ) + y_of_schema1 ( pane , pane.cursor1 )
--? print('cursor is at', cursor_sy)
local horizontally_ok = cursor_sy > Display_settings.y and cursor_sy < Display_settings.y + App.screen . height - Header_height - 2 * Line_height -- account for search bar along the bottom
if vertically_ok and horizontally_ok then
return
end
if dir == ' up ' then
if not vertically_ok then
Display_settings.x = left_edge_sx - Margin_left - Padding_horizontal
end
if not horizontally_ok then
Display_settings.y = cursor_sy - 3 * Line_height
end
else
assert ( dir == ' down ' )
if not vertically_ok then
Display_settings.x = left_edge_sx + Display_settings.column_width + Margin_right + Padding_horizontal - App.screen . width
end
if not horizontally_ok then
--? print('cursor used to be at ', cursor_sy - Display_settings.y)
--? print('subtract', App.screen.height, App.screen.height-Header_height)
Display_settings.y = cursor_sy + Text.search_bar_height ( pane ) - ( App.screen . height - Header_height )
-- Bah, temporarily giving up on debugging.
Display_settings.y = Display_settings.y + Line_height
--? print('=>', Display_settings.y)
--? print('cursor now at ', cursor_sy - Display_settings.y)
--? print('viewport height', App.screen.height)
--? print('cursor row starts', App.screen.height - (cursor_sy-Display_settings.y), 'px above bottom of viewport') -- totally wrong
assert ( App.screen . height - ( cursor_sy - Display_settings.y ) > 1.5 * Line_height )
end
end
--? print('viewport before clamp', Display_settings.x, Display_settings.y)
Display_settings.x = math.max ( Display_settings.x , 0 )
Display_settings.y = math.max ( Display_settings.y , 0 )
--? print('viewport now', Display_settings.x, Display_settings.y)
end
function clear_all_search_terms ( )
for col , column in ipairs ( Surface ) do
for row , pane in ipairs ( column ) do
pane.search_term = nil
pane.search_text = nil
end
end
end
2022-12-24 03:30:06 +00:00
function keychord_press_in_maximize_mode ( chord , key )
2022-09-04 01:10:12 +00:00
if Cursor_pane.col < 1 then
print ( ' no current note to edit ' )
return
end
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane == nil then
print ( ' no current note to edit ' )
return
end
if pane.editable then
if chord == ' C-e ' then
command.exit_editing ( )
else
2022-12-24 03:30:06 +00:00
edit.keychord_press ( pane , chord , key )
2022-09-04 01:10:12 +00:00
end
else
if chord == ' C-e ' then
command.edit_note ( )
elseif chord == ' C-c ' then
2022-12-24 03:30:06 +00:00
edit.keychord_press ( pane , chord , key )
2022-09-04 01:10:12 +00:00
end
end
end
function maybe_update_screen_top_of_cursor_pane ( pane , old_top )
local cursor_sy = up_edge_sy ( Cursor_pane.col , Cursor_pane.row ) + y_of_schema1 ( pane , pane.cursor1 )
if not eq ( old_top , pane.screen_top1 ) and eq ( old_top , { line = 1 , pos = 1 } ) and pane.top > Header_height and cursor_sy - Display_settings.y > App.screen . height - Header_height - Line_height then
-- pan the surface instead of scrolling within the pane
pane.screen_top1 = old_top
bring_cursor_of_cursor_pane_in_view ( ' down ' )
Surface.cursor_on_screen_check = true -- cursor was on screen before keystroke, so it should remain on screen after
return
end
2022-12-21 10:19:02 +00:00
if not eq ( old_top , pane.screen_top1 ) then
2022-09-04 01:10:12 +00:00
Display_settings.y = up_edge_sy ( Cursor_pane.col , Cursor_pane.row ) + y_of_schema1 ( pane , pane.screen_top1 )
Surface.cursor_on_screen_check = true -- cursor was on screen before keystroke, so it should remain on screen after
end
end
2022-12-24 03:30:06 +00:00
function keychord_press_in_normal_mode_with_immutable_pane ( pane , chord , key )
2022-09-04 01:10:12 +00:00
-- return if no part of cursor pane is visible
local left_sx = left_edge_sx ( Cursor_pane.col )
if not should_show_column ( left_sx ) then
2022-12-24 03:30:06 +00:00
panning_keychord_press ( chord , key )
2022-09-04 01:10:12 +00:00
return
end
local up_sy = up_edge_sy ( Cursor_pane.col , Cursor_pane.row )
if not should_show_pane ( pane , up_sy ) then
2022-12-24 03:30:06 +00:00
panning_keychord_press ( chord , key )
2022-09-04 01:10:12 +00:00
return
end
if chord == ' C-e ' then
command.edit_note ( )
2022-12-11 23:58:38 +00:00
profile.start ( )
2022-09-04 01:10:12 +00:00
elseif chord == ' C-c ' then
2022-12-24 03:30:06 +00:00
edit.keychord_press ( pane , chord , key )
2022-09-04 01:10:12 +00:00
else
2022-12-24 03:30:06 +00:00
panning_keychord_press ( chord , key )
2022-09-04 01:10:12 +00:00
end
end
-- y offset of a given (line, pos)
function y_of_schema1 ( pane , loc )
--? print(('updating viewport y; cursor pane starts at %d; screen top is at %d,%d'):format(result, loc.line, loc.pos))
local result = 0
if pane.title then
result = result + 5 + Line_height + 5
end
result = result + Margin_above
if loc.line == 1 and loc.pos == 1 then
return result
end
for i = 1 , loc.line - 1 do
--? print('', 'd', i, result)
Text.populate_screen_line_starting_pos ( pane , i )
--? print('', '', #pane.line_cache[i].screen_line_starting_pos, pane.left, pane.right)
result = result + line_height ( pane , i , pane.left , pane.right )
end
if pane.lines [ loc.line ] . mode == ' text ' then
Text.populate_screen_line_starting_pos ( pane , loc.line )
for i , screen_line_starting_pos in ipairs ( pane.line_cache [ loc.line ] . screen_line_starting_pos ) do
if screen_line_starting_pos >= loc.pos then
break
end
result = result + Line_height
end
end
--? print(('viewport at %d'):format(result))
return result
end
2022-12-24 03:30:06 +00:00
function keychord_press_in_search_mode ( chord , key )
2022-09-04 01:10:12 +00:00
if chord == ' escape ' then
Display_settings.mode = ' normal '
clear_all_search_terms ( )
2022-12-18 20:50:55 +00:00
dehydrate_all_panes ( )
2022-09-04 01:10:12 +00:00
-- go back to old viewport
--? print('esc; exiting search mode')
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- don't forget search text
elseif chord == ' return ' then
Display_settings.mode = ' normal '
clear_all_search_terms ( )
2022-12-18 20:50:55 +00:00
dehydrate_all_panes ( )
2022-09-04 01:10:12 +00:00
-- forget old viewport
--? print('return; exiting search mode')
Display_settings.search_backup_x = nil
Display_settings.search_backup_y = nil
Display_settings.search_backup_cursor_pane = nil
-- don't forget search text
elseif chord == ' backspace ' then
local len = utf8.len ( Display_settings.search_term )
local byte_offset = Text.offset ( Display_settings.search_term , len )
Display_settings.search_term = string.sub ( Display_settings.search_term , 1 , byte_offset - 1 )
Display_settings.search_text = nil
-- reset search state
clear_selections ( )
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next ( )
bring_cursor_of_cursor_pane_in_view ( ' down ' )
Surface.cursor_on_screen_check = true
plan_draw ( )
--? print('backspace; search term is now', Display_settings.search_term)
elseif chord == ' C-v ' then
Display_settings.search_term = Display_settings.search_term .. App.getClipboardText ( )
Display_settings.search_text = nil
-- reset search state
clear_selections ( )
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next ( )
bring_cursor_of_cursor_pane_in_view ( ' down ' )
Surface.cursor_on_screen_check = true
plan_draw ( )
--? print('paste; search term is now', Display_settings.search_term)
elseif chord == ' up ' then
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
search_previous ( )
bring_cursor_of_cursor_pane_in_view ( ' up ' )
Surface.cursor_on_screen_check = true
plan_draw ( )
end
end
elseif chord == ' down ' then
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
pane.cursor1 . pos = pane.cursor1 . pos + 1
search_next ( )
bring_cursor_of_cursor_pane_in_view ( ' down ' )
Surface.cursor_on_screen_check = true
plan_draw ( )
end
end
-- things from normal mode we still want
elseif chord == ' C-c ' then
if Cursor_pane.col >= 1 then
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane then
2022-12-24 03:30:06 +00:00
edit.keychord_press ( pane , chord , key )
2022-09-04 01:10:12 +00:00
end
end
end
end
2022-12-24 03:30:06 +00:00
function keychord_press_in_search_all_mode ( chord , key )
2022-09-04 01:10:12 +00:00
if chord == ' escape ' then
Display_settings.mode = ' normal '
-- don't forget search text
Display_settings.search_all_state = nil
elseif chord == ' return ' then
finalize_search_all_pane ( )
add_search_all_pane_to_right_of_cursor ( )
Display_settings.mode = ' searching_all '
plan_draw ( )
elseif chord == ' backspace ' then
local len = utf8.len ( Display_settings.search_all_query )
local byte_offset = Text.offset ( Display_settings.search_all_query , len )
Display_settings.search_all_query = string.sub ( Display_settings.search_all_query , 1 , byte_offset - 1 )
Display_settings.search_all_query_text = nil
--? print('backspace; search_all term is now', Display_settings.search_all_query)
elseif chord == ' C-v ' then
Display_settings.search_all_query = Display_settings.search_all_query .. App.getClipboardText ( )
Display_settings.search_all_query_text = nil
--? print('paste; search_all term is now', Display_settings.search_all_query)
end
end
-- return (line, pos) of the screen line starting near a given y offset, and
-- y_offset remaining after the calculation
-- invariants:
-- - 0 <= y_offset <= Line_height if line is text
-- - let loc, y_offset = schema1_of_y(pane, y)
-- y - y_offset == y_of_schema1(pane, loc)
function schema1_of_y ( pane , y )
assert ( y >= 0 )
local y_offset = y
for i = 1 , # pane.lines do
--? print('--', y_offset)
Text.populate_screen_line_starting_pos ( pane , i )
local height = line_height ( pane , i , pane.left , pane.right )
if y_offset < height then
local line = pane.lines [ i ]
if line.mode ~= ' text ' then
return { line = i , pos = 1 } , y_offset
else
local nlines = math.floor ( y_offset / pane.line_height )
--? print(y_offset, pane.line_height, nlines)
assert ( nlines >= 0 and nlines < # pane.line_cache [ i ] . screen_line_starting_pos )
local pos = pane.line_cache [ i ] . screen_line_starting_pos [ nlines + 1 ] -- switch to 1-indexing
y_offset = y_offset - nlines * pane.line_height
return { line = i , pos = pos } , y_offset
end
end
y_offset = y_offset - height
end
-- y is below the pane
return { line =# pane.lines + 1 , pos = 1 } , y_offset
end
function line_height ( State , line_index , left , right )
local line = State.lines [ line_index ]
local line_cache = State.line_cache [ line_index ]
if line.mode == ' text ' then
return Line_height *# line_cache.screen_line_starting_pos
else
return Drawing.pixels ( line.h , right - left ) + Drawing_padding_height
end
end
function stop_editing_all ( )
local edit_count = 0
for _ , column in ipairs ( Surface ) do
for _ , pane in ipairs ( column ) do
if pane.editable then
stop_editing ( pane )
edit_count = edit_count + 1
end
end
end
assert ( edit_count <= 1 )
end
function stop_editing ( pane )
edit.quit ( pane )
-- save symmetric links
2022-11-23 05:49:11 +00:00
for rel , x in pairs ( Links [ pane.id ] ) do
2022-11-23 06:22:38 +00:00
process_all_links ( x , function ( target_id )
save_links ( target_id )
2022-10-29 20:50:00 +00:00
end )
2022-09-04 01:10:12 +00:00
end
if Display_settings.mode ~= ' maximize ' then
refresh_panes ( pane )
end
pane.editable = false
end
2022-12-24 03:30:06 +00:00
function panning_keychord_press ( chord , key )
2022-09-04 01:10:12 +00:00
if chord == ' up ' then
Display_settings.y = math.max ( Display_settings.y - Pan_step , 0 )
local up_sy = up_edge_sy ( Cursor_pane.col , Cursor_pane.row )
local up_py = up_sy - Display_settings.y
if up_py > 2 / 3 * App.screen . height then
Cursor_pane.row = math.min ( # Surface [ Cursor_pane.col ] , row ( Cursor_pane.col , Display_settings.y + App.screen . height / 3 ) )
end
elseif chord == ' down ' then
local visible_column_max_y = most ( column_height , visible_columns ( ) )
if visible_column_max_y - Display_settings.y > App.screen . height / 2 then
Display_settings.y = Display_settings.y + Pan_step
end
local down_sx = down_edge_sx ( Cursor_pane.col , Cursor_pane.row )
local down_px = down_sx - Display_settings.y
if down_px < App.screen . height / 3 then
Cursor_pane.row = math.min ( # Surface [ Cursor_pane.col ] , row ( Cursor_pane.col , Display_settings.y + App.screen . height / 3 ) )
end
elseif chord == ' left ' then
Display_settings.x = math.max ( Display_settings.x - Pan_step , 0 )
local left_sx = left_edge_sx ( Cursor_pane.col )
local left_px = left_sx - Display_settings.x
if left_px > App.screen . width - Margin_right - Display_settings.column_width / 2 then
Cursor_pane.col = math.min ( # Surface , col ( Display_settings.x + App.screen . width - Margin_right - Display_settings.column_width / 2 ) )
Cursor_pane.row = math.min ( # Surface [ Cursor_pane.col ] , Cursor_pane.row )
end
elseif chord == ' right ' then
if Display_settings.x < ( # Surface - 1 ) * ( Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right ) then
Display_settings.x = Display_settings.x + Pan_step
end
local right_sx = left_edge_sx ( Cursor_pane.col ) + Display_settings.column_width
local right_px = right_sx - Display_settings.x
if right_px < Margin_left + Display_settings.column_width / 2 then
Cursor_pane.col = math.min ( # Surface , col ( Display_settings.x + Margin_left + Display_settings.column_width / 2 ) )
Cursor_pane.row = math.min ( # Surface [ Cursor_pane.col ] , Cursor_pane.row )
end
elseif chord == ' pageup ' or chord == ' S-up ' then
Display_settings.y = math.max ( Display_settings.y - App.screen . height + Line_height * 2 , 0 )
local up_sy = up_edge_sy ( Cursor_pane.col , Cursor_pane.row )
local up_py = up_sy - Display_settings.y
if up_py > 2 / 3 * App.screen . height then
Cursor_pane.row = math.min ( # Surface [ Cursor_pane.col ] , row ( Cursor_pane.col , Display_settings.y + App.screen . height / 3 ) )
end
elseif chord == ' pagedown ' or chord == ' S-down ' then
--? print('pagedown')
local visible_column_max_y = most ( column_height , visible_columns ( ) )
if visible_column_max_y - Display_settings.y > App.screen . height then
--? print('updating viewport')
Display_settings.y = Display_settings.y + App.screen . height - Line_height * 2
end
local down_sx = down_edge_sx ( Cursor_pane.col , Cursor_pane.row )
local down_px = down_sx - Display_settings.y
if down_px < App.screen . height / 3 then
--? print('updating row')
Cursor_pane.row = math.min ( # Surface [ Cursor_pane.col ] , row ( Cursor_pane.col , Display_settings.y + App.screen . height / 3 ) )
--? print('=>', Cursor_pane.row)
end
elseif chord == ' S-left ' then
Display_settings.x = math.max ( Display_settings.x - Margin_left - Display_settings.column_width - Margin_right - Padding_horizontal , 0 )
local left_sx = left_edge_sx ( Cursor_pane.col )
local left_px = left_sx - Display_settings.x
if left_px > App.screen . width - Margin_right - Display_settings.column_width / 2 then
Cursor_pane.col = math.min ( # Surface , col ( Display_settings.x + App.screen . width - Margin_right - Display_settings.column_width / 2 ) )
Cursor_pane.row = math.min ( # Surface [ Cursor_pane.col ] , Cursor_pane.row )
end
elseif chord == ' S-right ' then
if Display_settings.x < ( # Surface - 1 ) * ( Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right ) then
Display_settings.x = Display_settings.x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
local right_sx = left_edge_sx ( Cursor_pane.col ) + Display_settings.column_width
local right_px = right_sx - Display_settings.x
if right_px < Margin_left + Display_settings.column_width / 2 then
Cursor_pane.col = math.min ( # Surface , col ( Display_settings.x + Margin_left + Display_settings.column_width / 2 ) )
Cursor_pane.row = math.min ( # Surface [ Cursor_pane.col ] , Cursor_pane.row )
end
end
elseif chord == ' C-down ' then
command.down_one_pane ( )
elseif chord == ' C-up ' then
command.up_one_pane ( )
elseif chord == ' C-end ' then
command.bottom_pane_of_column ( )
elseif chord == ' C-home ' then
command.top_pane_of_column ( )
end
--? print('after', Cursor_pane.col, Cursor_pane.row)
end
function visible_columns ( )
local result = { }
local col = col ( Display_settings.x )
local x = left_edge_sx ( col ) - Display_settings.x
while col <= # Surface do
x = x + Padding_horizontal
table.insert ( result , col )
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
if x > App.screen . width then
break
end
col = col + 1
end
return result
end
function refresh_panes ( pane )
--? print('refreshing')
for x , col in ipairs ( Surface ) do
for y , p in ipairs ( col ) do
if p.id == pane.id then
--? print(x,y)
p.lines = pane.lines
p._height = nil
Text.redraw_all ( p )
end
end
end
plan_draw ( )
end
2022-12-18 20:43:10 +00:00
-- The basic text editor buffer (editor State) in edit.lua and text.lua
-- assumes there's always an element in line_cache for every line. However,
-- this can be a lot of overhead when we have hundreds of buffers. So pensieve
-- needs to violate this assumption while hiding it from buffer helpers.
2022-12-18 20:50:55 +00:00
function dehydrate_all_panes ( )
2022-09-04 01:10:12 +00:00
for x , col in ipairs ( Surface ) do
for y , p in ipairs ( col ) do
p._height = nil
2022-11-24 00:02:29 +00:00
p.line_cache = { }
2022-09-04 01:10:12 +00:00
end
end
plan_draw ( )
2022-09-03 21:13:22 +00:00
end
2022-12-18 20:43:10 +00:00
function rehydrate_pane ( pane )
for i = 1 , # pane.lines do
-- don't clobber starty/startpos if they exist
if pane.line_cache [ i ] == nil then
pane.line_cache [ i ] = { }
end
Text.clear_screen_line_cache ( pane , i )
end
end
2022-12-24 01:16:19 +00:00
function run . key_release ( key , scancode )
2022-09-04 01:10:12 +00:00
--? print('key release', key)
2022-09-03 21:13:22 +00:00
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
2022-09-04 01:10:12 +00:00
if Cursor_pane.col < 1 then
return
end
local pane = Surface [ Cursor_pane.col ] [ Cursor_pane.row ]
if pane and pane.editable then
2022-12-24 03:30:06 +00:00
edit.key_release ( pane , key , scancode )
2022-09-04 01:10:12 +00:00
end
end
function clear_all_pane_heights ( )
Text_cache = { }
for _ , column in ipairs ( Surface ) do
for _ , pane in ipairs ( column ) do
pane._height = nil
end
end
end
-- convert x surface pixel coordinate into column index
function col ( x )
2022-11-28 17:52:20 +00:00
return 1 + math.floor ( x / ( Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right ) )
2022-09-04 01:10:12 +00:00
end
-- col is 1-indexed
-- returns x surface pixel coordinate of left edge of column col
function left_edge_sx ( col )
return ( col - 1 ) * ( Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right ) + Padding_horizontal + Margin_left
end
function row ( col , y )
local sy = Padding_vertical
for i , pane in ipairs ( Surface [ col ] ) do
--? print('', i, y, sy, next_sy)
local next_sy = sy + Margin_above + height ( pane ) + Margin_below + Padding_vertical
if next_sy > y then
return i
end
sy = next_sy
end
return # Surface [ col ]
end
function up_edge_sy ( col , row )
local result = Padding_vertical
for i = 1 , row - 1 do
local pane = Surface [ col ] [ i ]
result = result + Margin_above + height ( pane ) + Margin_below + Padding_vertical
end
return result
end
function down_edge_sx ( col , row )
local result = Padding_vertical
for i = 1 , row do
local pane = Surface [ col ] [ i ]
result = result + Margin_above + height ( pane ) + Margin_below + Padding_vertical
end
return result - Padding_vertical
end
function column_height ( col )
local result = Padding_vertical
for pane_index , pane in ipairs ( Surface [ col ] ) do
result = result + Margin_above + height ( pane ) + Margin_below + Padding_vertical
end
return result
end
function most ( f , arr )
local result = nil
for _ , x in ipairs ( arr ) do
local curr = f ( x )
if result == nil or result < curr then
result = curr
end
end
return result
2022-09-03 21:13:22 +00:00
end
-- use this sparingly
function to_text ( s )
if Text_cache [ s ] == nil then
Text_cache [ s ] = App.newText ( love.graphics . getFont ( ) , s )
end
return Text_cache [ s ]
end
2022-09-04 01:10:12 +00:00
function num_panes ( )
local result = 0
for _ , column in ipairs ( Surface ) do
result = result +# column
end
return result
end