1619 lines
58 KiB
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
|