bugfix: don't query network while panning around

I'm taking this opportunity to significantly clean up the data flow.
It's easy to forget quite what A and B are supposed to do, and so to
pile on additional responsibilities on them:
  * A rebuilds Surface
  * B updates just a few attributes within Surface without structurally
    modifying it

In particular, A is not intended to modify the _inputs_ that go into
rebuilding Surface, in this app Nodes.

Perhaps I shouldn't call it A, but M or something to leave lots of space
for phases before A. For now, hopefully a comment will suffice.

3 major code paths:
  - on.initialize as well as C-v
    initialize Input_filename (v11) or Url (v12)
    initialize Nodes and Root
    initialize Cursor_node
    A()
    ensure cursor in viewport
  - on pan:
    A()
    In particular, don't touch Cursor_node
This commit is contained in:
Kartik K. Agaram 2023-10-15 20:18:19 -07:00
parent c6f09b7958
commit ee23ab653c
12 changed files with 92 additions and 79 deletions

View File

@ -1,4 +1,7 @@
on.code_change = function()
print('code changed')
load_nodes_from_url()
set_cursor(ml_from_url(Url).id)
A() -- just in case we edited something
ensure_cursor_node_within_viewport()
end

View File

@ -4,7 +4,10 @@ on.initialize = function(arg)
-- commandline arg is a url
if #arg > 0 then
Url = arg[1]
load_nodes_from_url()
set_cursor(ml_from_url(Url).id)
A()
ensure_cursor_node_within_viewport()
end
else
-- LÖVE pre-v12 doesn't have https
@ -15,14 +18,12 @@ on.initialize = function(arg)
error('When running with LÖVE v11, please pass in a file containing a Mastodon thread, and optionally an id within it to start out focused on.\n\nThis app currently needs to be invoked from a terminal.')
end
Input_filename = arg[1]
A()
load_nodes_from_input_file()
Cursor_node = Root
if #arg >= 2 then
local initial_id = arg[2]
if Nodes[initial_id] then
Cursor_node = Nodes[initial_id]
end
set_cursor(arg[2])
end
A()
ensure_cursor_node_within_viewport()
end
end

View File

@ -24,7 +24,10 @@ on.keychord_press = function(chord, key)
if cb:match('^%s*https://[^%s]*%s*$') then
print('clipboard contains a URL')
Url = trim(cb)
load_nodes_from_url()
set_cursor(ml_from_url(Url).id)
A()
ensure_cursor_node_within_viewport()
end
end
elseif chord == 'C-up' then

36
0028-A
View File

@ -1,34 +1,6 @@
A = function()
print('A')
if Major_version >= 12 then
-- reload Url from network
if Url then
-- indicate some progress
-- load can take some time, and it's synchronous, so we'll draw to screen right here
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.print('loading '..Url..'...', 5,5)
love.graphics.present()
local initial_ml = ml_from_url(Url)
local thread_data = try_load_nodes_from_url(initial_ml)
render_thread_to_surface(thread_data)
Cursor_node = Root
if Nodes[initial_ml.id] then
Cursor_node = Nodes[initial_ml.id]
end
ensure_cursor_node_within_viewport()
end
B()
else
-- LÖVE pre-v12 has no https
-- reload same thread from file
if Input_filename then
love.graphics.setFont(love.graphics.newFont(scale(20))) -- editor objects implicitly depend on current font
local f = io.open(Input_filename)
assert(f)
local thread_data = json.decode(f:read('*a'))
f:close()
render_thread_to_surface(thread_data)
end
B()
end
-- load Nodes to Surface
love.graphics.setFont(love.graphics.newFont(scale(20))) -- editor objects implicitly depend on current font
render_nodes_to_surface()
B()
end

View File

@ -25,4 +25,4 @@ ensure_cursor_node_within_viewport = function()
if dirty then
B()
end
end
end

View File

@ -1,5 +1,4 @@
render_thread_to_surface = function(thread_data)
-- side effects: Nodes, Root
render_nodes_to_surface = function()
-- design constraints:
-- trees only, no graphs or DAGs
-- parents above children
@ -12,33 +11,6 @@ render_thread_to_surface = function(thread_data)
Surface = {}
-- we're going to be computing box heights
love.graphics.setFont(love.graphics.newFont(scale(20)))
-- compute mapping from ids to nodes
Nodes = {} -- id to object
Nodes[thread_data.id] = thread_data
for _,x in ipairs(thread_data.descendants) do
Nodes[x.id] = x
end
Root = thread_data
-- compute children
for _,x in pairs(Nodes) do
parent_id = x.in_reply_to_id
if x.id ~= Root.id then
assert(parent_id)
local parent = Nodes[parent_id]
if parent then -- watch out for private posts
if parent.children == nil then
parent.children = {}
end
table.insert(parent.children, x.id)
end
end
end
-- sort children by time
for _,x in pairs(Nodes) do
if x.children then
table.sort(x.children)
end
end
-- compute number of tracks needed
for _,x in pairs(Nodes) do
if x.ntracks == nil then
@ -48,4 +20,4 @@ render_thread_to_surface = function(thread_data)
-- prepare the tracks
-- each track is 600px + 20px of gutter between nodes
render_node_and_descendants(Root.id, --[[y]] 0, --[[xlo]] 0, --[[xhi]] 620 * Root.ntracks)
end
end

19
0049-load_nodes_from_url Normal file
View File

@ -0,0 +1,19 @@
load_nodes_from_url = function()
-- side effects: Nodes, Root, Cursor_node
-- indicate some progress
-- load can take some time, and it's synchronous, so we'll draw to screen right here
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.print('loading '..Url..'...', 5,5)
love.graphics.present()
-- load JSON from Url
local initial_ml = ml_from_url(Url)
local thread = get_toot(initial_ml)
if thread.ancestors and #thread.ancestors > 0 then
-- redo with oldest ancestor
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.print('reloading from earliest ancestor '..thread.ancestors[1].url..'...', 5,5)
love.graphics.present()
thread = get_toot(ml_from_url(thread.ancestors[1].url))
end
load_nodes_from_json(thread, initial_ml.id)
end

View File

@ -1,11 +0,0 @@
try_load_nodes_from_url = function(initial_ml)
local result = get_toot(initial_ml)
if result.ancestors and #result.ancestors > 0 then
-- redo with oldest ancestor
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.print('reloading from earliest ancestor '..result.ancestors[1].url..'...', 5,5)
love.graphics.present()
result = get_toot(ml_from_url(result.ancestors[1].url))
end
return result
end

View File

@ -0,0 +1,9 @@
load_nodes_from_input_file = function()
-- side effects: Nodes, Root, Cursor_node
-- load JSON from Input_filename
local f = io.open(Input_filename)
assert(f)
local thread = json.decode(f:read('*a'))
f:close()
load_nodes_from_json(thread)
end

31
0054-load_nodes_from_json Normal file
View File

@ -0,0 +1,31 @@
load_nodes_from_json = function(thread_data, cursor_id)
-- massage JSON into a table from id to node
Nodes = {} -- id to object
Nodes[thread_data.id] = thread_data
for _,x in ipairs(thread_data.descendants) do
Nodes[x.id] = x
end
-- initialize Root
Root = thread_data
-- compute children
for _,x in pairs(Nodes) do
parent_id = x.in_reply_to_id
if x.id ~= Root.id then
assert(parent_id)
local parent = Nodes[parent_id]
if parent then -- watch out for private posts
if parent.children == nil then
parent.children = {}
end
table.insert(parent.children, x.id)
end
end
end
-- sort children by time
for _,x in pairs(Nodes) do
if x.children then
table.sort(x.children)
end
end
end

7
0055-set_cursor Normal file
View File

@ -0,0 +1,7 @@
set_cursor = function(cursor_id)
-- position Cursor on the specific toot at Url
Cursor_node = Root
if Nodes[cursor_id] then
Cursor_node = Nodes[cursor_id]
end
end

View File

@ -8,6 +8,13 @@ record those here.
- from defaults
- default_map absent/present
- run app using LÖVE v11 and `lua unfurl.lua` shim. Ensure cursor is on correct node and visible.
- run app using LÖVE v12. Ensure cursor is on correct node and visible.
Paste in a new thread using `ctrl+v`. Ensure cursor is on correct node and visible.
- pan around. Ensure cursor node can go out of viewport.
- Press `ctrl`+arrow keys. Ensure cursor moves to correct node and becomes
visible.
* How the screen looks. Our tests use a level of indirection to check text and
graphics printed to screen, but not the precise pixels they translate to.
- where exactly the cursor is drawn to highlight a given character