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
pull/2419/head
poire-z 7 years ago committed by Qingping Hou
parent d1f9cf932b
commit 5040bfe4c5

@ -50,7 +50,7 @@ function ScrollTextWidget:init()
}
local horizontal_group = HorizontalGroup:new{}
table.insert(horizontal_group, self.text_widget)
table.insert(horizontal_group, HorizontalSpan:new{self.text_scroll_span})
table.insert(horizontal_group, HorizontalSpan:new{width=self.text_scroll_span})
table.insert(horizontal_group, self.v_scroll_bar)
self[1] = horizontal_group
self.dimen = Geom:new(self[1]:getSize())
@ -62,6 +62,12 @@ function ScrollTextWidget:init()
range = function() return self.dimen end,
},
},
TapScrollText = { -- allow scrolling with tap
GestureRange:new{
ges = "tap",
range = function() return self.dimen end,
},
},
}
end
if Device:hasKeyboard() or Device:hasKeys() then
@ -101,10 +107,30 @@ end
function ScrollTextWidget:onScrollText(arg, ges)
if ges.direction == "north" then
self:scrollText(1)
return true
elseif ges.direction == "south" then
self:scrollText(-1)
return true
end
return true
-- if swipe west/east, let it propagate up (e.g. for quickdictlookup to
-- go to next/prev result)
end
function ScrollTextWidget:onTapScrollText(arg, ges)
-- same tests as done in TextBoxWidget:scrollUp/Down
if ges.pos.x < Screen:getWidth()/2 then
if self.text_widget.virtual_line_num > 1 then
self:scrollText(-1)
return true
end
else
if self.text_widget.virtual_line_num + self.text_widget:getVisLineCount() <= #self.text_widget.vertical_string_list then
self:scrollText(1)
return true
end
end
-- if we couldn't scroll (because we're already at top or bottom),
-- let it propagate up (e.g. for quickdictlookup to go to next/prev result)
end
function ScrollTextWidget:onScrollDown()

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

@ -146,9 +146,34 @@ function util.splitToWords(text)
return wlist
end
-- Test whether a string could be separated by a char for multi-line rendering
function util.isSplitable(c)
return util.isCJKChar(c) or c == " " or string.match(c, "%p") ~= nil
-- We don't want to split on a space if it is followed by some
-- specific punctuation : e.g. "word :" or "word )"
-- (In french, there is a space before a colon, and it better
-- not be wrapped there.)
-- Includes U+00BB >> (right double angle quotation mark) and
-- U+201D '' (right double quotation mark)
local non_splitable_space_tailers = ":;,.!?)]}$%-=/<>»”"
-- Test whether a string could be separated by this char for multi-line rendering
-- Optional next char may be provided to help make the decision
function util.isSplitable(c, next_c)
if util.isCJKChar(c) then
-- a CJKChar is a word in itself, and so is splitable
return true
elseif c == " " then
-- we only split on a space (so punctuation sticks to prev word)
-- if next_c is provided, we can make a better decision
if next_c and non_splitable_space_tailers:find(next_c, 1, true) then
-- this space is followed by some punctuation that is better
-- kept with us along previous word
return false
else
-- we can split on this space
return true
end
end
-- otherwise, non splitable
return false
end
return util

@ -93,14 +93,12 @@ describe("util module", function()
if i == #table_chars then table.insert(table_of_words, word) end
end
assert.are_same(table_of_words, {
"Pójdźże,",
" ",
"Pójdźże, ",
"chmurność ",
"glück ",
"schließen ",
"Štěstí ",
"neštěstí.",
" ",
"neštěstí. ",
"Uñas ",
"gavilán",
})
@ -126,4 +124,38 @@ describe("util module", function()
})
end)
it("should split text to line with next_c - unicode", function()
local text = "Ce test : 1) est très simple ; 2 ) simple comme ( 2/2 ) > 50 % ? ok."
local word = ""
local table_of_words = {}
local c
local table_chars = util.splitToChars(text)
for i = 1, #table_chars do
c = table_chars[i]
next_c = i < #table_chars and table_chars[i+1] or nil
word = word .. c
if util.isSplitable(c, next_c) then
table.insert(table_of_words, word)
word = ""
end
if i == #table_chars then table.insert(table_of_words, word) end
end
assert.are_same(table_of_words, {
"Ce ",
"test : ",
"1) ",
"est ",
"très ",
"simple ; ",
"2 ) ",
"simple ",
"comme ",
"( ",
"2/2 ) > ",
"50 % ? ",
"ok."
})
end)
end)

Loading…
Cancel
Save