techmeet.love/run.lua

1736 lines
61 KiB
Lua

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}
run = {}
profile = require 'profile'
-- 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.
Editor_state = {}
-- called both in tests and real run
function run.initialize_globals()
-- 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 = {}
-- The surface may show the same file in multiple panes. Share links between
-- files (which will never go in Surface).
Links = {}
-- 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,
show_debug=false,
palette=nil, -- {command='', alternative_index=1, candidates=nil}
search_term='',
search_backup_x=nil, search_backup_y=nil, search_backup_cursor_pane=nil,
search_all_query=nil, search_all_terms=nil,
search_all_progress=nil,
search_all_progress_indicator=nil,
search_all_pane=nil,
}
-- display settings that are constants
Font_filename = 'NotoSansJP-Regular.otf' -- in current directory
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 = nil
Directory_error = nil -- Any error encountered while determining Directory. If this is ever set, the app will do nothing but display it. (We can't rely on 'error' for that because we don't want to open up the sources in that situation.)
-- blinking cursor
Cursor_time = 0
-- a read-only buffer for errors
Error_log = edit.initialize_state(0, 0, Display_settings.column_width, Font_height, Line_height)
Error_log.id = 'errors'
Error_log.filename = nil -- Will live within Directory. Don't forget to set this once Directory is initialized.
Error_log.editable = false
Text.redraw_all(Error_log)
Current_error = nil
Current_error_time = nil
end
-- called only for real run
function run.initialize(arg)
log_new('run')
if Settings then
print('loading settings')
run.load_settings()
Directory = Settings.data_directory
else
print('no saved settings; initializing to defaults')
run.initialize_default_settings()
end
if #arg > 0 then
if not is_absolute_path(arg[1]) then
Directory_error = 'Please use an unambiguous absolute path for the notes directory.'
return
end
Directory = arg[1]
print('setting Directory from commandline: '..arg[1])
if Directory:sub(#Directory,#Directory) ~= '/' then
Directory = Directory..'/'
end
print('Directory is now '..Directory)
end
if Directory == nil then
print('setting Directory_error')
if Settings == nil then
Directory_error =
"Please run techmeet.love once from a terminal window and specify the\n"..
"location of your notes. The location will be remembered in future.\n"..
"Thank you! If all goes well, you won't see this message ever again."
else
Directory_error =
"Please perform a one-time migration for your notes:\n"..
"\n"..
"* (optional) Move any existing notes in "..App.save_dir.."data\n"..
" or similar locations to your preferred location.\n"..
"\n"..
"* (optional) Move any existing "..App.save_dir.."config\n"..
" file _inside_ your notes directory to preserve any prior state\n"..
" of your note-taking surface (open columns, etc.).\n"..
"\n"..
"* Please run techmeet.love once from a terminal window and specify the\n"..
" location of your notes as an absolute path. This location will be\n"..
" remembered in future.\n"..
"\n"..
"Thank you! If all goes well, you won't see this message ever again."
end
return
end
assert(Directory, 'no directory to browse notes in')
print('Directory initialized to '..Directory)
Error_log.filename = Directory..'errors'
run.load_more_settings_from_notes_directory()
Editor_state = nil -- not used outside editor tests
-- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
love.window.setTitle('techmeet.love')
if #arg > 1 then
print('ignoring commandline args after '..arg[1])
end
print_and_log('reading notes from '..Directory)
print_and_log('put any notes there (and make frequent backups)')
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)
end
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()
if rawget(_G, 'jit') then
jit.off()
jit.flush()
end
end
function print_and_log(s)
print(s)
log(3, s)
end
function run.load_settings()
Display_settings.font = love.graphics.newFont(Font_filename, Settings.font_height)
-- set up desired window dimensions and make window resizable
_, _, App.screen.flags = App.screen.size()
App.screen.flags.resizable = true
App.screen.width, App.screen.height = Settings.width, Settings.height
App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
run.set_window_position_from_settings(Settings)
Font_height = Settings.font_height
Line_height = math.floor(Font_height*1.3)
love.graphics.setFont(Display_settings.font)
end
function run.load_more_settings_from_notes_directory()
local f = App.open_for_reading(Directory..'config')
if f then
local directory_settings = json.decode(f:read())
f:close()
Display_settings.column_width = directory_settings.column_width
for _,column_name in ipairs(directory_settings.columns) do
create_column(column_name)
end
Cursor_pane.col = directory_settings.cursor_col
Cursor_pane.row = directory_settings.cursor_row
Display_settings.x = directory_settings.surface_x
Display_settings.y = directory_settings.surface_y
else
-- initialize surface with a single column
command.recently_modified()
end
end
function run.set_window_position_from_settings(settings)
local os = love.system.getOS()
if os == 'Linux' then
-- love.window.setPosition doesn't quite seem to do what is asked of it on Linux.
App.screen.move(settings.x, settings.y-37, settings.displayindex)
else
App.screen.move(settings.x, settings.y, settings.displayindex)
end
end
function run.initialize_default_settings()
Display_settings.font = love.graphics.newFont(Font_filename, Font_height)
love.graphics.setFont(Display_settings.font)
run.initialize_window_geometry()
Display_settings.column_width = 40*Display_settings.font:getWidth('m')
end
function run.initialize_window_geometry()
-- Initialize window width/height and make window resizable.
--
-- I get tempted to have opinions about window dimensions here, but they're
-- non-portable:
-- - maximizing doesn't work on mobile and messes things up
-- - maximizing keeps the title bar on screen in Linux, but off screen on
-- Windows. And there's no way to get the height of the title bar.
-- It seems more robust to just follow LÖVE's default window size until
-- someone overrides it.
App.screen.width, App.screen.height, App.screen.flags = App.screen.size()
App.screen.flags.resizable = true
App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
end
function run.resize(w, h)
App.screen.width, App.screen.height = w, h
if Directory_error then return end
--? 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), love.graphics.getFont(), Font_height, Line_height)
result.id = id
result.filename = Directory..id
load_from_disk(result)
-- links are shared across all instances of an id, so never clobber them once loaded
if Links[id] == nil then
Links[id] = load_links(id)
end
result.font_height = Font_height
result.line_height = Line_height
result.editable = false
edit.check_locs(result)
Text.redraw_all(result)
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
rehydrate_pane(pane)
for i=1,#pane.lines do
local line = pane.lines[i]
if line.mode == 'text' then
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
assert(false, ('unknown line mode %s'):format(line.mode))
end
end
if Links[pane.id] and not empty(Links[pane.id]) then
y = y + 5+Line_height+5 -- for crosslinks
end
pane._height = y
end
-- 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
function plan_draw()
--? print_and_log(('plan_draw %d,%d'):format(Display_settings.x, Display_settings.y))
Panes_to_draw = {}
Column_headers_to_draw = {}
local sx = Padding_horizontal + Margin_left
if Grab_pane then rehydrate_pane(Grab_pane) end
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_and_log(('plan_draw bounds %d,%d %s %d,%d'):format(column_index, pane_index, pane.id, sx,sy))
if should_show_pane(pane, sy) then
if not App.mouse_down(1) then
for _,line_cache in ipairs(pane.line_cache) do line_cache.starty = nil end -- just in case we scrolled at some point without sending information to any of the panes currently on screen
end
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
if body_sy < Display_settings.y then
pane.screen_top1, y_offset = schema1_of_y(pane, Display_settings.y - body_sy)
pane.top = 0
else
pane.screen_top1 = {line=1, pos=1}
pane.top = body_sy - Display_settings.y
end
pane.top = Header_height + Margin_above + 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
end
end
function run.draw()
if Directory_error then
love.graphics.print(Directory_error, 50,50)
return
end
--? print('draw', Display_settings.y)
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*pane.font:getWidth('m')
pane.right = App.screen.width/2 + 20*pane.font:getWidth('m')
pane.width = pane.right - pane.left
edit.draw(pane)
end
end
else
assert(false, ('pensieve is in an unknown mode %s'):format(Display_settings.mode))
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()
else
if Display_settings.palette then
draw_command_palette()
else
show_error()
end
end
draw_debug()
end
function draw_normal_mode()
assert(Cursor_pane.col, 'no current column')
assert(Cursor_pane.row, 'no current row')
--? print('draw', Display_settings.x, Display_settings.y)
for _,pane in ipairs(Panes_to_draw) do
assert(pane.top, "pane has no top coordinate; there's likely a problem in plan_draw")
--? if pane.column_index == 1 and pane.pane_index == 1 then
--? print('draw', pane.id, 'from y', pane.top, 'down to screen height', App.screen.height)
--? print('screen top', pane.screen_top1.line, pane.screen_top1.pos)
--? print('cursor', pane.cursor1.line, pane.cursor1.pos)
--? 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, 'pane has no title')
App.color(Pane_title_color)
App.screen.print(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)
local links = Links[pane.id]
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(pane.font, label, x, pane.bottom)
end
x = x + pane.font:getWidth(label) + 10 + 10
end
-- links we don't know about, just in case
for link,_ in pairs(links) do
if not Opposite[link] then
draw_link(pane.font, link, x, pane.bottom)
x = x + pane.font:getWidth(link) + 10 + 10
end
end
pane.bottom = pane.bottom + 5+Line_height+5
end
function draw_link(font, label, x,y)
App.color(Crosslink_color)
love.graphics.print(label, x, y+5)
App.color(Crosslink_background_color)
love.graphics.rectangle('fill', x-5, y+3, font:getWidth(label)+10, 2+Line_height+2)
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
end
function run.update(dt)
if Directory_error then return end
update_footprint()
Cursor_time = Cursor_time + dt
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.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
if Current_error then
if App.get_time() - Current_error_time > 3 then
Current_error = nil
Current_error_time = nil
end
end
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
end
function run.quit()
if Directory_error then return end
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
stop_editing(pane)
end
end
end
function run.settings()
-- avoid an infinite loop if handle_error ever tries to save settings
assert(Directory_error == nil,
"tried to save settings when we couldn't determine the directory to browse notes in")
-- side effect: save notes-related settings inside Directory
local column_names = {}
for _,column in ipairs(Surface) do
table.insert(column_names, column.name)
end
local status = App.write_file(Directory..'config',
json.encode({
columns=column_names,
column_width=Display_settings.column_width,
cursor_col=Cursor_pane.col,
cursor_row=Cursor_pane.row,
surface_x=Display_settings.x,
surface_y=Display_settings.y,
}))
assert(status, 'failed to write settings')
if Settings == nil then Settings = {} end
Settings.x, Settings.y, Settings.displayindex = App.screen.position()
return {
x=Settings.x, y=Settings.y, displayindex=Settings.displayindex,
width=App.screen.width, height=App.screen.height,
font_height=Font_height,
data_directory=Directory,
}
end
function run.mouse_press(x,y, mouse_button)
if Directory_error then return end
--? print('app mouse pressed', x,y)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
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
mouse_press_in_normal_mode(x,y, mouse_button)
elseif Display_settings.mode == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_press(pane, x,y, mouse_button)
end
end
else
assert(false, ('pensieve is in an unknown mode %s'):format(Display_settings.mode))
end
end
function clear_selections()
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane.selection1 = {}
end
end
end
function mouse_press_in_normal_mode(x,y, mouse_button)
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
edit.mouse_press(pane, x,y, mouse_button)
pane._height = nil
end
end
else
Pan = {x=sx, y=sy}
end
end
function run.mouse_release(x,y, mouse_button)
if Directory_error then return end
--? print('app mouse released')
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if in_pane(x,y) or Display_settings.mode == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_release(pane, x,y, mouse_button)
end
end
end
Pan = {}
end
function run.mouse_wheel_move(dx,dy)
if Directory_error then return end
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if pane.editable then
if pane.cursor_x then
edit.mouse_wheel_move(pane, dx,dy)
plan_draw()
return
end
end
end
end
-- shift+scroll wheel doesn't set dx for me; manually do so if necessary
if App.shift_down() then
if dx == 0 then
dx,dy = dy,dx
end
end
if dy > 0 then
for i=1,math.floor(dy) do
pan_up()
end
plan_draw()
elseif dy < 0 then
for i=1,math.floor(-dy) do
pan_down()
end
plan_draw()
end
if dx > 0 then
for i=1,math.floor(dx) do
pan_left()
end
plan_draw()
elseif dx < 0 then
for i=1,math.floor(-dx) do
pan_right()
end
plan_draw()
end
end
function run.text_input(t)
if Directory_error then return end
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
--? print('textinput', t)
-- hotkeys operating on the cursor pane
if Display_settings.palette then
Display_settings.palette.command = Display_settings.palette.command..t
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
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
--? print(('%s typed in editor pane'):format(t))
edit.text_input(pane, t)
bring_cursor_of_cursor_pane_in_view('down')
pane._height = nil
plan_draw()
end
end
end
end
end
elseif Display_settings.mode == 'search' then
--? print('insert', t)
Display_settings.search_term = Display_settings.search_term..t
-- 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()
elseif Display_settings.mode == 'search_all' then
Display_settings.search_all_query = Display_settings.search_all_query..t
elseif Display_settings.mode == 'searching_all' then
Display_settings.mode = 'normal'
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
edit.text_input(pane, t)
end
end
end
else
assert(false, ('pensieve is in an unknown mode %s'):format(Display_settings.mode))
end
end
function run.keychord_press(chord, key)
if Directory_error then return end
if App.run_tests == nil then
log(2, os.date('%Y/%m/%d/%H-%M-%S')..' app.keychord_press '..chord..' '..key)
end
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
-- 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.palette then
keychord_press_on_command_palette(chord, key)
elseif Display_settings.mode == 'normal' then
if chord == 'C-return' then
Display_settings.palette = {command='', alternative_index=1, candidates = initial_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]
if pane then
if pane.editable then
if chord == 'C-e' then
--? profile.stop()
--? print(profile.report())
command.exit_editing()
elseif pane.cursor_x == nil then
-- ignore if cursor is not visible on screen
assert(pane.cursor_y == nil, 'cursor x is not set but y is set')
panning_keychord_press(chord, key)
plan_draw()
else
local old_top = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}
edit.keychord_press(pane, chord, key)
--? print_and_log(('run.keychord_press A %s %d,%d'):format(chord, Display_settings.x, Display_settings.y))
if chord == 'pagedown' or chord == 'S-pagedown' then
pan_surface_to_screen_top_of_cursor_pane()
elseif chord == 'backspace' or chord == 'up' or chord == 'left' or chord == 'pageup' then
bring_cursor_of_cursor_pane_in_view('up')
else
bring_cursor_of_cursor_pane_in_view('down')
end
pane._height = nil
--? print_and_log(('run.keychord_press Z %s %d,%d'):format(chord, Display_settings.x, Display_settings.y))
plan_draw()
end
else
keychord_press_in_normal_mode_with_immutable_pane(pane, chord, key)
plan_draw()
end
end
end
elseif Display_settings.mode == 'search' then
keychord_press_in_search_mode(chord, key)
elseif Display_settings.mode == 'search_all' then
keychord_press_in_search_all_mode(chord, key)
elseif Display_settings.mode == 'searching_all' then
interrupt_search_all()
elseif Display_settings.mode == 'maximize' then
if chord == 'C-return' then
Display_settings.palette = {command='', alternative_index=1, candidates = initial_candidates()}
else
keychord_press_in_maximize_mode(chord, key)
end
else
assert(false, ('pensieve is in an unknown mode %s'):format(Display_settings.mode))
end
end
function update_font_settings(font_height)
local column_width_in_ems = Display_settings.column_width / Display_settings.font:getWidth('m')
Font_height = font_height
Display_settings.font = love.graphics.newFont(Font_filename, Font_height)
Line_height = math.floor(font_height*1.3)
Display_settings.column_width = column_width_in_ems*Display_settings.font:getWidth('m')
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane.font = Display_settings.font
pane.font_height = Font_height
pane.line_height = Line_height
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 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
if search_next_in_pane(pane) then
--? 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.check_locs(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.check_locs(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
local pane = Surface[Cursor_pane.col][current_pane_index]
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
-- 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.check_locs(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_backup = {cursor={line=pane.cursor1.line, pos=pane.cursor1.pos}, screen_top={line=pane.screen_top1.line, pos=pane.screen_top1.pos}}
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
if Text.le1(pane.search_backup.cursor, pane.cursor1) then
-- 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.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
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
if search_previous_in_pane(pane) then
--? 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)
local pane = Surface[Cursor_pane.col][current_pane_index]
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
-- 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_backup = {cursor={line=pane.cursor1.line, pos=pane.cursor1.pos}, screen_top={line=pane.screen_top1.line, pos=pane.screen_top1.pos}}
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
if Text.lt1(pane.cursor1, pane.search_backup.cursor) then
-- 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.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
end
function pan_surface_to_screen_top_of_cursor_pane()
if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.screen_top1)
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
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 - pane.font:getWidth('m')
local cursor_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.cursor1)
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
elseif dir == 'down' then
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('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
assert(App.screen.height - (cursor_sy-Display_settings.y) > 1.5*Line_height, 'ugh, ancient bug is back: panning the viewport when cursor falls off')
end
else
assert(false, ('unknown dir %s'):format(dir))
end
Display_settings.x = math.max(Display_settings.x, 0)
Display_settings.y = math.max(Display_settings.y, 0)
end
function clear_all_search_terms()
for col,column in ipairs(Surface) do
for row,pane in ipairs(column) do
pane.search_term = nil
end
end
end
function keychord_press_in_maximize_mode(chord, key)
if Cursor_pane.col < 1 then
print_and_log('keychord_press (maximized): no current note to edit')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
print_and_log('keychord_press (maximized): no current note to edit')
return
end
if pane.editable then
if chord == 'C-e' then
command.exit_editing()
else
edit.keychord_press(pane, chord, key)
end
else
if chord == 'C-e' then
command.edit_note()
elseif chord == 'C-c' then
edit.keychord_press(pane, chord, key)
end
end
end
function keychord_press_in_normal_mode_with_immutable_pane(pane, chord, key)
-- 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
panning_keychord_press(chord, key)
return
end
local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)
if not should_show_pane(pane, up_sy) then
panning_keychord_press(chord, key)
return
end
if chord == 'C-e' then
command.edit_note()
profile.start()
elseif chord == 'C-c' then
edit.keychord_press(pane, chord, key)
else
panning_keychord_press(chord, key)
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
function keychord_press_in_search_mode(chord, key)
if chord == 'escape' then
Display_settings.mode = 'normal'
clear_all_search_terms()
dehydrate_all_panes()
-- 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()
dehydrate_all_panes()
-- 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)
-- 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.get_clipboard()
-- 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
edit.keychord_press(pane, chord, key)
end
end
end
end
function keychord_press_in_search_all_mode(chord, key)
if chord == 'escape' then
Display_settings.mode = 'normal'
-- don't forget search text
Display_settings.search_all_progress = 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)
--? 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.get_clipboard()
--? 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, 'something is at negative y on the surface')
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, 'error in mapping y coordinate to schema-1')
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(pane, line_index, left, right)
local line = pane.lines[line_index]
local line_cache = pane.line_cache[line_index]
if line.mode == 'text' then
return pane.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, 'multiple panes were editable')
end
function stop_editing(pane)
log(2, 'stop_editing: '..pane.id)
edit.quit(pane)
-- save symmetric links
for rel,x in pairs(Links[pane.id]) do
process_all_links(x, function(target_id)
log(2, 'stop_editing '..pane.id..': saving links for '..target_id)
if Links[target_id] then
save_links(target_id)
end
end)
end
if Display_settings.mode ~= 'maximize' then
refresh_panes(pane)
end
pane.editable = false
end
function panning_keychord_press(chord, key)
if chord == 'up' then
pan_up()
elseif chord == 'down' then
pan_down()
elseif chord == 'left' then
pan_left()
elseif chord == 'right' then
pan_right()
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()
elseif chord == 'C-left' then
command.left_one_column()
elseif chord == 'C-right' then
command.right_one_column()
end
--? print('after', Cursor_pane.col, Cursor_pane.row)
end
function pan_up()
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
end
function pan_down()
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
end
function pan_left()
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
end
function pan_right()
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
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
if p ~= pane then
Surface[x][y] = load_pane(pane.id)
end
end
end
end
plan_draw()
end
-- 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.
function dehydrate_all_panes()
for x,col in ipairs(Surface) do
for y,p in ipairs(col) do
p._height = nil
p.line_cache = {}
end
end
plan_draw()
end
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
function run.key_release(key, scancode)
if Directory_error then return end
--? print('key release', key)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
edit.key_release(pane, key, scancode)
end
end
function clear_all_pane_heights()
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)
return 1 + math.floor(x / (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right))
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
end
function width(s)
return love.graphics.getFont():getWidth(s)
end
function num_panes()
local result = 0
for _,column in ipairs(Surface) do
result = result+#column
end
return result
end