2024-07-20 20:16:14 +01:00
|
|
|
---- moving around the editor in terms of locations (loc)
|
|
|
|
--
|
|
|
|
-- locations within text lines look like this:
|
|
|
|
-- {mode='text', line=, pos=}
|
|
|
|
-- (pos counts in utf8 codepoints starting from 1)
|
|
|
|
--
|
|
|
|
-- locations within drawings look like this:
|
2024-07-21 17:14:01 +01:00
|
|
|
-- {mode='drawing', line=}
|
|
|
|
-- (drawing lines are atomic as far as the cursor is concerned)
|
2024-07-20 20:16:14 +01:00
|
|
|
--
|
|
|
|
-- all movements are built primarily using the following primitives, defined
|
|
|
|
-- further down:
|
|
|
|
-- - to_loc: (x, y) -> loc
|
|
|
|
-- identify the location at pixel coordinates (x,y) on screen
|
|
|
|
-- returns nil if (x,y) is not on screen
|
|
|
|
-- - to_coord: loc -> x, y
|
|
|
|
-- identify the top-left coordinate on screen of location loc
|
|
|
|
-- returns nil if loc is not on screen
|
|
|
|
-- - down: loc, dy -> loc
|
|
|
|
-- find the location at the start of a screen line dy pixels down from loc
|
|
|
|
-- returns nil if dy is taller than the screen
|
2024-07-21 06:46:04 +01:00
|
|
|
-- returns bottom of file if we hit it
|
2024-07-20 20:16:14 +01:00
|
|
|
-- - up: loc, dy -> loc
|
|
|
|
-- find the location at the start of a screen line dy pixels up from loc
|
|
|
|
-- returns nil if dy is taller than the screen
|
2024-07-21 06:46:04 +01:00
|
|
|
-- returns top of file if we hit it
|
2024-07-20 20:16:14 +01:00
|
|
|
-- - hor: loc, x -> loc
|
|
|
|
-- find the location at x=x0 on the same screen line as loc
|
|
|
|
--
|
|
|
|
-- I have tried to make these definitions as clear as possible while being fast enough.
|
|
|
|
-- My mental model for trading off performance for clarity:
|
|
|
|
-- - text is the bottleneck; my line drawings are clear and cheap no matter
|
|
|
|
-- how you go about things.
|
|
|
|
-- - any computation limited to the number of characters a screen can show
|
|
|
|
-- will be an order of magnitude faster than it needs to be to draw 30
|
|
|
|
-- frames per second.
|
|
|
|
-- So I don't mind doing up to 5 scans per interactive operation if that
|
|
|
|
-- makes the code clearer.
|
|
|
|
-- - no caching across frames; it makes the code less clear and has also
|
|
|
|
-- caused bugs.
|
|
|
|
-- - short-lived memory allocations that live within a single frame are cheap
|
|
|
|
--
|
|
|
|
-- This API was non-trivial to arrive at. It seems promising for any
|
|
|
|
-- pixel-based editor using proportional fonts. It seems independent of the
|
|
|
|
-- data structures the editor uses, whether arrays as here or ropes or gap
|
|
|
|
-- buffers, though I cheat a bit and use knowledge of the data structures
|
|
|
|
-- where it saves me a scan or two.
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.up(editor)
|
|
|
|
local x, _ = edit.to_coord(editor, editor.cursor) -- scan
|
|
|
|
editor.cursor = edit.up(editor, editor.cursor, 1 --[[px]]) -- scan
|
|
|
|
assert(editor.cursor)
|
|
|
|
editor.cursor = edit.hor(editor, editor.cursor, x) -- 0-1 scan
|
|
|
|
assert(editor.cursor)
|
|
|
|
maybe_snap_cursor_to_top_of_screen(editor) -- 0-1 scan
|
2024-07-20 20:16:14 +01:00
|
|
|
end -- 2-4 scans
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.down(editor)
|
|
|
|
local x, _ = edit.to_coord(editor, editor.cursor) -- scan
|
|
|
|
local dy0 = edit.screen_line_height(editor, editor.cursor)
|
|
|
|
editor.cursor = edit.down(editor, editor.cursor, dy0) -- scan
|
|
|
|
assert(editor.cursor)
|
|
|
|
editor.cursor = edit.hor(editor, editor.cursor, x) -- 0-1 scan
|
|
|
|
assert(editor.cursor)
|
|
|
|
maybe_snap_cursor_to_bottom_of_screen(editor) -- 0-2 scans
|
2024-07-20 20:16:14 +01:00
|
|
|
end -- 2-5 scans
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.left(editor)
|
|
|
|
Text.left_without_scroll(editor) -- 0-2 scans
|
|
|
|
maybe_snap_cursor_to_top_of_screen(editor) -- 0-1 scan
|
2024-08-30 08:48:33 +01:00
|
|
|
end -- 0-3 scans
|
2024-07-20 20:16:14 +01:00
|
|
|
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.left_without_scroll(editor)
|
|
|
|
if editor.cursor.pos and editor.cursor.pos > 1 then
|
|
|
|
editor.cursor.pos = editor.cursor.pos-1
|
|
|
|
elseif editor.cursor.line > 1 then
|
|
|
|
editor.cursor = edit.up(editor, editor.cursor, 1 --[[px]]) -- scan
|
|
|
|
assert(editor.cursor)
|
|
|
|
editor.cursor = edit.hor(editor, editor.cursor, editor.right) -- 0-1 scan
|
|
|
|
assert(editor.cursor)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end -- 0-2 scans
|
|
|
|
|
|
|
|
-- without cheating
|
2024-09-01 02:48:40 +01:00
|
|
|
--? function Text.left_without_scroll(editor)
|
|
|
|
--? local x, _ = edit.to_coord(editor, editor.cursor) -- scan
|
|
|
|
--? if x <= editor.left then -- 0-2 scans
|
|
|
|
--? editor.cursor = edit.up(editor, editor.cursor, 1 --[[px]]) -- scan
|
|
|
|
--? editor.cursor = edit.hor(editor, editor.cursor, editor.right) -- 0-1 scan
|
2024-07-20 20:16:14 +01:00
|
|
|
--? else
|
2024-09-01 02:48:40 +01:00
|
|
|
--? editor.cursor = edit.hor(editor, editor.cursor, x-1) -- 0-1 scan
|
2024-07-20 20:16:14 +01:00
|
|
|
--? end
|
|
|
|
--? end -- 1-3 scans
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.right(editor)
|
2024-09-09 00:37:17 +01:00
|
|
|
Text.right_without_scroll(editor) -- 0-2 scans
|
2024-09-01 02:48:40 +01:00
|
|
|
maybe_snap_cursor_to_bottom_of_screen(editor) -- 0-2 scans
|
2024-09-09 00:37:17 +01:00
|
|
|
end -- 0-4 scans
|
2024-07-20 20:16:14 +01:00
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.right_without_scroll(editor)
|
|
|
|
if editor.cursor.pos and editor.cursor.pos <= utf8.len(editor.lines[editor.cursor.line].data) then
|
|
|
|
editor.cursor.pos = editor.cursor.pos+1
|
2024-07-20 20:16:14 +01:00
|
|
|
else
|
2024-09-01 02:48:40 +01:00
|
|
|
local _, y = edit.to_coord(editor, editor.cursor) -- scan
|
|
|
|
local dy = edit.screen_line_height(editor, editor.cursor)
|
|
|
|
local new_cursor = edit.down(editor, editor.cursor, dy) -- scan
|
2024-09-09 00:37:17 +01:00
|
|
|
if edit.lt(editor.cursor, new_cursor) then -- there's further down to go
|
2024-09-01 02:48:40 +01:00
|
|
|
editor.cursor = new_cursor
|
2024-07-22 05:16:22 +01:00
|
|
|
end
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-22 05:16:22 +01:00
|
|
|
end -- 0-3 scans
|
2024-07-20 20:16:14 +01:00
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.pageup(editor)
|
|
|
|
editor.screen_top = edit.up(editor, editor.screen_top, editor.bottom - editor.top - editor.line_height) -- scan
|
|
|
|
assert(editor.screen_top)
|
|
|
|
editor.cursor = deepcopy(editor.screen_top)
|
|
|
|
assert(editor.cursor)
|
2024-07-20 20:16:14 +01:00
|
|
|
end -- 1 scan
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.pagedown(editor)
|
|
|
|
editor.screen_top = edit.down(editor, editor.screen_top, editor.bottom - editor.top - editor.line_height) -- scan
|
|
|
|
assert(editor.screen_top)
|
|
|
|
editor.cursor = deepcopy(editor.screen_top)
|
|
|
|
assert(editor.cursor)
|
2024-07-20 20:16:14 +01:00
|
|
|
end -- 1 scan
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.start_of_line(editor)
|
|
|
|
if editor.cursor.mode == 'drawing' then return end
|
|
|
|
editor.cursor.pos = 1
|
|
|
|
maybe_snap_cursor_to_top_of_screen(editor) -- 0-1 scan
|
2024-07-20 20:16:14 +01:00
|
|
|
end -- 0-1 scan
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.end_of_line(editor)
|
|
|
|
if editor.cursor.mode == 'drawing' then return end
|
|
|
|
editor.cursor.pos = utf8.len(editor.lines[editor.cursor.line].data) + 1
|
|
|
|
maybe_snap_cursor_to_bottom_of_screen(editor) -- 0-1 scan
|
2024-07-20 20:16:14 +01:00
|
|
|
end -- 0-1 scan
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.word_left(editor)
|
2024-07-20 20:16:14 +01:00
|
|
|
-- skip some whitespace
|
|
|
|
while true do
|
2024-09-01 02:48:40 +01:00
|
|
|
if editor.cursor.pos == nil or editor.cursor.pos == 1 then
|
2024-07-20 20:16:14 +01:00
|
|
|
break -- line boundary is always whitespace
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
if Text.match(editor.lines[editor.cursor.line].data, editor.cursor.pos-1, '%S') then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
Text.left_without_scroll(editor)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
-- skip some non-whitespace
|
|
|
|
while true do
|
2024-09-01 02:48:40 +01:00
|
|
|
Text.left_without_scroll(editor)
|
|
|
|
if editor.cursor.pos == nil or editor.cursor.pos == 1 then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
assert(editor.cursor.pos > 1, 'bumped up against start of line')
|
|
|
|
if Text.match(editor.lines[editor.cursor.line].data, editor.cursor.pos-1, '%s') then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
maybe_snap_cursor_to_top_of_screen(editor)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.word_right(editor)
|
2024-07-20 20:16:14 +01:00
|
|
|
-- skip some whitespace
|
|
|
|
while true do
|
2024-09-01 02:48:40 +01:00
|
|
|
if editor.cursor.pos == nil or editor.cursor.pos > utf8.len(editor.lines[editor.cursor.line].data) then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
if Text.match(editor.lines[editor.cursor.line].data, editor.cursor.pos, '%S') then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
Text.right_without_scroll(editor)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
while true do
|
2024-09-01 02:48:40 +01:00
|
|
|
Text.right_without_scroll(editor)
|
|
|
|
if editor.cursor.pos == nil or editor.cursor.pos > utf8.len(editor.lines[editor.cursor.line].data) then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
if Text.match(editor.lines[editor.cursor.line].data, editor.cursor.pos, '%s') then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
maybe_snap_cursor_to_bottom_of_screen(editor)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function Text.match(s, pos, pat)
|
|
|
|
local start_offset = Text.offset(s, pos)
|
|
|
|
local end_offset = Text.offset(s, pos+1)
|
|
|
|
assert(end_offset > start_offset, ('end_offset %d not > start_offset %d'):format(end_offset, start_offset))
|
|
|
|
local curr = s:sub(start_offset, end_offset-1)
|
|
|
|
return curr:match(pat)
|
|
|
|
end
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function maybe_snap_cursor_to_top_of_screen(editor)
|
|
|
|
if edit.lt(editor.cursor, editor.screen_top) then
|
|
|
|
editor.screen_top = edit.hor(editor, editor.cursor, editor.left) -- scan
|
|
|
|
assert(editor.screen_top)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-21 06:46:04 +01:00
|
|
|
end -- 0-1 scan
|
2024-07-20 20:16:14 +01:00
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function maybe_snap_cursor_to_bottom_of_screen(editor)
|
|
|
|
local _, y = edit.to_coord(editor, editor.cursor) -- scan
|
|
|
|
local h = edit.screen_line_height(editor, editor.cursor)
|
|
|
|
if y == nil or y > editor.bottom - editor.top - h then
|
|
|
|
editor.screen_top = edit._up_whole_screen_lines(editor, editor.cursor, editor.bottom - editor.top - h) -- scan
|
|
|
|
assert(editor.screen_top)
|
2024-07-20 20:16:14 +01:00
|
|
|
else
|
|
|
|
-- no need to scroll
|
|
|
|
return
|
|
|
|
end
|
2024-07-21 06:46:04 +01:00
|
|
|
end -- 1-2 scans
|
2024-07-20 20:16:14 +01:00
|
|
|
|
|
|
|
---- various primitives that simulate drawing on screen without actually doing so
|
|
|
|
--
|
|
|
|
-- These _do_ depend on the data structures the editor uses. But you should go
|
|
|
|
-- a long way just by reimplementing them if you ever change the data structures.
|
|
|
|
|
2024-07-27 05:43:01 +01:00
|
|
|
-- These are based on the output of edit.get_rect, which provides:
|
|
|
|
-- - an array of rects {x,y, dx,dy}, one for each line
|
|
|
|
-- - if it's a text line:
|
|
|
|
-- - rects for each screen line in rect[].screen_line_rects, and
|
|
|
|
-- - rects for each utf8 codepoint in rect[].screen_line_rects[].char_rects
|
2024-07-22 01:58:46 +01:00
|
|
|
|
2024-09-19 01:45:39 +01:00
|
|
|
-- return the location corresponding to a pixel
|
|
|
|
-- may return nil if:
|
|
|
|
-- the coord is above the top margin and the top line is not text
|
|
|
|
-- or the coord is below the bottom margin and the bottom line is not text
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit.to_loc(editor, mx,my)
|
|
|
|
if my < editor.top then
|
|
|
|
return deepcopy(editor.screen_top)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
local x, y = mx - editor.left, my - editor.top
|
2024-07-22 01:58:46 +01:00
|
|
|
local rect
|
2024-09-01 02:48:40 +01:00
|
|
|
for line_index,line in array.each(editor.lines, editor.screen_top.line) do
|
|
|
|
if line_index == editor.screen_top.line then
|
|
|
|
rect = edit.get_rect(editor, editor.screen_top)
|
2024-07-27 05:43:01 +01:00
|
|
|
else
|
2024-09-01 02:48:40 +01:00
|
|
|
rect = edit.get_rect(editor, {mode=line.mode, line=line_index, pos=1})
|
2024-07-27 05:43:01 +01:00
|
|
|
end
|
2024-07-22 07:08:54 +01:00
|
|
|
if line.mode == 'text' then
|
2024-07-27 05:43:01 +01:00
|
|
|
if y < rect.dy then
|
|
|
|
local s = find_y(rect.screen_line_rects, y)
|
|
|
|
local char_rect = find_x(s.char_rects, x)
|
|
|
|
assert(char_rect)
|
|
|
|
return {mode='text', line=line_index, pos=char_rect.pos}
|
2024-07-22 01:58:46 +01:00
|
|
|
else
|
2024-07-27 05:43:01 +01:00
|
|
|
y = y - rect.dy
|
2024-07-29 04:13:05 +01:00
|
|
|
if y <= 0 then
|
2024-07-27 05:43:01 +01:00
|
|
|
break
|
2024-07-22 07:08:54 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if y < rect.dy then
|
2024-07-21 17:14:01 +01:00
|
|
|
return {mode='drawing', line=line_index}
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-22 07:08:54 +01:00
|
|
|
y = y - rect.dy
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
if y <= 0 then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
-- below all lines; return final rect on screen
|
2024-07-22 07:18:20 +01:00
|
|
|
if rect.screen_line_rects then
|
|
|
|
local s = rect.screen_line_rects
|
|
|
|
local c = s[#s].char_rects
|
|
|
|
return {mode='text', line=rect.line_index, pos=c[#c].pos}
|
2024-07-22 01:58:46 +01:00
|
|
|
else
|
|
|
|
return {mode='drawing', line=rect.line_index}
|
|
|
|
end
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit.to_coord(editor, loc) -- scans
|
|
|
|
if edit.lt(loc, editor.screen_top) then return end
|
|
|
|
local y = editor.top
|
|
|
|
for line_index, line in array.each(editor.lines, editor.screen_top.line) do
|
2024-07-27 05:43:01 +01:00
|
|
|
local rect
|
2024-09-01 02:48:40 +01:00
|
|
|
if line_index == editor.screen_top.line then
|
|
|
|
rect = edit.get_rect(editor, editor.screen_top)
|
2024-07-27 05:43:01 +01:00
|
|
|
else
|
2024-09-01 02:48:40 +01:00
|
|
|
rect = edit.get_rect(editor, {mode=line.mode, line=line_index, pos=1})
|
2024-07-27 05:43:01 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
if line_index == loc.line then
|
|
|
|
if rect.screen_line_rects then
|
|
|
|
-- text
|
|
|
|
for _,s in ipairs(rect.screen_line_rects) do
|
2024-07-27 05:43:01 +01:00
|
|
|
for _,c in ipairs(s.char_rects) do
|
2024-09-16 09:06:52 +01:00
|
|
|
if c.pos == loc.pos and (c.data or c.pos == utf8.len(line.data)+1) then
|
2024-09-01 02:48:40 +01:00
|
|
|
return editor.left + c.x, y + c.y
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
assert(false, 'edit.to_coord: invalid pos in text loc')
|
|
|
|
else
|
|
|
|
-- drawing
|
2024-09-01 02:48:40 +01:00
|
|
|
return editor.left, y + rect.y + Drawing_padding_top
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end
|
2024-07-27 05:43:01 +01:00
|
|
|
y = y + rect.dy
|
2024-09-01 02:48:40 +01:00
|
|
|
if y > editor.bottom then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end -- 1 scan
|
|
|
|
|
|
|
|
-- find the location at the start of a screen line dy pixels down from loc
|
2024-07-21 19:34:54 +01:00
|
|
|
-- return nil if dy is more than screen height away
|
|
|
|
-- return bottommost screen line in file if we hit it
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit.down(editor, loc, dy) -- scans
|
2024-07-20 20:16:14 +01:00
|
|
|
local y = 0
|
2024-07-21 19:34:54 +01:00
|
|
|
local prevloc = loc
|
2024-09-01 02:48:40 +01:00
|
|
|
for line_index, line in array.each(editor.lines, loc.line) do
|
|
|
|
local rect = edit.get_rect(editor, {mode=line.mode, line=line_index, pos=1})
|
2024-07-22 01:58:46 +01:00
|
|
|
if rect.screen_line_rects then
|
|
|
|
-- text
|
|
|
|
for _,s in ipairs(rect.screen_line_rects) do
|
2024-07-22 06:50:10 +01:00
|
|
|
if line_index > loc.line or loc.pos < s.pos+s.dpos then
|
|
|
|
local currloc = {mode='text', line=line_index, pos=s.pos}
|
|
|
|
if y + s.dy > dy then
|
|
|
|
return currloc
|
|
|
|
end
|
|
|
|
y = y + s.dy
|
|
|
|
prevloc = currloc
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
else
|
|
|
|
-- drawing
|
|
|
|
currloc = {mode='drawing', line=line_index}
|
|
|
|
if y + rect.dy > dy then
|
2024-07-21 19:34:54 +01:00
|
|
|
return currloc
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
y = y + rect.dy
|
2024-07-21 19:34:54 +01:00
|
|
|
prevloc = currloc
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end
|
2024-07-21 19:34:54 +01:00
|
|
|
return prevloc
|
2024-07-20 20:16:14 +01:00
|
|
|
end -- 1 scan
|
|
|
|
|
|
|
|
-- find the location at the start of a screen line dy pixels up from loc
|
2024-07-21 19:34:54 +01:00
|
|
|
-- return nil if dy is more than screen height away
|
|
|
|
-- return topmost screen line in file if we hit it
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit.up(editor, loc, dy) -- scans
|
2024-07-20 20:16:14 +01:00
|
|
|
local y = 0
|
2024-07-22 01:58:46 +01:00
|
|
|
-- special handling for loc's line
|
2024-09-01 02:48:40 +01:00
|
|
|
local rect = edit.get_rect(editor, {mode=loc.mode, line=loc.line, pos=1})
|
2024-07-22 01:58:46 +01:00
|
|
|
if rect.screen_line_rects then
|
|
|
|
-- text
|
|
|
|
local found = false
|
|
|
|
for is = #rect.screen_line_rects,1,-1 do
|
|
|
|
local s = rect.screen_line_rects[is]
|
|
|
|
if not found and within(loc.pos, s.pos, s.pos+s.dpos) then
|
|
|
|
found = true
|
|
|
|
elseif found then
|
|
|
|
if y + s.dy > dy then
|
2024-07-22 06:34:54 +01:00
|
|
|
return {mode='text', line=loc.line, pos=s.pos}
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
y = y + s.dy
|
|
|
|
else
|
|
|
|
-- below loc's screen line; skip
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
end
|
|
|
|
else
|
|
|
|
-- drawing; skip
|
|
|
|
end
|
|
|
|
for line_index = loc.line-1,1,-1 do
|
2024-09-01 02:48:40 +01:00
|
|
|
local line = editor.lines[line_index]
|
|
|
|
local rect = edit.get_rect(editor, {mode=line.mode, line=line_index, pos=1})
|
2024-07-22 01:58:46 +01:00
|
|
|
if rect.screen_line_rects then
|
|
|
|
-- text
|
|
|
|
for is = #rect.screen_line_rects,1,-1 do
|
|
|
|
local s = rect.screen_line_rects[is]
|
|
|
|
if y + s.dy > dy then
|
|
|
|
return {mode='text', line=line_index, pos=s.pos}
|
|
|
|
end
|
|
|
|
y = y + s.dy
|
2024-07-21 19:53:29 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
else
|
|
|
|
-- drawing
|
|
|
|
if y + rect.dy > dy then
|
2024-07-21 17:14:01 +01:00
|
|
|
return {mode='drawing', line=line_index}
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
y = y + rect.dy
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
return edit.top(editor)
|
2024-07-20 20:16:14 +01:00
|
|
|
end -- 1 scan
|
|
|
|
|
2024-07-21 19:34:54 +01:00
|
|
|
-- find the location at the start of a screen line up to dy pixels up from loc, but count only whole screen lines.
|
|
|
|
-- So the result will be at or below dy pixels above loc.
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit._up_whole_screen_lines(editor, loc, dy) -- scans
|
2024-07-22 01:58:46 +01:00
|
|
|
local prevloc = loc -- bug: not at start of screen line
|
2024-07-21 19:34:54 +01:00
|
|
|
local y = 0
|
2024-07-22 01:58:46 +01:00
|
|
|
-- special handling for loc's line
|
2024-09-01 02:48:40 +01:00
|
|
|
local rect = edit.get_rect(editor, {mode=loc.mode, line=loc.line, pos=1})
|
2024-07-22 01:58:46 +01:00
|
|
|
if rect.screen_line_rects then
|
|
|
|
-- text
|
|
|
|
local found = false
|
|
|
|
for is = #rect.screen_line_rects,1,-1 do
|
|
|
|
local s = rect.screen_line_rects[is]
|
|
|
|
if not found and within(loc.pos, s.pos, s.pos+s.dpos) then
|
|
|
|
found = true
|
|
|
|
elseif found then
|
|
|
|
local currloc = {mode='text', line=loc.line, pos=s.pos}
|
|
|
|
if y + s.dy > dy then
|
2024-07-21 19:34:54 +01:00
|
|
|
return prevloc
|
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
y = y + s.dy
|
2024-07-21 19:34:54 +01:00
|
|
|
prevloc = currloc
|
2024-07-22 01:58:46 +01:00
|
|
|
else
|
|
|
|
-- below loc's screen line; skip
|
2024-07-21 19:34:54 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
end
|
|
|
|
else
|
|
|
|
-- drawing; skip
|
|
|
|
end
|
|
|
|
for line_index = loc.line-1,1,-1 do
|
2024-09-01 02:48:40 +01:00
|
|
|
local line = editor.lines[line_index]
|
|
|
|
local rect = edit.get_rect(editor, {mode=line.mode, line=line_index, pos=1})
|
2024-07-22 01:58:46 +01:00
|
|
|
if rect.screen_line_rects then
|
|
|
|
-- text
|
|
|
|
for is = #rect.screen_line_rects,1,-1 do
|
|
|
|
local s = rect.screen_line_rects[is]
|
|
|
|
local currloc = {mode='text', line=line_index, pos=s.pos}
|
|
|
|
if y + s.dy > dy then
|
|
|
|
return prevloc
|
|
|
|
end
|
|
|
|
y = y + s.dy
|
|
|
|
prevloc = currloc
|
2024-07-21 19:53:29 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
else
|
|
|
|
-- drawing
|
|
|
|
currloc = {mode='drawing', line=line_index}
|
|
|
|
if y + rect.dy > dy then
|
2024-07-21 19:34:54 +01:00
|
|
|
return prevloc
|
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
y = y + rect.dy
|
|
|
|
prevloc = currloc
|
2024-07-21 19:34:54 +01:00
|
|
|
end
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
return edit.top(editor)
|
2024-07-21 19:34:54 +01:00
|
|
|
end -- 1 scan
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit.top(editor)
|
|
|
|
assert(#editor.lines > 0)
|
|
|
|
if editor.lines[1].mode == 'text' then
|
2024-07-21 06:46:04 +01:00
|
|
|
return {mode='text', line=1, pos=1}
|
|
|
|
else
|
2024-07-21 17:14:01 +01:00
|
|
|
return {mode='drawing', line=1}
|
2024-07-21 06:46:04 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-07-20 20:16:14 +01:00
|
|
|
-- find the location at x=x0 on the same screen line as loc
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit.hor(editor, loc, x0) -- scans line
|
2024-07-20 20:16:14 +01:00
|
|
|
if loc.mode == 'drawing' then
|
2024-09-01 02:48:40 +01:00
|
|
|
assert(editor.lines[loc.line].mode == 'drawing')
|
2024-07-22 01:47:01 +01:00
|
|
|
return deepcopy(loc)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
local rect = edit.get_rect(editor, {mode=loc.mode, line=loc.line, pos=1})
|
|
|
|
x0 = x0 - editor.left
|
2024-07-22 01:58:46 +01:00
|
|
|
assert(rect.screen_line_rects)
|
|
|
|
for i,sc in ipairs(rect.screen_line_rects) do
|
|
|
|
if loc.pos >= sc.pos and loc.pos < sc.pos+sc.dpos then
|
2024-07-29 04:20:13 +01:00
|
|
|
local prevx = nil
|
2024-07-22 01:58:46 +01:00
|
|
|
for _,c in ipairs(sc.char_rects) do
|
2024-07-29 04:20:13 +01:00
|
|
|
if (prevx == nil or x0 >= (prevx+c.x)/2) and x0 < c.x+c.dx/2 then
|
2024-07-22 01:58:46 +01:00
|
|
|
return {mode='text', line=loc.line, pos=c.pos}
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-29 04:20:13 +01:00
|
|
|
prevx = c.x
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
2024-07-22 06:25:30 +01:00
|
|
|
return {mode='text', line=loc.line, pos=sc.pos+sc.dpos-1}
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end -- 0-1 scans
|
|
|
|
|
|
|
|
-- helper to check if space character at pos, x is final possible candidate
|
2024-09-01 02:48:40 +01:00
|
|
|
-- for line wrapping before x = editor.right
|
|
|
|
-- x lies between editor.left and editor.right
|
|
|
|
function Text.line_wrap_at_word_boundary(editor, x, line, pos)
|
|
|
|
if x - editor.left > 0.8 * (editor.right - editor.left) then
|
2024-07-20 20:16:14 +01:00
|
|
|
-- word boundary more than 80% of the way across.
|
2024-09-01 02:48:40 +01:00
|
|
|
-- check if there's any more word boundaries before x gets to editor.right
|
2024-07-20 20:16:14 +01:00
|
|
|
for pos, char in utf8chars(line, pos+1) do
|
2024-09-01 02:48:40 +01:00
|
|
|
if x > editor.right then
|
2024-07-20 20:16:14 +01:00
|
|
|
return true
|
|
|
|
end
|
2024-09-01 02:48:40 +01:00
|
|
|
local w = editor.font:getWidth(char) -- width of char
|
2024-07-20 20:16:14 +01:00
|
|
|
if char:match('%s') then
|
2024-09-01 02:48:40 +01:00
|
|
|
if x+w < editor.right then
|
2024-07-20 20:16:14 +01:00
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
x = x+w
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit.screen_line_height(editor, loc)
|
2024-07-20 20:16:14 +01:00
|
|
|
if loc.mode == 'text' then
|
2024-09-01 02:48:40 +01:00
|
|
|
return editor.line_height
|
2024-07-20 20:16:14 +01:00
|
|
|
else
|
2024-09-01 02:48:40 +01:00
|
|
|
local line = editor.lines[loc.line]
|
2024-07-20 20:16:14 +01:00
|
|
|
assert(line.mode == 'drawing')
|
2024-09-01 02:48:40 +01:00
|
|
|
return Drawing.height(editor, line)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function Drawing.height(editor, line)
|
|
|
|
return Drawing_padding_height + Drawing.pixels(line.h, editor.width)
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function edit.eq(a, b)
|
2024-07-21 17:14:01 +01:00
|
|
|
return a.mode == b.mode and a.line == b.line and --[[just for text mode]] a.pos == b.pos
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function edit.lt(a, b)
|
|
|
|
if a.line < b.line then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
if a.line > b.line then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
if a.mode == 'drawing' then
|
|
|
|
return false -- no further comparisons within drawings
|
|
|
|
end
|
|
|
|
return a.pos < b.pos
|
|
|
|
end
|
|
|
|
|
|
|
|
function edit.le(a, b)
|
|
|
|
if a.line < b.line then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
if a.line > b.line then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
if a.mode == 'drawing' then
|
|
|
|
return true -- no further comparisons within drawings
|
|
|
|
end
|
|
|
|
return a.pos <= b.pos
|
|
|
|
end
|
|
|
|
|
2024-09-01 02:48:40 +01:00
|
|
|
function edit.get_rect(editor, loc)
|
2024-07-27 05:43:01 +01:00
|
|
|
if loc.mode == 'text' then
|
2024-09-01 02:48:40 +01:00
|
|
|
return Text.get_rect(editor, loc)
|
2024-07-22 01:58:46 +01:00
|
|
|
else
|
2024-09-01 02:48:40 +01:00
|
|
|
return {x=0, y=0, dx=editor.width, dy=edit.screen_line_height(editor, loc)}
|
2024-07-20 20:16:14 +01:00
|
|
|
end
|
|
|
|
end
|
2024-07-21 19:10:25 +01:00
|
|
|
|
2024-09-03 23:23:41 +01:00
|
|
|
-- generate rects for each screen line in it and the range [pos,pos+dpos-1] associated with each
|
|
|
|
-- within each screen line generate rects for each char (utf8 codepoint) and the pos associated with each.
|
2024-09-01 02:48:40 +01:00
|
|
|
function Text.get_rect(editor, loc)
|
|
|
|
local line = editor.lines[loc.line]
|
2024-07-21 19:10:25 +01:00
|
|
|
assert(line.mode == 'text')
|
2024-07-22 01:58:46 +01:00
|
|
|
local screen_lines = {}
|
|
|
|
local curr_screen_line = {}
|
|
|
|
local spos = 1
|
|
|
|
local x, y = 0, 0
|
2024-07-27 05:43:01 +01:00
|
|
|
for pos,char in utf8chars(line.data, loc.pos) do
|
2024-09-01 02:48:40 +01:00
|
|
|
local w = editor.font:getWidth(char)
|
2024-07-21 19:10:25 +01:00
|
|
|
if char:match('%s') then
|
2024-09-01 02:48:40 +01:00
|
|
|
if Text.line_wrap_at_word_boundary(editor, editor.left + x, line.data, pos) then
|
2024-07-22 01:58:46 +01:00
|
|
|
table.insert(curr_screen_line,
|
2024-09-16 07:14:04 +01:00
|
|
|
{x=x, y=y, dx=w, dy=editor.line_height, pos=pos, data=char}) -- char
|
2024-09-15 19:18:14 +01:00
|
|
|
table.insert(curr_screen_line,
|
2024-09-15 19:24:07 +01:00
|
|
|
{x=x+w, y=y, dx=editor.width-x-w, dy=editor.line_height, pos=pos+1}) -- filler
|
2024-07-22 01:58:46 +01:00
|
|
|
table.insert(screen_lines,
|
2024-09-01 02:48:40 +01:00
|
|
|
{x=0, y=y, dx=editor.width, dy=editor.line_height,
|
2024-07-22 02:12:06 +01:00
|
|
|
pos=spos, dpos=pos-spos+1, char_rects=curr_screen_line})
|
2024-07-21 19:10:25 +01:00
|
|
|
curr_screen_line = {}
|
2024-07-22 01:58:46 +01:00
|
|
|
spos = pos+1
|
|
|
|
x = 0
|
2024-09-01 02:48:40 +01:00
|
|
|
y = y + editor.line_height
|
|
|
|
if y > editor.bottom - editor.top then
|
|
|
|
return {x=0, y=0, dx=editor.width, dy=y, screen_line_rects=screen_lines}
|
2024-07-27 05:43:01 +01:00
|
|
|
end
|
2024-07-21 19:10:25 +01:00
|
|
|
else
|
2024-07-22 01:58:46 +01:00
|
|
|
table.insert(curr_screen_line,
|
2024-09-16 07:14:04 +01:00
|
|
|
{x=x, y=y, dx=w, dy=editor.line_height, pos=pos, data=char})
|
2024-07-21 19:10:25 +01:00
|
|
|
x = x + w
|
|
|
|
end
|
|
|
|
else
|
2024-09-01 02:48:40 +01:00
|
|
|
if x+w > editor.width then
|
2024-07-22 01:58:46 +01:00
|
|
|
assert(pos > 1)
|
|
|
|
table.insert(curr_screen_line,
|
2024-09-15 19:24:07 +01:00
|
|
|
{x=x, y=y, dx=editor.width-x, dy=editor.line_height, pos=pos}) -- filler
|
2024-07-22 01:58:46 +01:00
|
|
|
table.insert(screen_lines,
|
2024-09-01 02:48:40 +01:00
|
|
|
{x=0, y=y, dx=editor.width, dy=editor.line_height,
|
2024-07-22 02:12:06 +01:00
|
|
|
pos=spos, dpos=(pos-1)-spos+1, char_rects=curr_screen_line})
|
2024-07-21 19:10:25 +01:00
|
|
|
curr_screen_line = {}
|
2024-07-22 01:58:46 +01:00
|
|
|
spos = pos
|
|
|
|
x = 0
|
2024-09-01 02:48:40 +01:00
|
|
|
y = y + editor.line_height
|
|
|
|
if y > editor.bottom - editor.top then
|
|
|
|
return {x=0, y=0, dx=editor.width, dy=y, screen_line_rects=screen_lines}
|
2024-07-27 05:43:01 +01:00
|
|
|
end
|
2024-07-21 19:10:25 +01:00
|
|
|
else
|
|
|
|
-- nothing
|
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
table.insert(curr_screen_line,
|
2024-09-16 07:14:04 +01:00
|
|
|
{x=x, y=y, dx=w, dy=editor.line_height, pos=pos, data=char})
|
2024-07-21 19:10:25 +01:00
|
|
|
x = x + w
|
|
|
|
end
|
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
table.insert(curr_screen_line,
|
2024-09-16 07:14:04 +01:00
|
|
|
{x=x, y=y, dx=editor.width-x, dy=editor.line_height, pos=utf8.len(line.data)+1}) -- filler
|
2024-07-22 01:58:46 +01:00
|
|
|
table.insert(screen_lines,
|
2024-09-01 02:48:40 +01:00
|
|
|
{x=0, y=y, dx=editor.width, dy=editor.line_height,
|
2024-07-22 02:12:06 +01:00
|
|
|
pos=spos, dpos=utf8.len(line.data)+1-spos+1, char_rects=curr_screen_line})
|
2024-09-01 02:48:40 +01:00
|
|
|
y = y + editor.line_height
|
|
|
|
return {x=0, y=0, dx=editor.width, dy=y, line_index=loc.line, screen_line_rects=screen_lines}
|
2024-07-22 01:58:46 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function find_x(rects, x)
|
|
|
|
if x < 0 then return rects[1] end
|
|
|
|
for _, rect in ipairs(rects) do
|
|
|
|
if within(x, rect.x, rect.x+rect.dx) then
|
|
|
|
return rect
|
|
|
|
end
|
2024-07-21 19:10:25 +01:00
|
|
|
end
|
2024-07-22 01:58:46 +01:00
|
|
|
return rects[#rects]
|
|
|
|
end
|
|
|
|
|
|
|
|
function find_y(rects, y)
|
|
|
|
if y < 0 then return rects[1] end
|
|
|
|
for _, rect in ipairs(rects) do
|
|
|
|
if within(y, rect.y, rect.y+rect.dy) then
|
|
|
|
return rect
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return rects[#rects]
|
|
|
|
end
|
|
|
|
|
|
|
|
function within_rect(rect, x,y)
|
|
|
|
return within(x, rect.x, rect.x+rect.dx)
|
|
|
|
and within(y, rect.y, rect.y+rect.dy)
|
|
|
|
end
|
|
|
|
|
|
|
|
function within(a, lo, hi)
|
|
|
|
return a >= lo and a < hi
|
|
|
|
end
|
|
|
|
|
|
|
|
-- create a new iterator for s which provides the index and UTF-8 bytes corresponding to each codepoint
|
|
|
|
function utf8chars(s, startpos)
|
|
|
|
local next_pos = startpos or 1 -- in code points
|
|
|
|
local next_offset = utf8.offset(s, next_pos) -- in bytes
|
|
|
|
return function()
|
|
|
|
assert(next_offset) -- never call the iterator after it returns nil
|
|
|
|
local curr_pos = next_pos
|
|
|
|
next_pos = next_pos+1
|
|
|
|
local curr_offset = next_offset
|
|
|
|
next_offset = utf8.offset(s, 2, next_offset)
|
|
|
|
if next_offset == nil then return end
|
|
|
|
local curr_char = s:sub(curr_offset, next_offset-1)
|
|
|
|
return curr_pos, curr_char
|
|
|
|
end
|
|
|
|
end
|