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:
poire-z
2016-12-06 22:10:25 +01:00
committed by Qingping Hou
parent d1f9cf932b
commit 5040bfe4c5
4 changed files with 225 additions and 12 deletions

View File

@@ -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