2022-06-03 22:14:53 +01:00
-- text editor, particularly text drawing, horizontal wrap, vertical scrolling
2022-05-18 04:29:08 +01:00
Text = { }
2022-05-18 06:36:10 +01:00
local utf8 = require ' utf8 '
2022-06-03 22:14:53 +01:00
require ' search '
require ' select '
2022-06-02 23:45:25 +01:00
require ' undo '
2022-06-03 22:14:53 +01:00
require ' text_tests '
2022-06-02 23:45:25 +01:00
2022-05-22 04:52:44 +01:00
-- return values:
-- y coordinate drawn until in px
-- position of start of final screen line drawn
2022-05-20 14:09:37 +01:00
function Text . draw ( line , line_width , line_index )
2022-05-23 19:02:37 +01:00
--? print('text.draw')
2022-05-18 04:29:08 +01:00
love.graphics . setColor ( 0 , 0 , 0 )
2022-05-20 01:29:14 +01:00
-- wrap long lines
local x = 25
local y = line.y
local pos = 1
2022-05-22 04:52:44 +01:00
local screen_line_starting_pos = 1
2022-05-20 01:29:14 +01:00
if line.fragments == nil then
Text.compute_fragments ( line , line_width )
2022-05-18 04:29:08 +01:00
end
2022-05-24 06:31:36 +01:00
if line.screen_line_starting_pos == nil then
Text.populate_screen_line_starting_pos ( line_index )
end
2022-05-24 07:00:07 +01:00
--? print('--')
2022-05-20 01:29:14 +01:00
for _ , f in ipairs ( line.fragments ) do
local frag , frag_text = f.data , f.text
-- render fragment
2022-06-03 07:36:07 +01:00
local frag_width = App.width ( frag_text )
2022-05-29 16:12:47 +01:00
local frag_len = utf8.len ( frag )
2022-05-29 06:24:17 +01:00
--? local s=tostring
2022-05-24 06:31:36 +01:00
--? print('('..s(x)..','..s(y)..') '..frag..'('..s(frag_width)..' vs '..s(line_width)..') '..s(line_index)..' vs '..s(Screen_top1.line)..'; '..s(pos)..' vs '..s(Screen_top1.pos)..'; bottom: '..s(Screen_bottom1.line)..'/'..s(Screen_bottom1.pos))
2022-05-20 01:29:14 +01:00
if x + frag_width > line_width then
assert ( x > 25 ) -- no overfull lines
2022-05-24 00:35:25 +01:00
-- update y only after drawing the first screen line of screen top
2022-05-29 07:11:53 +01:00
if Text.lt1 ( Screen_top1 , { line = line_index , pos = pos } ) then
2022-06-03 07:36:07 +01:00
y = y + Line_height
if y + Line_height > App.screen . height then
2022-05-24 06:31:36 +01:00
--? print('b', y, App.screen.height, '=>', screen_line_starting_pos)
2022-05-22 06:08:13 +01:00
return y , screen_line_starting_pos
end
2022-05-24 06:31:36 +01:00
screen_line_starting_pos = pos
--? print('text: new screen line', y, App.screen.height, screen_line_starting_pos)
2022-05-22 06:08:13 +01:00
end
2022-05-24 06:31:36 +01:00
x = 25
2022-05-20 01:29:14 +01:00
end
2022-05-24 06:31:36 +01:00
--? print('checking to draw', pos, Screen_top1.pos)
2022-05-23 23:34:05 +01:00
-- don't draw text above screen top
2022-05-29 07:11:53 +01:00
if Text.le1 ( Screen_top1 , { line = line_index , pos = pos } ) then
2022-05-29 16:12:47 +01:00
if Selection1.line then
local lo , hi = Text.clip_selection ( line_index , pos , pos + frag_len )
2022-06-03 22:30:51 +01:00
Text.draw_highlight ( line , x , y , pos , lo , hi )
2022-05-29 16:12:47 +01:00
end
2022-05-24 07:00:07 +01:00
--? print('drawing '..frag)
2022-06-03 07:36:07 +01:00
App.screen . draw ( frag_text , x , y )
2022-05-20 14:58:17 +01:00
end
2022-05-20 01:29:14 +01:00
-- render cursor if necessary
2022-05-22 00:24:07 +01:00
if line_index == Cursor1.line then
if pos <= Cursor1.pos and pos + frag_len > Cursor1.pos then
2022-06-03 06:53:34 +01:00
if Search_term then
if Lines [ Cursor1.line ] . data : sub ( Cursor1.pos , Cursor1.pos + utf8.len ( Search_term ) - 1 ) == Search_term then
2022-06-03 22:37:32 +01:00
local lo_px = Text.draw_highlight ( line , x , y , pos , Cursor1.pos , Cursor1.pos + utf8.len ( Search_term ) )
2022-06-03 06:53:34 +01:00
love.graphics . setColor ( 0 , 0 , 0 )
2022-06-03 22:37:32 +01:00
love.graphics . print ( Search_term , x + lo_px , y )
2022-06-03 06:53:34 +01:00
end
else
Text.draw_cursor ( x + Text.x ( frag , Cursor1.pos - pos + 1 ) , y )
end
2022-05-20 01:29:14 +01:00
end
end
x = x + frag_width
pos = pos + frag_len
end
2022-06-03 06:53:34 +01:00
if Search_term == nil then
if line_index == Cursor1.line and Cursor1.pos == pos then
Text.draw_cursor ( x , y )
end
2022-05-20 06:35:26 +01:00
end
2022-05-22 04:52:44 +01:00
return y , screen_line_starting_pos
2022-05-18 04:29:08 +01:00
end
2022-05-20 01:29:14 +01:00
-- manual tests:
-- draw with small line_width of 100
2022-05-29 16:12:47 +01:00
2022-05-20 13:40:42 +01:00
function Text . draw_cursor ( x , y )
love.graphics . setColor ( 1 , 0 , 0 )
2022-06-04 19:46:01 +01:00
love.graphics . circle ( ' fill ' , x , y + Line_height , 3 )
2022-05-20 13:40:42 +01:00
love.graphics . setColor ( 0 , 0 , 0 )
2022-05-20 13:41:25 +01:00
Cursor_x = x
2022-06-03 07:36:07 +01:00
Cursor_y = y + Line_height
2022-05-20 13:40:42 +01:00
end
2022-05-20 06:58:04 +01:00
function Text . compute_fragments ( line , line_width )
2022-05-25 21:35:32 +01:00
--? print('compute_fragments', line_width)
2022-05-20 06:58:04 +01:00
line.fragments = { }
local x = 25
-- try to wrap at word boundaries
for frag in line.data : gmatch ( ' %S*%s* ' ) do
2022-05-23 07:17:06 +01:00
local frag_text = App.newText ( love.graphics . getFont ( ) , frag )
2022-06-03 07:36:07 +01:00
local frag_width = App.width ( frag_text )
2022-05-20 06:58:04 +01:00
--? print('x: '..tostring(x)..'; '..tostring(line_width-x)..'px to go')
--? print('frag: ^'..frag..'$ is '..tostring(frag_width)..'px wide')
if x + frag_width > line_width then
while x + frag_width > line_width do
2022-05-28 17:02:15 +01:00
--? print(x, frag, frag_width, line_width)
2022-05-20 06:58:04 +01:00
if x < 0.8 * line_width then
2022-05-23 23:38:42 +01:00
--? print(frag, x, frag_width, line_width)
2022-05-20 06:58:04 +01:00
-- long word; chop it at some letter
-- We're not going to reimplement TeX here.
2022-05-23 23:22:16 +01:00
local b = Text.nearest_pos_less_than ( frag , line_width - x )
2022-05-28 17:02:15 +01:00
assert ( b > 0 ) -- avoid infinite loop when window is too narrow
2022-05-20 06:58:04 +01:00
--? print('space for '..tostring(b)..' graphemes')
local frag1 = string.sub ( frag , 1 , b )
2022-05-23 07:17:06 +01:00
local frag1_text = App.newText ( love.graphics . getFont ( ) , frag1 )
2022-06-03 07:36:07 +01:00
local frag1_width = App.width ( frag1_text )
2022-05-23 23:38:42 +01:00
--? print(frag, x, frag1_width, line_width)
2022-05-23 23:22:16 +01:00
assert ( x + frag1_width <= line_width )
2022-05-20 06:58:04 +01:00
--? print('inserting '..frag1..' of width '..tostring(frag1_width)..'px')
table.insert ( line.fragments , { data = frag1 , text = frag1_text } )
frag = string.sub ( frag , b + 1 )
2022-05-23 07:17:06 +01:00
frag_text = App.newText ( love.graphics . getFont ( ) , frag )
2022-06-03 07:36:07 +01:00
frag_width = App.width ( frag_text )
2022-05-20 06:58:04 +01:00
end
x = 25 -- new line
end
end
if # frag > 0 then
--? print('inserting '..frag..' of width '..tostring(frag_width)..'px')
table.insert ( line.fragments , { data = frag , text = frag_text } )
end
2022-05-28 17:02:15 +01:00
x = x + frag_width
2022-05-20 06:58:04 +01:00
end
end
2022-05-21 22:03:06 +01:00
function Text . textinput ( t )
2022-05-18 06:36:10 +01:00
if love.mouse . isDown ( ' 1 ' ) then return end
2022-05-30 23:36:53 +01:00
if App.ctrl_down ( ) or App.alt_down ( ) or App.cmd_down ( ) then return end
2022-06-03 01:46:30 +01:00
if Selection1.line then
Text.delete_selection ( )
end
local before = snapshot ( Cursor1.line )
2022-05-19 04:22:57 +01:00
Text.insert_at_cursor ( t )
2022-06-03 01:46:30 +01:00
record_undo_event ( { before = before , after = snapshot ( Cursor1.line ) } )
2022-05-19 04:22:57 +01:00
end
function Text . insert_at_cursor ( t )
2022-06-05 20:04:21 +01:00
--? print(Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos, Screen_bottom1.line, Screen_bottom1.pos)
2022-05-18 06:36:10 +01:00
local byte_offset
2022-05-22 00:24:07 +01:00
if Cursor1.pos > 1 then
2022-05-29 15:57:35 +01:00
byte_offset = utf8.offset ( Lines [ Cursor1.line ] . data , Cursor1.pos )
2022-05-18 06:36:10 +01:00
else
2022-05-29 15:57:35 +01:00
byte_offset = 1
2022-05-18 06:36:10 +01:00
end
2022-06-05 20:04:21 +01:00
if byte_offset == nil then
print ( Cursor1.line , Cursor1.pos , byte_offset , Lines [ Cursor1.line ] . data )
assert ( false )
end
2022-05-29 15:57:35 +01:00
Lines [ Cursor1.line ] . data = string.sub ( Lines [ Cursor1.line ] . data , 1 , byte_offset - 1 ) .. t .. string.sub ( Lines [ Cursor1.line ] . data , byte_offset )
2022-05-22 00:24:07 +01:00
Lines [ Cursor1.line ] . fragments = nil
2022-05-25 22:31:16 +01:00
Lines [ Cursor1.line ] . screen_line_starting_pos = nil
2022-05-22 00:24:07 +01:00
Cursor1.pos = Cursor1.pos + 1
2022-06-05 18:08:28 +01:00
if Cursor_y >= App.screen . height - Line_height then
2022-06-05 16:29:38 +01:00
Text.populate_screen_line_starting_pos ( Cursor1.line )
Text.snap_cursor_to_bottom_of_screen ( )
2022-06-05 17:50:19 +01:00
--? print('=>', Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos, Screen_bottom1.line, Screen_bottom1.pos)
2022-06-05 16:29:38 +01:00
end
2022-05-18 06:36:10 +01:00
end
-- Don't handle any keys here that would trigger love.textinput above.
function Text . keychord_pressed ( chord )
2022-05-29 16:12:47 +01:00
--? print(chord)
2022-05-29 06:27:47 +01:00
--== shortcuts that mutate text
2022-05-18 06:36:10 +01:00
if chord == ' return ' then
2022-06-03 01:46:30 +01:00
local before_line = Cursor1.line
local before = snapshot ( before_line )
2022-06-03 02:35:18 +01:00
Text.insert_return ( )
2022-05-18 06:36:10 +01:00
save_to_disk ( Lines , Filename )
2022-06-03 01:46:30 +01:00
record_undo_event ( { before = before , after = snapshot ( before_line , Cursor1.line ) } )
2022-05-19 04:22:57 +01:00
elseif chord == ' tab ' then
2022-06-03 01:46:30 +01:00
local before = snapshot ( Cursor1.line )
2022-05-19 04:22:57 +01:00
Text.insert_at_cursor ( ' \t ' )
save_to_disk ( Lines , Filename )
2022-06-03 01:46:30 +01:00
record_undo_event ( { before = before , after = snapshot ( Cursor1.line ) } )
2022-05-18 06:36:10 +01:00
elseif chord == ' backspace ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Text.delete_selection ( )
2022-06-02 23:45:25 +01:00
save_to_disk ( Lines , Filename )
2022-05-29 16:12:47 +01:00
return
end
2022-06-03 01:46:30 +01:00
local before
if Cursor1.pos > 1 then
before = snapshot ( Cursor1.line )
2022-05-22 00:24:07 +01:00
local byte_start = utf8.offset ( Lines [ Cursor1.line ] . data , Cursor1.pos - 1 )
local byte_end = utf8.offset ( Lines [ Cursor1.line ] . data , Cursor1.pos )
2022-05-18 06:36:10 +01:00
if byte_start then
if byte_end then
2022-05-22 00:24:07 +01:00
Lines [ Cursor1.line ] . data = string.sub ( Lines [ Cursor1.line ] . data , 1 , byte_start - 1 ) .. string.sub ( Lines [ Cursor1.line ] . data , byte_end )
2022-05-18 06:36:10 +01:00
else
2022-05-22 00:24:07 +01:00
Lines [ Cursor1.line ] . data = string.sub ( Lines [ Cursor1.line ] . data , 1 , byte_start - 1 )
2022-05-18 06:36:10 +01:00
end
2022-05-22 00:24:07 +01:00
Lines [ Cursor1.line ] . fragments = nil
Cursor1.pos = Cursor1.pos - 1
2022-05-18 06:36:10 +01:00
end
2022-05-22 00:24:07 +01:00
elseif Cursor1.line > 1 then
2022-06-05 06:13:34 +01:00
before = snapshot ( Cursor1.line - 1 , Cursor1.line )
2022-05-22 00:24:07 +01:00
if Lines [ Cursor1.line - 1 ] . mode == ' drawing ' then
table.remove ( Lines , Cursor1.line - 1 )
2022-05-18 06:36:10 +01:00
else
-- join lines
2022-05-22 00:24:07 +01:00
Cursor1.pos = utf8.len ( Lines [ Cursor1.line - 1 ] . data ) + 1
Lines [ Cursor1.line - 1 ] . data = Lines [ Cursor1.line - 1 ] . data .. Lines [ Cursor1.line ] . data
Lines [ Cursor1.line - 1 ] . fragments = nil
table.remove ( Lines , Cursor1.line )
2022-05-18 06:36:10 +01:00
end
2022-05-22 00:24:07 +01:00
Cursor1.line = Cursor1.line - 1
2022-05-18 06:36:10 +01:00
end
2022-05-29 07:11:53 +01:00
if Text.lt1 ( Cursor1 , Screen_top1 ) then
2022-05-28 17:41:02 +01:00
local top2 = Text.to2 ( Screen_top1 )
top2 = Text.previous_screen_line ( top2 )
Screen_top1 = Text.to1 ( top2 )
end
2022-05-29 07:11:53 +01:00
assert ( Text.le1 ( Screen_top1 , Cursor1 ) )
2022-05-18 06:36:10 +01:00
save_to_disk ( Lines , Filename )
2022-06-03 01:46:30 +01:00
record_undo_event ( { before = before , after = snapshot ( Cursor1.line ) } )
2022-05-18 06:36:10 +01:00
elseif chord == ' delete ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Text.delete_selection ( )
2022-06-02 23:45:25 +01:00
save_to_disk ( Lines , Filename )
2022-05-29 16:12:47 +01:00
return
end
2022-06-03 01:46:30 +01:00
local before
if Cursor1.pos <= utf8.len ( Lines [ Cursor1.line ] . data ) then
before = snapshot ( Cursor1.line )
else
before = snapshot ( Cursor1.line , Cursor1.line + 1 )
end
2022-05-22 00:24:07 +01:00
if Cursor1.pos <= utf8.len ( Lines [ Cursor1.line ] . data ) then
local byte_start = utf8.offset ( Lines [ Cursor1.line ] . data , Cursor1.pos )
local byte_end = utf8.offset ( Lines [ Cursor1.line ] . data , Cursor1.pos + 1 )
2022-05-18 06:36:10 +01:00
if byte_start then
if byte_end then
2022-05-22 00:24:07 +01:00
Lines [ Cursor1.line ] . data = string.sub ( Lines [ Cursor1.line ] . data , 1 , byte_start - 1 ) .. string.sub ( Lines [ Cursor1.line ] . data , byte_end )
2022-05-18 06:36:10 +01:00
else
2022-05-22 00:24:07 +01:00
Lines [ Cursor1.line ] . data = string.sub ( Lines [ Cursor1.line ] . data , 1 , byte_start - 1 )
2022-05-18 06:36:10 +01:00
end
2022-05-22 00:24:07 +01:00
Lines [ Cursor1.line ] . fragments = nil
-- no change to Cursor1.pos
2022-05-18 06:36:10 +01:00
end
2022-05-22 00:24:07 +01:00
elseif Cursor1.line < # Lines then
if Lines [ Cursor1.line + 1 ] . mode == ' drawing ' then
table.remove ( Lines , Cursor1.line + 1 )
2022-05-18 06:36:10 +01:00
else
-- join lines
2022-05-22 00:24:07 +01:00
Lines [ Cursor1.line ] . data = Lines [ Cursor1.line ] . data .. Lines [ Cursor1.line + 1 ] . data
Lines [ Cursor1.line ] . fragments = nil
table.remove ( Lines , Cursor1.line + 1 )
2022-05-18 06:36:10 +01:00
end
end
save_to_disk ( Lines , Filename )
2022-06-03 01:46:30 +01:00
record_undo_event ( { before = before , after = snapshot ( Cursor1.line ) } )
2022-05-29 06:27:47 +01:00
--== shortcuts that move the cursor
elseif chord == ' left ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
2022-05-29 06:27:47 +01:00
Text.left ( )
elseif chord == ' right ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
Text.right ( )
elseif chord == ' S-left ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
Text.left ( )
elseif chord == ' S-right ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
2022-05-29 06:27:47 +01:00
Text.right ( )
-- C- hotkeys reserved for drawings, so we'll use M-
elseif chord == ' M-left ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
2022-05-29 06:36:05 +01:00
Text.word_left ( )
2022-05-29 06:27:47 +01:00
elseif chord == ' M-right ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
Text.word_right ( )
elseif chord == ' M-S-left ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
Text.word_left ( )
elseif chord == ' M-S-right ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
2022-05-29 06:36:05 +01:00
Text.word_right ( )
2022-05-29 06:27:47 +01:00
elseif chord == ' home ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
2022-05-29 06:27:47 +01:00
Cursor1.pos = 1
elseif chord == ' end ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
Cursor1.pos = utf8.len ( Lines [ Cursor1.line ] . data ) + 1
elseif chord == ' S-home ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
Cursor1.pos = 1
elseif chord == ' S-end ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
2022-05-29 06:27:47 +01:00
Cursor1.pos = utf8.len ( Lines [ Cursor1.line ] . data ) + 1
2022-05-18 06:36:10 +01:00
elseif chord == ' up ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
2022-05-29 06:36:05 +01:00
Text.up ( )
2022-05-18 06:36:10 +01:00
elseif chord == ' down ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
Text.down ( )
elseif chord == ' S-up ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
Text.up ( )
elseif chord == ' S-down ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
2022-05-29 06:36:05 +01:00
Text.down ( )
2022-05-29 06:42:11 +01:00
elseif chord == ' pageup ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
2022-05-29 06:45:53 +01:00
Text.pageup ( )
2022-05-29 06:42:11 +01:00
elseif chord == ' pagedown ' then
2022-05-29 16:12:47 +01:00
if Selection1.line then
Selection1 = { }
end
Text.pagedown ( )
elseif chord == ' S-pageup ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
Text.pageup ( )
elseif chord == ' S-pagedown ' then
if Selection1.line == nil then
Selection1 = { line = Cursor1.line , pos = Cursor1.pos }
end
2022-05-29 06:45:53 +01:00
Text.pagedown ( )
2022-05-29 06:36:05 +01:00
end
end
2022-06-03 02:35:18 +01:00
function Text . insert_return ( )
local byte_offset = utf8.offset ( Lines [ Cursor1.line ] . data , Cursor1.pos )
table.insert ( Lines , Cursor1.line + 1 , { mode = ' text ' , data = string.sub ( Lines [ Cursor1.line ] . data , byte_offset ) } )
2022-06-03 07:36:07 +01:00
local scroll_down = ( Cursor_y + Line_height ) > App.screen . height
2022-06-03 02:35:18 +01:00
Lines [ Cursor1.line ] . data = string.sub ( Lines [ Cursor1.line ] . data , 1 , byte_offset - 1 )
Lines [ Cursor1.line ] . fragments = nil
Cursor1.line = Cursor1.line + 1
Cursor1.pos = 1
if scroll_down then
2022-06-05 16:11:50 +01:00
Text.snap_cursor_to_bottom_of_screen ( )
2022-06-03 02:35:18 +01:00
end
end
2022-05-29 06:45:53 +01:00
function Text . pageup ( )
-- duplicate some logic from love.draw
local top2 = Text.to2 ( Screen_top1 )
--? print(App.screen.height)
2022-06-03 07:36:07 +01:00
local y = App.screen . height - Line_height
2022-06-03 07:40:20 +01:00
while y >= Margin_top do
2022-05-29 06:45:53 +01:00
--? print(y, top2.line)
if Screen_top1.line == 1 and Screen_top1.pos == 1 then break end
if Lines [ Screen_top1.line ] . mode == ' text ' then
2022-06-03 07:36:07 +01:00
y = y - Line_height
2022-05-29 06:45:53 +01:00
elseif Lines [ Screen_top1.line ] . mode == ' drawing ' then
y = y - 20 - Drawing.pixels ( Lines [ Screen_top1.line ] . h )
end
top2 = Text.previous_screen_line ( top2 )
end
Screen_top1 = Text.to1 ( top2 )
Cursor1.line = Screen_top1.line
Cursor1.pos = Screen_top1.pos
Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary ( )
--? print(Cursor1.line, Cursor1.pos, Screen_top1.line, Screen_top1.pos)
end
function Text . pagedown ( )
2022-06-04 22:55:52 +01:00
-- If a line/paragraph gets to a page boundary, I often want to scroll
-- before I get to the bottom.
-- However, only do this if it makes forward progress.
local top2 = Text.to2 ( Screen_bottom1 )
if top2.screen_line > 1 then
top2.screen_line = math.max ( top2.screen_line - 10 , 1 )
end
local new_top1 = Text.to1 ( top2 )
if Text.lt1 ( Screen_top1 , new_top1 ) then
Screen_top1 = new_top1
else
Screen_top1.line = Screen_bottom1.line
Screen_top1.pos = Screen_bottom1.pos
end
2022-05-29 06:45:53 +01:00
--? print('setting top to', Screen_top1.line, Screen_top1.pos)
Cursor1.line = Screen_top1.line
Cursor1.pos = Screen_top1.pos
Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary ( )
--? print('top now', Screen_top1.line)
end
2022-05-29 06:36:05 +01:00
function Text . up ( )
assert ( Lines [ Cursor1.line ] . mode == ' text ' )
--? print('up', Cursor1.pos, Screen_top1.pos)
local screen_line_index , screen_line_starting_pos = Text.pos_at_start_of_cursor_screen_line ( )
if screen_line_starting_pos == 1 then
--? print('cursor is at first screen line of its line')
-- line is done; skip to previous text line
local new_cursor_line = Cursor1.line
while new_cursor_line > 1 do
new_cursor_line = new_cursor_line - 1
if Lines [ new_cursor_line ] . mode == ' text ' then
--? print('found previous text line')
Cursor1.line = new_cursor_line
Text.populate_screen_line_starting_pos ( Cursor1.line )
if Lines [ Cursor1.line ] . screen_line_starting_pos == nil then
2022-05-22 00:24:07 +01:00
Cursor1.pos = Text.nearest_cursor_pos ( Lines [ Cursor1.line ] . data , Cursor_x )
2022-05-21 04:33:30 +01:00
break
end
2022-05-29 06:36:05 +01:00
-- previous text line found, pick its final screen line
--? print('has multiple screen lines')
local screen_line_starting_pos = Lines [ Cursor1.line ] . screen_line_starting_pos
--? print(#screen_line_starting_pos)
screen_line_starting_pos = screen_line_starting_pos [ # screen_line_starting_pos ]
--? print('previous screen line starts at pos '..tostring(screen_line_starting_pos)..' of its line')
if Screen_top1.line > Cursor1.line then
Screen_top1.line = Cursor1.line
Screen_top1.pos = screen_line_starting_pos
--? print('pos of top of screen is also '..tostring(Screen_top1.pos)..' of the same line')
end
local s = string.sub ( Lines [ Cursor1.line ] . data , screen_line_starting_pos )
Cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos ( s , Cursor_x ) - 1
break
2022-05-18 06:36:10 +01:00
end
2022-05-29 06:36:05 +01:00
end
if Cursor1.line < Screen_top1.line then
Screen_top1.line = Cursor1.line
end
else
-- move up one screen line in current line
--? print('cursor is NOT at first screen line of its line')
assert ( screen_line_index > 1 )
new_screen_line_starting_pos = Lines [ Cursor1.line ] . screen_line_starting_pos [ screen_line_index - 1 ]
--? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
if Screen_top1.line == Cursor1.line and Screen_top1.pos == screen_line_starting_pos then
Screen_top1.pos = new_screen_line_starting_pos
--? print('also setting pos of top of screen to '..tostring(Screen_top1.pos))
end
local s = string.sub ( Lines [ Cursor1.line ] . data , new_screen_line_starting_pos )
Cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos ( s , Cursor_x ) - 1
--? print('cursor pos is now '..tostring(Cursor1.pos))
end
end
function Text . down ( )
assert ( Lines [ Cursor1.line ] . mode == ' text ' )
--? print('down', Cursor1.line, Cursor1.pos, Screen_top1.line, Screen_top1.pos, Screen_bottom1.line, Screen_bottom1.pos)
if Text.cursor_at_final_screen_line ( ) then
-- line is done, skip to next text line
--? print('cursor at final screen line of its line')
local new_cursor_line = Cursor1.line
while new_cursor_line < # Lines do
new_cursor_line = new_cursor_line + 1
if Lines [ new_cursor_line ] . mode == ' text ' then
Cursor1.line = new_cursor_line
Cursor1.pos = Text.nearest_cursor_pos ( Lines [ Cursor1.line ] . data , Cursor_x )
--? print(Cursor1.pos)
break
2022-05-24 23:01:09 +01:00
end
2022-05-19 01:22:04 +01:00
end
2022-05-29 06:36:05 +01:00
if Cursor1.line > Screen_bottom1.line then
--? print('screen top before:', Screen_top1.line, Screen_top1.pos)
--? print('scroll up preserving cursor')
2022-06-05 16:11:50 +01:00
Text.snap_cursor_to_bottom_of_screen ( )
2022-05-29 06:36:05 +01:00
--? print('screen top after:', Screen_top1.line, Screen_top1.pos)
end
else
-- move down one screen line in current line
2022-06-05 15:59:44 +01:00
local scroll_down = false
2022-05-29 07:11:53 +01:00
if Text.le1 ( Screen_bottom1 , Cursor1 ) then
2022-06-05 15:59:44 +01:00
scroll_down = true
2022-05-29 06:36:05 +01:00
end
--? print('cursor is NOT at final screen line of its line')
local screen_line_index , screen_line_starting_pos = Text.pos_at_start_of_cursor_screen_line ( )
new_screen_line_starting_pos = Lines [ Cursor1.line ] . screen_line_starting_pos [ screen_line_index + 1 ]
--? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
local s = string.sub ( Lines [ Cursor1.line ] . data , new_screen_line_starting_pos )
Cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos ( s , Cursor_x ) - 1
--? print('cursor pos is now', Cursor1.line, Cursor1.pos)
2022-06-05 15:59:44 +01:00
if scroll_down then
2022-05-29 06:36:05 +01:00
--? print('scroll up preserving cursor')
2022-06-05 16:11:50 +01:00
Text.snap_cursor_to_bottom_of_screen ( )
2022-05-29 06:36:05 +01:00
--? print('screen top after:', Screen_top1.line, Screen_top1.pos)
end
end
--? print('=>', Cursor1.line, Cursor1.pos, Screen_top1.line, Screen_top1.pos, Screen_bottom1.line, Screen_bottom1.pos)
end
function Text . word_left ( )
while true do
Text.left ( )
if Cursor1.pos == 1 then break end
assert ( Cursor1.pos > 1 )
local offset = utf8.offset ( Lines [ Cursor1.line ] . data , Cursor1.pos )
assert ( offset > 1 )
if Lines [ Cursor1.line ] . data : sub ( offset - 1 , offset - 1 ) == ' ' then
break
end
end
end
function Text . word_right ( )
while true do
Text.right ( )
if Cursor1.pos > utf8.len ( Lines [ Cursor1.line ] . data ) then break end
local offset = utf8.offset ( Lines [ Cursor1.line ] . data , Cursor1.pos )
2022-05-29 16:12:47 +01:00
if Lines [ Cursor1.line ] . data : sub ( offset , offset ) == ' ' then -- TODO: other space characters
2022-05-29 06:36:05 +01:00
break
end
2022-05-18 06:36:10 +01:00
end
end
2022-05-25 23:25:10 +01:00
function Text . left ( )
assert ( Lines [ Cursor1.line ] . mode == ' text ' )
if Cursor1.pos > 1 then
Cursor1.pos = Cursor1.pos - 1
else
local new_cursor_line = Cursor1.line
while new_cursor_line > 1 do
new_cursor_line = new_cursor_line - 1
if Lines [ new_cursor_line ] . mode == ' text ' then
Cursor1.line = new_cursor_line
Cursor1.pos = utf8.len ( Lines [ Cursor1.line ] . data ) + 1
break
end
end
if Cursor1.line < Screen_top1.line then
Screen_top1.line = Cursor1.line
end
end
end
function Text . right ( )
assert ( Lines [ Cursor1.line ] . mode == ' text ' )
if Cursor1.pos <= utf8.len ( Lines [ Cursor1.line ] . data ) then
Cursor1.pos = Cursor1.pos + 1
else
local new_cursor_line = Cursor1.line
while new_cursor_line <= # Lines - 1 do
new_cursor_line = new_cursor_line + 1
if Lines [ new_cursor_line ] . mode == ' text ' then
Cursor1.line = new_cursor_line
Cursor1.pos = 1
break
end
end
if Cursor1.line > Screen_bottom1.line then
Screen_top1.line = Cursor1.line
end
end
end
2022-05-21 00:41:21 +01:00
function Text . pos_at_start_of_cursor_screen_line ( )
2022-05-22 00:24:07 +01:00
if Lines [ Cursor1.line ] . screen_line_starting_pos == nil then
2022-05-21 00:41:21 +01:00
return 1 , 1
end
2022-05-22 00:24:07 +01:00
for i =# Lines [ Cursor1.line ] . screen_line_starting_pos , 1 , - 1 do
local spos = Lines [ Cursor1.line ] . screen_line_starting_pos [ i ]
if spos <= Cursor1.pos then
2022-05-21 00:41:21 +01:00
return i , spos
end
end
assert ( false )
end
2022-05-21 04:33:30 +01:00
function Text . cursor_at_final_screen_line ( )
2022-05-22 00:24:07 +01:00
if Lines [ Cursor1.line ] . screen_line_starting_pos == nil then
2022-05-21 04:33:30 +01:00
return true
end
2022-05-25 22:56:21 +01:00
local screen_lines = Lines [ Cursor1.line ] . screen_line_starting_pos
2022-05-23 23:38:42 +01:00
--? print(screen_lines[#screen_lines], Cursor1.pos)
2022-05-22 05:08:21 +01:00
return screen_lines [ # screen_lines ] <= Cursor1.pos
2022-05-21 04:33:30 +01:00
end
2022-05-19 02:19:27 +01:00
function Text . move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary ( )
2022-06-03 07:40:20 +01:00
local y = Margin_top
2022-05-22 00:24:07 +01:00
while Cursor1.line <= # Lines do
if Lines [ Cursor1.line ] . mode == ' text ' then
2022-05-19 02:19:27 +01:00
break
end
2022-05-23 23:38:42 +01:00
--? print('cursor skips', Cursor1.line)
2022-05-23 16:52:13 +01:00
y = y + 20 + Drawing.pixels ( Lines [ Cursor1.line ] . h )
2022-05-22 00:24:07 +01:00
Cursor1.line = Cursor1.line + 1
2022-05-19 02:19:27 +01:00
end
-- hack: insert a text line at bottom of file if necessary
2022-05-22 00:24:07 +01:00
if Cursor1.line > # Lines then
assert ( Cursor1.line == # Lines + 1 )
2022-05-19 02:19:27 +01:00
table.insert ( Lines , { mode = ' text ' , data = ' ' } )
end
2022-06-03 07:36:07 +01:00
--? print(y, App.screen.height, App.screen.height-Line_height)
if y > App.screen . height - Line_height then
2022-05-23 23:38:42 +01:00
--? print('scroll up')
2022-06-05 16:11:50 +01:00
Text.snap_cursor_to_bottom_of_screen ( )
2022-05-19 02:19:27 +01:00
end
end
2022-06-05 16:11:50 +01:00
function Text . snap_cursor_to_bottom_of_screen ( )
2022-05-22 07:02:04 +01:00
local top2 = Text.to2 ( Cursor1 )
2022-06-05 17:50:19 +01:00
top2.screen_pos = 1 -- start of screen line
2022-05-22 07:02:04 +01:00
--? print('cursor pos '..tostring(Cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
2022-06-03 07:36:07 +01:00
local y = App.screen . height - Line_height
2022-05-21 06:43:09 +01:00
-- duplicate some logic from love.draw
2022-05-19 02:19:27 +01:00
while true do
2022-05-22 07:02:04 +01:00
--? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
if top2.line == 1 and top2.screen_line == 1 then break end
if top2.screen_line > 1 or Lines [ top2.line - 1 ] . mode == ' text ' then
2022-06-03 07:36:07 +01:00
local h = Line_height
2022-06-03 07:40:20 +01:00
if y - h < Margin_top then
2022-05-22 07:02:04 +01:00
break
end
y = y - h
2022-05-21 06:43:09 +01:00
else
2022-05-22 07:02:04 +01:00
assert ( top2.line > 1 )
assert ( Lines [ top2.line - 1 ] . mode == ' drawing ' )
-- We currently can't draw partial drawings, so either skip it entirely
-- or not at all.
2022-05-22 17:15:56 +01:00
local h = 20 + Drawing.pixels ( Lines [ top2.line - 1 ] . h )
2022-06-03 07:40:20 +01:00
if y - h < Margin_top then
2022-05-22 07:02:04 +01:00
break
end
--? print('skipping drawing of height', h)
y = y - h
2022-05-19 02:19:27 +01:00
end
2022-05-22 07:02:04 +01:00
top2 = Text.previous_screen_line ( top2 )
2022-05-19 02:19:27 +01:00
end
2022-05-22 07:02:04 +01:00
--? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
Screen_top1 = Text.to1 ( top2 )
--? print('top1 finally:', Screen_top1.line, Screen_top1.pos)
2022-05-19 02:19:27 +01:00
end
2022-05-18 06:53:09 +01:00
function Text . in_line ( line , x , y )
2022-05-19 01:22:04 +01:00
if line.y == nil then return false end -- outside current page
2022-05-25 21:38:33 +01:00
if x < 25 then return false end
2022-05-20 06:07:12 +01:00
if y < line.y then return false end
2022-06-03 07:36:07 +01:00
if line.screen_line_starting_pos == nil then return y < line.y + Line_height end
return y < line.y + # line.screen_line_starting_pos * Line_height
2022-05-18 06:53:09 +01:00
end
2022-06-03 21:22:03 +01:00
-- convert mx,my in pixels to schema-1 coordinates
function Text . to_pos_on_line ( line , mx , my )
2022-05-20 06:07:12 +01:00
if line.screen_line_starting_pos == nil then
2022-06-03 21:22:03 +01:00
return Text.nearest_cursor_pos ( line.data , mx )
2022-05-20 06:07:12 +01:00
end
2022-06-07 02:57:46 +01:00
if line.fragments == nil then
Text.compute_fragments ( line , Line_width )
end
2022-05-20 06:07:12 +01:00
assert ( my >= line.y )
-- duplicate some logic from Text.draw
local y = line.y
2022-05-20 06:26:15 +01:00
for screen_line_index , screen_line_starting_pos in ipairs ( line.screen_line_starting_pos ) do
2022-06-03 07:36:07 +01:00
local nexty = y + Line_height
2022-05-20 06:07:12 +01:00
if my < nexty then
2022-05-20 06:26:15 +01:00
-- On all wrapped screen lines but the final one, clicks past end of
-- line position cursor on final character of screen line.
-- (The final screen line positions past end of screen line as always.)
if mx > Line_width and screen_line_index < # line.screen_line_starting_pos then
2022-06-03 21:22:03 +01:00
return line.screen_line_starting_pos [ screen_line_index + 1 ]
2022-05-20 06:26:15 +01:00
end
2022-05-20 06:07:12 +01:00
local s = string.sub ( line.data , screen_line_starting_pos )
2022-06-03 21:22:03 +01:00
return screen_line_starting_pos + Text.nearest_cursor_pos ( s , mx ) - 1
2022-05-20 06:07:12 +01:00
end
y = nexty
end
assert ( false )
2022-05-18 06:53:09 +01:00
end
2022-05-20 06:07:12 +01:00
-- manual test:
-- line: abc
-- def
-- gh
-- fragments: abc, def, gh
-- click inside e
-- line_starting_pos = 1 + 3 = 4
-- nearest_cursor_pos('defgh', mx) = 2
2022-05-22 00:24:07 +01:00
-- Cursor1.pos = 4 + 2 - 1 = 5
2022-05-20 06:07:12 +01:00
-- manual test:
-- click inside h
-- line_starting_pos = 1 + 3 + 3 = 7
-- nearest_cursor_pos('gh', mx) = 2
2022-05-22 00:24:07 +01:00
-- Cursor1.pos = 7 + 2 - 1 = 8
2022-05-18 06:53:09 +01:00
2022-05-23 23:22:16 +01:00
function Text . nearest_cursor_pos ( line , x ) -- x includes left margin
2022-05-18 04:29:08 +01:00
if x == 0 then
return 1
end
2022-05-20 01:46:02 +01:00
local len = utf8.len ( line )
2022-05-25 23:20:18 +01:00
local max_x = 25 + Text.x ( line , len + 1 )
2022-05-18 04:29:08 +01:00
if x > max_x then
2022-05-20 01:46:02 +01:00
return len + 1
2022-05-18 04:29:08 +01:00
end
2022-05-20 01:46:02 +01:00
local left , right = 1 , len + 1
2022-05-24 22:34:50 +01:00
--? print('-- nearest', x)
2022-05-20 05:52:38 +01:00
while true do
2022-05-24 22:34:50 +01:00
--? print('nearest', x, '^'..line..'$', left, right)
if left == right then
return left
end
2022-05-18 04:29:08 +01:00
local curr = math.floor ( ( left + right ) / 2 )
2022-05-25 23:20:18 +01:00
local currxmin = 25 + Text.x ( line , curr )
local currxmax = 25 + Text.x ( line , curr + 1 )
2022-05-24 22:34:50 +01:00
--? print('nearest', x, left, right, curr, currxmin, currxmax)
2022-05-18 04:29:08 +01:00
if currxmin <= x and x < currxmax then
2022-05-25 23:08:37 +01:00
if x - currxmin < currxmax - x then
return curr
else
return curr + 1
end
2022-05-18 04:29:08 +01:00
end
2022-05-20 05:52:38 +01:00
if left >= right - 1 then
return right
end
2022-05-18 04:29:08 +01:00
if currxmin > x then
right = curr
else
left = curr
end
end
2022-05-20 05:52:38 +01:00
assert ( false )
2022-05-18 04:29:08 +01:00
end
2022-05-23 23:22:16 +01:00
function Text . nearest_pos_less_than ( line , x ) -- x DOES NOT include left margin
if x == 0 then
return 1
end
local len = utf8.len ( line )
2022-05-25 23:20:18 +01:00
local max_x = Text.x ( line , len + 1 )
2022-05-23 23:22:16 +01:00
if x > max_x then
return len + 1
end
local left , right = 1 , len + 1
--? print('--')
while true do
local curr = math.floor ( ( left + right ) / 2 )
2022-05-25 23:20:18 +01:00
local currxmin = Text.x ( line , curr + 1 )
local currxmax = Text.x ( line , curr + 2 )
2022-05-23 23:22:16 +01:00
--? print(x, left, right, curr, currxmin, currxmax)
if currxmin <= x and x < currxmax then
return curr
end
if left >= right - 1 then
return left
end
if currxmin > x then
right = curr
else
left = curr
end
end
assert ( false )
end
2022-05-25 23:20:18 +01:00
function Text . x ( s , pos )
local offset = utf8.offset ( s , pos )
assert ( offset )
local s_before = s : sub ( 1 , offset - 1 )
local text_before = App.newText ( love.graphics . getFont ( ) , s_before )
2022-06-03 07:36:07 +01:00
return App.width ( text_before )
2022-05-20 01:29:14 +01:00
end
2022-05-22 00:24:07 +01:00
function Text . to2 ( pos1 )
2022-05-30 23:40:36 +01:00
if Lines [ pos1.line ] . mode == ' drawing ' then
return { line = pos1.line , screen_line = 1 , screen_pos = 1 }
end
2022-05-22 00:24:07 +01:00
local result = { line = pos1.line , screen_line = 1 }
2022-05-22 06:08:13 +01:00
if Lines [ pos1.line ] . screen_line_starting_pos == nil then
2022-05-22 00:24:07 +01:00
result.screen_pos = pos1.pos
else
for i =# Lines [ pos1.line ] . screen_line_starting_pos , 1 , - 1 do
local spos = Lines [ pos1.line ] . screen_line_starting_pos [ i ]
2022-05-22 07:02:04 +01:00
if spos <= pos1.pos then
2022-05-22 00:24:07 +01:00
result.screen_line = i
2022-05-22 07:02:04 +01:00
result.screen_pos = pos1.pos - spos + 1
2022-05-22 00:24:07 +01:00
break
end
end
end
assert ( result.screen_pos )
return result
end
function Text . to1 ( pos2 )
local result = { line = pos2.line , pos = pos2.screen_pos }
if pos2.screen_line > 1 then
2022-05-22 07:02:04 +01:00
result.pos = Lines [ pos2.line ] . screen_line_starting_pos [ pos2.screen_line ] + pos2.screen_pos - 1
2022-05-22 00:24:07 +01:00
end
return result
end
2022-06-03 16:11:18 +01:00
function Text . eq1 ( a , b )
return a.line == b.line and a.pos == b.pos
end
2022-05-29 07:11:53 +01:00
function Text . lt1 ( a , b )
if a.line < b.line then
return true
end
if a.line > b.line then
return false
end
return a.pos < b.pos
end
function Text . le1 ( a , b )
if a.line < b.line then
return true
end
if a.line > b.line then
return false
end
return a.pos <= b.pos
end
2022-05-22 07:02:04 +01:00
function Text . previous_screen_line ( pos2 )
if pos2.screen_line > 1 then
return { line = pos2.line , screen_line = pos2.screen_line - 1 , screen_pos = 1 }
elseif pos2.line == 1 then
return pos2
elseif Lines [ pos2.line - 1 ] . mode == ' drawing ' then
return { line = pos2.line - 1 , screen_line = 1 , screen_pos = 1 }
else
local l = Lines [ pos2.line - 1 ]
if l.screen_line_starting_pos == nil then
return { line = pos2.line - 1 , screen_line = 1 , screen_pos = 1 }
else
return { line = pos2.line - 1 , screen_line =# Lines [ pos2.line - 1 ] . screen_line_starting_pos , screen_pos = 1 }
end
end
end
2022-05-24 01:39:00 +01:00
function Text . populate_screen_line_starting_pos ( line_index )
2022-05-25 21:35:32 +01:00
--? print('Text.populate_screen_line_starting_pos')
2022-05-24 01:39:00 +01:00
local line = Lines [ line_index ]
2022-05-25 22:56:21 +01:00
if line.screen_line_starting_pos then
return
end
-- duplicate some logic from Text.draw
2022-05-24 01:39:00 +01:00
if line.fragments == nil then
Text.compute_fragments ( line , Line_width )
end
local x = 25
local pos = 1
for _ , f in ipairs ( line.fragments ) do
local frag , frag_text = f.data , f.text
--? print(x, frag)
-- render fragment
2022-06-03 07:36:07 +01:00
local frag_width = App.width ( frag_text )
2022-05-24 01:39:00 +01:00
if x + frag_width > Line_width then
x = 25
if line.screen_line_starting_pos == nil then
line.screen_line_starting_pos = { 1 , pos }
else
2022-05-25 22:56:21 +01:00
--? print(' ', #line.screen_line_starting_pos, line.data)
2022-05-24 01:39:00 +01:00
table.insert ( line.screen_line_starting_pos , pos )
end
end
x = x + frag_width
local frag_len = utf8.len ( frag )
pos = pos + frag_len
end
end
2022-06-03 07:36:07 +01:00
function Text . redraw_all ( )
for _ , line in ipairs ( Lines ) do
2022-06-05 04:52:36 +01:00
line.y = nil
2022-06-03 07:36:07 +01:00
line.fragments = nil
line.screen_line_starting_pos = nil
end
end
2022-05-18 04:29:08 +01:00
return Text