mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
textboxwidget and scrolltextwidget enhancements (#2393)
util: made isSplitable() accept an optional next_char for wiser decision textboxwidget: speed up rendering, enhanced text wrapping, allow selection of multiple words with Hold. scrolltextwidget: allow scrolling with Tap. Details in #2393
This commit is contained in:
@@ -20,6 +20,7 @@ local Screen = require("device").screen
|
||||
local Geom = require("ui/geometry")
|
||||
local util = require("util")
|
||||
local DEBUG= require("dbg")
|
||||
local TimeVal = require("ui/timeval")
|
||||
|
||||
local TextBoxWidget = Widget:new{
|
||||
text = nil,
|
||||
@@ -79,8 +80,14 @@ function TextBoxWidget:_evalCharWidthList()
|
||||
self.charpos = #self.charlist + 1
|
||||
end
|
||||
self.char_width_list = {}
|
||||
-- use a cache to avoid many calls to RenderText:sizeUtf8Text()
|
||||
local char_width_cache = {}
|
||||
for _, v in ipairs(self.charlist) do
|
||||
local w = RenderText:sizeUtf8Text(0, Screen:getWidth(), self.face, v, true, self.bold).x
|
||||
local w = char_width_cache[v]
|
||||
if w == nil then
|
||||
w = RenderText:sizeUtf8Text(0, Screen:getWidth(), self.face, v, true, self.bold).x
|
||||
char_width_cache[v] = w
|
||||
end
|
||||
table.insert(self.char_width_list, {char = v, width = w})
|
||||
end
|
||||
end
|
||||
@@ -112,7 +119,9 @@ function TextBoxWidget:_splitCharWidthList()
|
||||
else
|
||||
-- Backtrack the string until the length fit into one line.
|
||||
local c = self.char_width_list[idx].char
|
||||
if util.isSplitable(c) then
|
||||
-- We give next char to isSplitable() for a wiser decision
|
||||
local next_c = idx+1 <= size and self.char_width_list[idx+1].char or nil
|
||||
if util.isSplitable(c, next_c) then
|
||||
cur_line_text = table.concat(self.charlist, "", offset, idx - 1)
|
||||
cur_line_width = cur_line_width - self.char_width_list[idx].width
|
||||
else
|
||||
@@ -122,8 +131,9 @@ function TextBoxWidget:_splitCharWidthList()
|
||||
adjusted_width = adjusted_width - self.char_width_list[adjusted_idx].width
|
||||
if adjusted_idx == 1 then break end
|
||||
adjusted_idx = adjusted_idx - 1
|
||||
next_c = c
|
||||
c = self.char_width_list[adjusted_idx].char
|
||||
until adjusted_idx > offset and util.isSplitable(c)
|
||||
until adjusted_idx == offset or util.isSplitable(c, next_c)
|
||||
if adjusted_idx == offset then -- a very long english word ocuppying more than one line
|
||||
cur_line_text = table.concat(self.charlist, "", offset, idx - 1)
|
||||
cur_line_width = cur_line_width - self.char_width_list[idx].width
|
||||
@@ -144,6 +154,12 @@ function TextBoxWidget:_splitCharWidthList()
|
||||
idx = idx + 1
|
||||
-- FIXME: reuse newline entry
|
||||
self.vertical_string_list[ln+1] = {text = "", offset = idx, width = 0}
|
||||
else
|
||||
-- If next char is a space, discard it so it does not become
|
||||
-- an ugly leading space on the next line
|
||||
if idx <= size and self.char_width_list[idx].char == " " then
|
||||
idx = idx + 1
|
||||
end
|
||||
end
|
||||
ln = ln + 1
|
||||
-- Make sure `idx` point to the next char to be processed in the next loop.
|
||||
@@ -288,6 +304,7 @@ function TextBoxWidget:free()
|
||||
end
|
||||
end
|
||||
|
||||
-- Allow selection of a single word at hold position
|
||||
function TextBoxWidget:onHoldWord(callback, ges)
|
||||
if not callback then return end
|
||||
|
||||
@@ -333,4 +350,117 @@ function TextBoxWidget:onHoldWord(callback, ges)
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
-- Allow selection of one or more words (with no visual feedback)
|
||||
-- Gestures should be declared in widget using us (e.g dictquicklookup.lua)
|
||||
|
||||
-- Constants for which side of a word to find
|
||||
local FIND_START = 1
|
||||
local FIND_END = 2
|
||||
|
||||
function TextBoxWidget:onHoldStartText(_, ges)
|
||||
-- just store hold start position and timestamp, will be used on release
|
||||
self.hold_start_x = ges.pos.x - self.dimen.x
|
||||
self.hold_start_y = ges.pos.y - self.dimen.y
|
||||
self.hold_start_tv = TimeVal.now()
|
||||
return true
|
||||
end
|
||||
|
||||
function TextBoxWidget:onHoldReleaseText(callback, ges)
|
||||
if not callback then return end
|
||||
|
||||
local hold_end_x = ges.pos.x - self.dimen.x
|
||||
local hold_end_y = ges.pos.y - self.dimen.y
|
||||
local hold_duration = TimeVal.now() - self.hold_start_tv
|
||||
hold_duration = hold_duration.sec + hold_duration.usec/1000000
|
||||
|
||||
-- Swap start and end if needed
|
||||
local x0, y0, x1, y1
|
||||
-- first, sort by y/line_num
|
||||
local start_line_num = math.ceil(self.hold_start_y / self.line_height_px)
|
||||
local end_line_num = math.ceil(hold_end_y / self.line_height_px)
|
||||
if start_line_num < end_line_num then
|
||||
x0, y0 = self.hold_start_x, self.hold_start_y
|
||||
x1, y1 = hold_end_x, hold_end_y
|
||||
elseif start_line_num > end_line_num then
|
||||
x0, y0 = hold_end_x, hold_end_y
|
||||
x1, y1 = self.hold_start_x, self.hold_start_y
|
||||
else -- same line_num : sort by x
|
||||
if self.hold_start_x <= hold_end_x then
|
||||
x0, y0 = self.hold_start_x, self.hold_start_y
|
||||
x1, y1 = hold_end_x, hold_end_y
|
||||
else
|
||||
x0, y0 = hold_end_x, hold_end_y
|
||||
x1, y1 = self.hold_start_x, self.hold_start_y
|
||||
end
|
||||
end
|
||||
|
||||
-- similar code to find start or end is in _findWordEdge() helper
|
||||
local sel_start_idx = self:_findWordEdge(x0, y0, FIND_START)
|
||||
local sel_end_idx = self:_findWordEdge(x1, y1, FIND_END)
|
||||
|
||||
if not sel_start_idx or not sel_end_idx then
|
||||
-- one or both hold points were out of text
|
||||
return true
|
||||
end
|
||||
|
||||
local selected_text = table.concat(self.charlist, "", sel_start_idx, sel_end_idx)
|
||||
DEBUG("onHoldReleaseText (duration:", hold_duration, ") :", sel_start_idx, ">", sel_end_idx, "=", selected_text)
|
||||
callback(selected_text, hold_duration)
|
||||
return true
|
||||
end
|
||||
|
||||
function TextBoxWidget:_findWordEdge(x, y, side)
|
||||
if side ~= FIND_START and side ~= FIND_END then
|
||||
return
|
||||
end
|
||||
local line_num = math.ceil(y / self.line_height_px) + self.virtual_line_num-1
|
||||
local line = self.vertical_string_list[line_num]
|
||||
if not line then
|
||||
return -- below last line : no selection
|
||||
end
|
||||
local char_start = line.offset
|
||||
local char_end -- char_end is non-inclusive
|
||||
if line_num >= #self.vertical_string_list then
|
||||
char_end = #self.char_width_list + 1
|
||||
else
|
||||
char_end = self.vertical_string_list[line_num+1].offset
|
||||
end
|
||||
local char_probe_x = 0
|
||||
local idx = char_start
|
||||
local edge_idx = nil
|
||||
-- find which character the touch is holding
|
||||
while idx < char_end do
|
||||
local c = self.char_width_list[idx]
|
||||
char_probe_x = char_probe_x + c.width
|
||||
if char_probe_x > x then
|
||||
-- character found, find which word the character is in, and
|
||||
-- get its start/end idx
|
||||
local words = util.splitToWords(line.text)
|
||||
-- words may contain separators (space, punctuation) : we don't
|
||||
-- discriminate here, it's the caller job to clean what was
|
||||
-- selected
|
||||
local probe_idx = char_start
|
||||
local next_probe_idx
|
||||
for _, w in ipairs(words) do
|
||||
next_probe_idx = probe_idx + #util.splitToChars(w)
|
||||
if idx < next_probe_idx then
|
||||
if side == FIND_START then
|
||||
edge_idx = probe_idx
|
||||
elseif side == FIND_END then
|
||||
edge_idx = next_probe_idx - 1
|
||||
end
|
||||
break
|
||||
end
|
||||
probe_idx = next_probe_idx
|
||||
end
|
||||
if edge_idx then
|
||||
break
|
||||
end
|
||||
end
|
||||
idx = idx + 1
|
||||
end
|
||||
return edge_idx
|
||||
end
|
||||
|
||||
return TextBoxWidget
|
||||
|
||||
Reference in New Issue
Block a user