techmeet.love/run.lua

1619 lines
58 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 = {}
-- 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. This cache tries to
-- share data between such aliases:
-- line contents when panes are not editable (editable panes can diverge)
-- links between files (never in Surface, can never diverge between panes)
Cache = {}
-- 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_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/'
-- This little bit of state ensures we don't mess with a pane's screen_top
-- if it was just used to update the viewport.
Editable_cursor_pane_updated_screen_top = false
-- a few text objects we can avoid recomputing unless the font changes
Text_cache = {}
-- blinking cursor
Cursor_time = 0
end
-- called only for real run
function run.initialize(arg)
love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
love.keyboard.setKeyRepeat(true)
Editor_state = nil -- not used outside editor tests
love.graphics.setBackgroundColor(1,1,1)
assert(#arg <= 1)
if #arg == 1 then
Directory = 'data.'..arg[1]..'/'
end
love.window.setTitle('techmeet.love')
print('reading notes from '..love.filesystem.getSaveDirectory()..'/'..Directory)
print('put any notes there (and make frequent backups)')
if Settings.width then
load_settings()
else
initialize_default_settings()
end
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 load_settings()
-- maximize window to determine maximum allowable dimensions
love.window.setMode(0, 0) -- maximize
App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
--? print('max height', App.screen.height)
-- 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)
App.screen.width, App.screen.height = Settings.width, Settings.height
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
love.window.setPosition(Settings.x, Settings.y, Settings.displayindex)
Font_height = Settings.font_height
Line_height = math.floor(Font_height*1.3)
love.graphics.setFont(love.graphics.newFont('NotoSansJP-Regular.otf', Font_height))
Em = App.newText(love.graphics.getFont(), 'm')
Display_settings.column_width = Settings.column_width
for _,column_name in ipairs(Settings.columns) do
create_column(column_name)
end
Cursor_pane.col = Settings.cursor_col
Cursor_pane.row = Settings.cursor_row
Display_settings.x = Settings.surface_x
Display_settings.y = Settings.surface_y
end
function initialize_default_settings()
initialize_window_geometry()
love.graphics.setFont(love.graphics.newFont('NotoSansJP-Regular.otf', 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()
-- 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
App.screen.width = App.screen.width-100
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
--? print('resize:', App.screen.width, App.screen.height)
plan_draw()
end
function initialize_cache_if_necessary(id)
if Cache[id] then return end
--? print('init:', id)
Cache[id] = {id=id, filename=Directory..id, left=0, right=Display_settings.column_width, lines={}, line_cache={}}
load_from_disk(Cache[id])
Cache[id].links = load_links(id)
end
function load_pane(id)
--? print('load pane from file', id)
initialize_cache_if_necessary(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
result.lines = Cache[id].lines
result.line_cache = deepcopy(Cache[id].line_cache) -- should be tiny; deepcopy is just to eliminate any chance of aliasing
result.font_height = Font_height
result.line_height = Line_height
result.em = Em
result.editable = false
edit.fixup_cursor(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
for i=1,#pane.lines do
local line = pane.lines[i]
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
if line.mode == 'text' then
pane.line_cache[i].fragments = nil
pane.line_cache[i].screen_line_starting_pos = nil
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
if Cache[pane.id].links and not empty(Cache[pane.id].links) 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(options)
--? print('update pane bounds')
--? print(#Surface, 'columns;', num_panes(), 'panes')
Panes_to_draw = {}
Column_headers_to_draw = {}
local sx = Padding_horizontal + Margin_left
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
if should_update_screen_top(column_index, pane_index, pane, options) then
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
end
end
function should_update_screen_top(column_index, pane_index, pane, options)
if column_index ~= Cursor_pane.col then return true end
if pane_index ~= Cursor_pane.row then return true end
-- update the cursor pane either if it's not editable, or
-- if it was explicitly requested
if not pane.editable then return true end
if options == nil then return true end
if not options.ignore_editable_cursor_pane then return true end
if not Editable_cursor_pane_updated_screen_top then return true end
return false
end
function run.draw()
--? print(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*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()
elseif Display_settings.show_palette then
draw_command_palette()
end
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)
local links = Cache[pane.id].links
if links == nil then return end
if empty(links) then return end
local x = pane.left
for _,label in ipairs(Edge_list) do
if Text_cache[label] == nil then
Text_cache[label] = App.newText(love.graphics.getFont(), label)
end
if links[label] then
draw_link(label, x, pane.bottom)
end
x = x + App.width(Text_cache[label]) + 10 + 10
end
-- links we don't know about, just in case
for link,_ in pairs(links) do
if not Opposite[link] then
if Text_cache[link] == nil then
Text_cache[link] = App.newText(love.graphics.getFont(), link)
end
draw_link(link, x, pane.bottom)
x = x + App.width(Text_cache[link]) + 10 + 10
end
end
pane.bottom = pane.bottom + 5+Line_height+5
end
function draw_link(label, x,y)
App.color(Crosslink_color)
love.graphics.draw(Text_cache[label], x, y+5)
App.color(Crosslink_background_color)
love.graphics.rectangle('fill', x-5, y+3, App.width(Text_cache[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)
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.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
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 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
end
function run.settings()
assert(Settings)
if Current_app == 'run' then
Settings.x, Settings.y, Settings.displayindex = love.window.getPosition()
end
local column_names = {}
for _,column in ipairs(Surface) do
table.insert(column_names, column.name)
end
return {
x=Settings.x, y=Settings.y, displayindex=Settings.displayindex,
width=App.screen.width, height=App.screen.height,
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,
}
end
function run.mouse_pressed(x,y, mouse_button)
--? 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_pressed_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_pressed(pane, x,y, mouse_button)
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
function mouse_pressed_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
--? print('click on pane')
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_pressed(pane, x,y, mouse_button)
pane._height = nil
end
end
else
Pan = {x=sx, y=sy}
end
end
function run.mouse_released(x,y, mouse_button)
--? print('app mouse released')
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Cursor_pane.col >= 1 then
edit.mouse_released(Surface[Cursor_pane.col][Cursor_pane.row], x,y, mouse_button)
end
Pan = {}
end
function run.textinput(t)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
--? 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
if pane.cursor_x >= 0 and pane.cursor_x < App.screen.width then
if pane.cursor_y >= Header_height and pane.cursor_y < App.screen.height then
--? print(('%s typed in editor pane'):format(t))
local old_top = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}
edit.textinput(pane, t)
maybe_update_screen_top_of_cursor_pane(pane, old_top)
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
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()
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
edit.textinput(pane, t)
end
end
end
else
print(Display_settings.mode)
assert(false)
end
end
function run.keychord_pressed(chord, key)
--? print('keychord press', chord)
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.show_palette then
keychord_pressed_on_command_palette(chord, key)
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]
if pane and pane.editable then
keychord_pressed_on_editable_pane(pane, chord, key)
else
keychord_pressed_in_normal_mode_with_immutable_pane(pane, chord, key)
end
-- editable cursor pane will have already updated its screen_top, so don't clobber it here
plan_draw{ignore_editable_cursor_pane=true}
end
elseif Display_settings.mode == 'search' then
keychord_pressed_in_search_mode(chord, key)
elseif Display_settings.mode == 'search_all' then
keychord_pressed_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.show_palette = true
Display_settings.palette_candidates = candidates()
else
keychord_pressed_in_maximize_mode(chord, key)
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('NotoSansJP-Regular.otf', 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
if search_next_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) 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.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
if search_next_in_pane(Surface[Cursor_pane.col][current_pane_index]) 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.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
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.search_text = 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(Surface[Cursor_pane.col][Cursor_pane.row]) 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)
if search_previous_in_pane(Surface[Cursor_pane.col][current_pane_index]) 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_text = Display_settings.search_text
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.search_text = 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 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
function keychord_pressed_in_maximize_mode(chord, key)
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
edit.keychord_pressed(pane, chord, key)
end
else
if chord == 'C-e' then
command.edit_note()
elseif chord == 'C-c' then
edit.keychord_pressed(pane, chord, key)
end
end
end
function keychord_pressed_on_editable_pane(pane, chord, key)
-- ignore if cursor is not visible on screen
if pane.cursor_x == nil then
assert(pane.cursor_y == nil)
panning_keychord_pressed(chord, key)
return
end
if chord == 'C-e' then
command.exit_editing()
else
--? print(('%s pressed in editor pane'):format(chord))
--? print(pane.cursor_x, pane.cursor_y)
local old_top = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}
edit.keychord_pressed(pane, chord, key)
maybe_update_screen_top_of_cursor_pane(pane, old_top)
pane._height = nil
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)
--? print(eq(old_top, pane.screen_top1), eq(old_top, {line=1, pos=1}), pane.top, cursor_sy, cursor_sy - Display_settings.y, App.screen.height - Header_height - Line_height)
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
Editable_cursor_pane_updated_screen_top = not eq(old_top, pane.screen_top1)
if Editable_cursor_pane_updated_screen_top then
--? print(('screen top changed from (%d,%d) to (%d,%d)'):format(old_top.line, old_top.pos, pane.screen_top1.line, pane.screen_top1.pos))
--? print('updating viewport based on screen top')
--? print('from', Display_settings.y, y_of_schema1(pane, pane.screen_top1))
Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.screen_top1)
--? print('to', Display_settings.y)
Surface.cursor_on_screen_check = true -- cursor was on screen before keystroke, so it should remain on screen after
end
end
function keychord_pressed_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_pressed(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_pressed(chord, key)
return
end
if chord == 'C-e' then
command.edit_note()
elseif chord == 'C-c' then
edit.keychord_pressed(pane, chord, key)
else
panning_keychord_pressed(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_pressed_in_search_mode(chord, key)
if chord == 'escape' then
Display_settings.mode = 'normal'
clear_all_search_terms()
clean_up_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()
clean_up_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)
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
edit.keychord_pressed(pane, chord, key)
end
end
end
end
function keychord_pressed_in_search_all_mode(chord, key)
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
for rel,target in pairs(Cache[pane.id].links) do
initialize_cache_if_necessary(target)
save_links(target)
end
if Display_settings.mode ~= 'maximize' then
refresh_panes(pane)
end
pane.editable = false
end
function panning_keychord_pressed(chord, key)
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')
Cache[pane.id].lines = pane.lines
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
function clean_up_panes()
for x,col in ipairs(Surface) do
for y,p in ipairs(col) do
p._height = nil
Text.redraw_all(p)
end
end
plan_draw()
end
function run.key_released(key, scancode)
--? 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_released(pane, key, scancode)
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)
return 1 + math.floor(x / (Padding_horizontal + Display_settings.column_width))
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
-- 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
function num_panes()
local result = 0
for _,column in ipairs(Surface) do
result = result+#column
end
return result
end