textboxwidget(fix): handle onHoldWord event

pull/2061/head
Qingping Hou 8 years ago
parent adf5ffdd26
commit 301925e34a

@ -1,3 +1,17 @@
--[[--
A TextWidget that handles long text wrapping
Example:
local Foo = TextBoxWidget:new{
face = Font:getFace("cfont", 25),
text = 'We can show multiple lines.\nFoo.\nBar.',
-- width = Screen:getWidth()*2/3,
}
UIManager:show(Foo)
]]
local Blitbuffer = require("ffi/blitbuffer") local Blitbuffer = require("ffi/blitbuffer")
local Widget = require("ui/widget/widget") local Widget = require("ui/widget/widget")
local LineWidget = require("ui/widget/linewidget") local LineWidget = require("ui/widget/linewidget")
@ -5,11 +19,7 @@ local RenderText = require("ui/rendertext")
local Screen = require("device").screen local Screen = require("device").screen
local Geom = require("ui/geometry") local Geom = require("ui/geometry")
local util = require("util") local util = require("util")
local DEBUG = require("dbg")
--[[
A TextWidget that handles long text wrapping
--]]
local TextBoxWidget = Widget:new{ local TextBoxWidget = Widget:new{
text = nil, text = nil,
charlist = nil, charlist = nil,
@ -29,11 +39,11 @@ local TextBoxWidget = Widget:new{
} }
function TextBoxWidget:init() function TextBoxWidget:init()
local line_height = (1 + self.line_height) * self.face.size self.line_height_px = (1 + self.line_height) * self.face.size
self.cursor_line = LineWidget:new{ self.cursor_line = LineWidget:new{
dimen = Geom:new{ dimen = Geom:new{
w = Screen:scaleBySize(1), w = Screen:scaleBySize(1),
h = line_height, h = self.line_height_px,
} }
} }
self:_evalCharWidthList() self:_evalCharWidthList()
@ -51,7 +61,7 @@ function TextBoxWidget:init()
self.dimen = Geom:new(self:getSize()) self.dimen = Geom:new(self:getSize())
end end
-- Evaluate the width of each char in `self.charlist`. -- Split `self.text` into `self.charlist` and evaluate the width of each char in it.
function TextBoxWidget:_evalCharWidthList() function TextBoxWidget:_evalCharWidthList()
if self.charlist == nil then if self.charlist == nil then
self.charlist = util.splitToChars(self.text) self.charlist = util.splitToChars(self.text)
@ -66,8 +76,9 @@ end
-- Split the text into logical lines to fit into the text box. -- Split the text into logical lines to fit into the text box.
function TextBoxWidget:_splitCharWidthList() function TextBoxWidget:_splitCharWidthList()
self.vertical_string_list = {} self.vertical_string_list = {
self.vertical_string_list[1] = {text = "Demo hint", offset = 1, width = 0} -- hint for empty string {text = "Demo hint", offset = 1, width = 0} -- hint for empty string
}
local idx = 1 local idx = 1
local size = #self.char_width_list local size = #self.char_width_list
@ -113,10 +124,15 @@ function TextBoxWidget:_splitCharWidthList()
end end
end -- endif util.isSplitable(c) end -- endif util.isSplitable(c)
end -- endif cur_line_width > self.width end -- endif cur_line_width > self.width
self.vertical_string_list[ln] = {text = cur_line_text, offset = offset, width = cur_line_width} self.vertical_string_list[ln] = {
text = cur_line_text,
offset = offset,
width = cur_line_width
}
if hard_newline then if hard_newline then
idx = idx + 1 idx = idx + 1
self.vertical_string_list[ln + 1] = {text = "", offset = idx, width = 0} -- FIXME: reuse newline entry
self.vertical_string_list[ln+1] = {text = "", offset = idx, width = 0}
end end
ln = ln + 1 ln = ln + 1
-- Make sure `idx` point to the next char to be processed in the next loop. -- Make sure `idx` point to the next char to be processed in the next loop.
@ -125,11 +141,10 @@ end
function TextBoxWidget:_renderText(start_row_idx, end_row_idx) function TextBoxWidget:_renderText(start_row_idx, end_row_idx)
local font_height = self.face.size local font_height = self.face.size
local line_height = (1 + self.line_height) * font_height
if start_row_idx < 1 then start_row_idx = 1 end if start_row_idx < 1 then start_row_idx = 1 end
if end_row_idx > #self.vertical_string_list then end_row_idx = #self.vertical_string_list end if end_row_idx > #self.vertical_string_list then end_row_idx = #self.vertical_string_list end
local row_count = end_row_idx == 0 and 1 or end_row_idx - start_row_idx + 1 local row_count = end_row_idx == 0 and 1 or end_row_idx - start_row_idx + 1
local h = line_height * row_count local h = self.line_height_px * row_count
self._bb = Blitbuffer.new(self.width, h) self._bb = Blitbuffer.new(self.width, h)
self._bb:fill(Blitbuffer.COLOR_WHITE) self._bb:fill(Blitbuffer.COLOR_WHITE)
local y = font_height local y = font_height
@ -139,7 +154,7 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx)
--@TODO Don't use kerning for monospaced fonts. (houqp) --@TODO Don't use kerning for monospaced fonts. (houqp)
-- refert to cb25029dddc42693cc7aaefbe47e9bd3b7e1a750 in master tree -- refert to cb25029dddc42693cc7aaefbe47e9bd3b7e1a750 in master tree
RenderText:renderUtf8Text(self._bb, pen_x, y, self.face, line.text, true, self.bold, self.fgcolor) RenderText:renderUtf8Text(self._bb, pen_x, y, self.face, line.text, true, self.bold, self.fgcolor)
y = y + line_height y = y + self.line_height_px
end end
-- -- if text is shorter than one line, shrink to text's width -- -- if text is shorter than one line, shrink to text's width
-- if #v_list == 1 then -- if #v_list == 1 then
@ -162,17 +177,15 @@ function TextBoxWidget:_findCharPos()
x = x + self.char_width_list[offset].width x = x + self.char_width_list[offset].width
offset = offset + 1 offset = offset + 1
end end
local line_height = (1 + self.line_height) * self.face.size return x + 1, (ln - 1) * self.line_height_px -- offset `x` by 1 to avoid overlap
return x + 1, (ln - 1) * line_height -- offset `x` by 1 to avoid overlap
end end
-- Click event: Move the cursor to a new location with (x, y), in pixels. -- Click event: Move the cursor to a new location with (x, y), in pixels.
-- Be aware of virtual line number of the scorllTextWidget. -- Be aware of virtual line number of the scorllTextWidget.
function TextBoxWidget:moveCursor(x, y) function TextBoxWidget:moveCursor(x, y)
local w = 0 local w = 0
local line_height = (1 + self.line_height) * self.face.size
local ln = self.height == nil and 1 or self.virtual_line_num local ln = self.height == nil and 1 or self.virtual_line_num
ln = ln + math.ceil(y / line_height) - 1 ln = ln + math.ceil(y / self.line_height_px) - 1
if ln > #self.vertical_string_list then if ln > #self.vertical_string_list then
ln = #self.vertical_string_list ln = #self.vertical_string_list
x = self.width x = self.width
@ -191,13 +204,13 @@ function TextBoxWidget:moveCursor(x, y)
end end
self:free() self:free()
self:_renderText(1, #self.vertical_string_list) self:_renderText(1, #self.vertical_string_list)
self.cursor_line:paintTo(self._bb, w + 1, (ln - self.virtual_line_num) * line_height) self.cursor_line:paintTo(self._bb, w + 1,
(ln - self.virtual_line_num) * self.line_height_px)
return offset return offset
end end
function TextBoxWidget:getVisLineCount() function TextBoxWidget:getVisLineCount()
local line_height = (1 + self.line_height) * self.face.size return math.floor(self.height / self.line_height_px)
return math.floor(self.height / line_height)
end end
function TextBoxWidget:getAllLineCount() function TextBoxWidget:getAllLineCount()
@ -213,7 +226,7 @@ function TextBoxWidget:scrollDown()
self.virtual_line_num = self.virtual_line_num + visible_line_count self.virtual_line_num = self.virtual_line_num + visible_line_count
self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1) self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1)
end end
return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list
end end
-- TODO: modify `charpos` so that it can render the cursor -- TODO: modify `charpos` so that it can render the cursor
@ -228,7 +241,7 @@ function TextBoxWidget:scrollUp()
end end
self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1) self:_renderText(self.virtual_line_num, self.virtual_line_num + visible_line_count - 1)
end end
return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list return (self.virtual_line_num - 1) / #self.vertical_string_list, (self.virtual_line_num - 1 + visible_line_count) / #self.vertical_string_list
end end
function TextBoxWidget:getSize() function TextBoxWidget:getSize()
@ -252,21 +265,48 @@ function TextBoxWidget:free()
end end
function TextBoxWidget:onHoldWord(callback, ges) function TextBoxWidget:onHoldWord(callback, ges)
if not callback then return end
local x, y = ges.pos.x - self.dimen.x, ges.pos.y - self.dimen.y local x, y = ges.pos.x - self.dimen.x, ges.pos.y - self.dimen.y
for _, l in ipairs(self.rendering_vlist) do local line_num = math.ceil(y / self.line_height_px)
for _, w in ipairs(l) do local line = self.vertical_string_list[line_num]
local box = w.box
if x > box.x and x < box.x + box.w and if line then
y > box.y and y < box.y + box.h then local char_start = line.offset
DEBUG("found word", w, "at", x, y) local char_end -- char_end is non-inclusive
if callback then if line_num >= #self.vertical_string_list then
callback(w.word) 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
-- find which character the touch is holding
while idx < char_end do
local c = self.char_width_list[idx]
-- FIXME: this might break if kerning is enabled
char_probe_x = char_probe_x + c.width
if char_probe_x > x then
-- ignore spaces
if c.char == " " then break end
-- now find which word the character is in
local words = util.splitToWords(line.text)
local probe_idx = char_start
for _,w in ipairs(words) do
-- +1 for word separtor
probe_idx = probe_idx + string.len(w)
if idx <= probe_idx then
callback(w)
return
end
end end
break break
end end
idx = idx + 1
end end
end end
return true
return
end end
return TextBoxWidget return TextBoxWidget

@ -1,16 +1,18 @@
local BaseUtil = require("ffi/util")
--[[-- --[[--
Miscellaneous helper functions for KOReader frontend. Miscellaneous helper functions for KOReader frontend.
]] ]]
local BaseUtil = require("ffi/util")
local util = {} local util = {}
function util.stripePunctuations(word) --- Strip all punctuations and spaces in a string.
if not word then return end ---- @string text the string to be stripped
-- strip ASCII punctuation characters around word ---- @treturn string stripped text
-- and strip any generic punctuation (U+2000 - U+206F) in the word function util.stripePunctuations(text)
return word:gsub("\226[\128-\131][\128-\191]", ''):gsub("^%p+", ''):gsub("%p+$", '') if not text then return end
-- strip ASCII punctuation characters around text
-- and strip any generic punctuation (U+2000 - U+206F) in the text
return text:gsub("\226[\128-\131][\128-\191]", ''):gsub("^%p+", ''):gsub("%p+$", '')
end end
--[[ --[[
@ -74,7 +76,7 @@ end
--- Returns number of keys in a table. --- Returns number of keys in a table.
---- @param T Lua table ---- @param T Lua table
---- @return number of keys in table T ---- @treturn int number of keys in table T
function util.tableSize(T) function util.tableSize(T)
local count = 0 local count = 0
for _ in pairs(T) do count = count + 1 end for _ in pairs(T) do count = count + 1 end
@ -97,8 +99,9 @@ function util.lastIndexOf(string, ch)
end end
-- Split string into a list of UTF-8 chars. --- Split string into a list of UTF-8 chars.
-- @text: the string to be splitted. ---- @string text the string to be splitted.
---- @treturn table list of UTF-8 chars
function util.splitToChars(text) function util.splitToChars(text)
local tab = {} local tab = {}
if text ~= nil then if text ~= nil then
@ -114,6 +117,20 @@ function util.splitToChars(text)
return tab return tab
end end
--- Split text into a list of words, spaces and punctuations.
---- @string text text to split
---- @treturn table list of words, spaces and punctuations
function util.splitToWords(text)
-- TODO: write test
local wlist = {}
for words in text:gmatch("[\32-\127\192-\255]+[\128-\191]*") do
for word in util.gsplit(words, "[%s%p]+", true) do
table.insert(wlist, word)
end
end
return wlist
end
-- Test whether a string could be separated by a char for multi-line rendering -- Test whether a string could be separated by a char for multi-line rendering
function util.isSplitable(c) function util.isSplitable(c)
return #c > 1 or c == " " or string.match(c, "%p") ~= nil return #c > 1 or c == " " or string.match(c, "%p") ~= nil

@ -37,4 +37,19 @@ describe("util module", function()
end end
assert.are_same(argv, {"./sdcv", "-nj", "words", "a lot", "more or less", "--data-dir=dict"}) assert.are_same(argv, {"./sdcv", "-nj", "words", "a lot", "more or less", "--data-dir=dict"})
end) end)
it("should split line into words", function()
local words = util.splitToWords("one two,three four . five")
assert.are_same(words, {
"one",
" ",
"two",
",",
"three",
" ",
"four",
" . ",
"five",
})
end)
end) end)

Loading…
Cancel
Save