@ -13,17 +13,24 @@ Example:
] ]
local Blitbuffer = require ( " ffi/blitbuffer " )
local Device = require ( " device " )
local Font = require ( " ui/font " )
local FrameContainer = require ( " ui/widget/container/framecontainer " )
local Geom = require ( " ui/geometry " )
local GestureRange = require ( " ui/gesturerange " )
local InputContainer = require ( " ui/widget/container/inputcontainer " )
local LineWidget = require ( " ui/widget/linewidget " )
local RenderText = require ( " ui/rendertext " )
local RightContainer = require ( " ui/widget/container/rightcontainer " )
local Size = require ( " ui/size " )
local TextWidget = require ( " ui/widget/textwidget " )
local TimeVal = require ( " ui/timeval " )
local Widget = require ( " ui/widget/widget " )
local UIManager = require ( " ui/uimanager " )
local logger = require ( " logger " )
local util = require ( " util " )
local Screen = require ( " device " ) . screen
local TextBoxWidget = Widget : new {
local TextBoxWidget = InputContainer : new {
text = nil ,
charlist = nil ,
charpos = nil ,
@ -31,6 +38,7 @@ local TextBoxWidget = Widget:new{
vertical_string_list = nil ,
editable = false , -- Editable flag for whether drawing the cursor or not.
justified = false , -- Should text be justified (spaces widened to fill width)
alignment = " left " , -- or "center", "right"
cursor_line = nil , -- LineWidget to draw the vertical cursor.
face = nil ,
bold = nil ,
@ -40,6 +48,30 @@ local TextBoxWidget = Widget:new{
height = nil , -- nil value indicates unscrollable text widget
virtual_line_num = 1 , -- used by scroll bar
_bb = nil ,
-- We can provide a list of images: each image will be displayed on each
-- scrolled page, in its top right corner (if more images than pages, remaining
-- images will not be displayed at all - if more pages than images, remaining
-- pages won't have any image).
-- Each 'image' is a table with the following keys:
-- width width of small image displayed by us
-- height height of small image displayed by us
-- bb blitbuffer of small image, may be initially nil
-- optional:
-- hi_width same as previous for a high-resolution version of the
-- hi_height image, to be displayed by ImageViewer when Hold on
-- hi_bb the low-resolution image
-- title ImageViewer title
-- caption ImageViewer caption
--
-- load_bb_func function called (with one arg: false to load 'bb', true to load 'hi_bb)
-- when bb or hi_bb is nil: its job is to load/build bb or hi_bb.
-- The page will refresh itself when load_bb_func returns.
images = nil , -- list of such images
line_num_to_image = nil , -- will be filled by self:_splitCharWidthList()
image_padding_left = Screen : scaleBySize ( 10 ) ,
image_padding_bottom = Screen : scaleBySize ( 3 ) ,
image_alt_face = Font : getFace ( " xx_smallinfofont " ) ,
image_alt_fgcolor = Blitbuffer.COLOR_BLACK ,
}
function TextBoxWidget : init ( )
@ -55,6 +87,10 @@ function TextBoxWidget:init()
if self.height == nil then
self : _renderText ( 1 , # self.vertical_string_list )
else
-- luajit may segfault if we were provided with a negative height
if self.height < 0 then
self.height = 0
end
self : _renderText ( 1 , self : getVisLineCount ( ) )
end
if self.editable then
@ -63,6 +99,16 @@ function TextBoxWidget:init()
self.cursor_line : paintTo ( self._bb , x , y )
end
self.dimen = Geom : new ( self : getSize ( ) )
if Device : isTouchDevice ( ) then
self.ges_events = {
TapImage = {
GestureRange : new {
ges = " tap " ,
range = function ( ) return self.dimen end ,
} ,
} ,
}
end
end
function TextBoxWidget : unfocus ( )
@ -103,9 +149,52 @@ function TextBoxWidget:_splitCharWidthList()
local size = # self.char_width_list
local ln = 1
local offset , cur_line_width , cur_line_text
local lines_per_page
if self.height then
lines_per_page = self : getVisLineCount ( )
end
local image_num = 0
local targeted_width = self.width
local image_lines_remaining = 0
while idx <= size do
-- Every scrolled page, we want to add the next (if any) image at its top right
-- (if not scrollable, we will display only the first image)
-- We need to make shorter lines and leave room for the image
if self.images and # self.images > 0 then
if self.line_num_to_image == nil then
self.line_num_to_image = { }
end
if ( lines_per_page and ln % lines_per_page == 1 ) -- first line of a scrolled page
or ( lines_per_page == nil and ln == 1 ) then -- first line if not scrollabled
image_num = image_num + 1
if image_num <= # self.images then
local image = self.images [ image_num ]
self.line_num_to_image [ ln ] = image
-- Resize image if really too big: bb will be cropped if already there,
-- but if loaded later with load_bb_func, load_bb_func may resize it
-- to the width and height we have updated here.
if image.width > self.width / 2 then
image.height = math.floor ( image.height * ( self.width / 2 / image.width ) )
image.width = math.floor ( self.width / 2 )
end
if image.height > self.height / 2 then
image.width = math.floor ( image.width * ( self.height / 2 / image.height ) )
image.height = math.floor ( self.height / 2 )
end
targeted_width = self.width - image.width - self.image_padding_left
image_lines_remaining = math.ceil ( ( image.height + self.image_padding_bottom ) / self.line_height_px )
end
end
if image_lines_remaining > 0 then
image_lines_remaining = image_lines_remaining - 1
else
targeted_width = self.width -- text can now use full width
end
end
offset = idx
-- Appending chars until the accumulated width exceeds `self.width`,
-- Appending chars until the accumulated width exceeds ` targeted_ width`,
-- or a newline occurs, or no more chars to consume.
cur_line_width = 0
local hard_newline = false
@ -116,9 +205,9 @@ function TextBoxWidget:_splitCharWidthList()
break
end
cur_line_width = cur_line_width + self.char_width_list [ idx ] . width
if cur_line_width > self. width then break else idx = idx + 1 end
if cur_line_width > targeted_ width then break else idx = idx + 1 end
end
if cur_line_width <= self. width then -- a hard newline or end of string
if cur_line_width <= targeted_ width then -- a hard newline or end of string
cur_line_text = table.concat ( self.charlist , " " , offset , idx - 1 )
else
-- Backtrack the string until the length fit into one line.
@ -161,7 +250,7 @@ function TextBoxWidget:_splitCharWidthList()
-- this line was splitted and can be justified
-- we build a list of char_pads, pixels to add to some chars to make the
-- whole line justified
local fill_width = self. width - cur_line_width
local fill_width = targeted_ width - cur_line_width
if fill_width > 0 then
local _ , nbspaces = string.gsub ( cur_line_text , " " , " " )
if nbspaces > 0 then
@ -200,7 +289,7 @@ function TextBoxWidget:_splitCharWidthList()
end
end
end
end -- endif cur_line_width > self. width
end -- endif cur_line_width > targeted_ width
if cur_line_width < 0 then break end
self.vertical_string_list [ ln ] = {
text = cur_line_text ,
@ -229,23 +318,186 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx)
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
local row_count = end_row_idx == 0 and 1 or end_row_idx - start_row_idx + 1
local h = self.line_height_px * row_count
-- We need a bb with the full height (even if we display only a few lines, we
-- may have to draw an image bigger than these lines)
local h = self.height or self.line_height_px * row_count
if self._bb then self._bb : free ( ) end
self._bb = Blitbuffer.new ( self.width , h )
local bbtype = nil
if self.line_num_to_image and self.line_num_to_image [ start_row_idx ] then
-- Whether Screen:isColorEnabled() or not, it's best to always use BBRGB32
-- and alphablitFrom() for the best display of various images:
-- With greyscale screen TYPE_BB8 (the default, and what we would
-- have chosen when not Screen:isColorEnabled()):
-- alphablitFrom: some images are all white (ex: flags on Milan, Ilkhanides on wiki.fr)
-- blitFrom: some images have a black background (ex: RDA, Allemagne on wiki.fr)
-- With TYPE_BBRGB32:
-- blitFrom: some images have a black background (ex: RDA, Allemagne on wiki.fr)
-- alphablitFrom: all these images looks good, with a white background
bbtype = Blitbuffer.TYPE_BBRGB32
end
self._bb = Blitbuffer.new ( self.width , h , bbtype )
self._bb : fill ( Blitbuffer.COLOR_WHITE )
local y = font_height
for i = start_row_idx , end_row_idx do
local line = self.vertical_string_list [ i ]
local pen_x = self.alignment == " center " and ( self.width - line.width ) / 2 or 0
--@TODO Don't use kerning for monospaced fonts. (houqp)
local pen_x = 0 -- when alignment == "left"
if self.alignment == " center " then
pen_x = ( self.width - line.width ) / 2 or 0
elseif self.alignment == " right " then
pen_x = ( self.width - line.width )
end
--@todo don't use kerning for monospaced fonts. (houqp)
-- refert to cb25029dddc42693cc7aaefbe47e9bd3b7e1a750 in master tree
RenderText : renderUtf8Text ( self._bb , pen_x , y , self.face , line.text , true , self.bold , self.fgcolor , nil , line.char_pads )
y = y + self.line_height_px
end
-- -- if text is shorter than one line, shrink to text's width
-- if #v_list == 1 then
-- self.width = pen_x
-- end
-- Render image if any
self : _renderImage ( start_row_idx )
end
function TextBoxWidget : _renderImage ( start_row_idx )
local scheduled_update = self.scheduled_update
self.scheduled_update = nil -- reset it, so we don't have to whenever we return below
if not self.line_num_to_image or not self.line_num_to_image [ start_row_idx ] then
return -- no image on this page
end
local image = self.line_num_to_image [ start_row_idx ]
local do_schedule_update = false
local display_bb = false
local display_alt = false
local status_text = nil
local alt_text = image.title or " "
if image.caption then
alt_text = alt_text .. " \n " .. image.caption
end
-- Decide what to do/display
if image.bb then -- we have a bb
if scheduled_update then -- we're called from a scheduled update
display_bb = true -- display the bb we got
else
-- not from a scheduled update, but update from Tap on image
-- or we are back to this page from another one
if self.image_show_alt_text then
display_alt = true -- display alt_text
else
display_bb = true -- display the bb we have
end
end
else -- no bb yet
display_alt = true -- nothing else to display but alt_text
if scheduled_update then -- we just failed loading a bb in a scheduled update
status_text = " ⚠ " -- show a warning triangle below alt_text
else
-- initial display of page (or back on it and previous
-- load_bb_func failed: try it again)
if image.load_bb_func then -- we can load a bb
do_schedule_update = true -- load it and call us again
status_text = " ♲ " -- display loading recycle sign below alt_text
end
end
end
-- logger.dbg("display_bb:", display_bb, "display_alt", display_alt, "status_text:", status_text, "do_schedule_update:", do_schedule_update)
-- Do what's been decided
if display_bb then
self._bb : alphablitFrom ( image.bb , self.width - image.width , 0 )
end
local status_height = 0
if status_text then
local status_widget = TextWidget : new {
text = status_text ,
face = Font : getFace ( " cfont " , 20 ) ,
fgcolor = Blitbuffer.COLOR_GREY ,
bold = true ,
}
status_height = status_widget : getSize ( ) . h
status_widget = FrameContainer : new {
background = Blitbuffer.COLOR_WHITE ,
bordersize = 0 ,
margin = 0 ,
padding = 0 ,
RightContainer : new {
dimen = {
w = image.width ,
h = status_height ,
} ,
status_widget ,
} ,
}
status_widget : paintTo ( self._bb , self.width - image.width , image.height - status_height )
status_widget : free ( )
end
if display_alt then
local alt_widget = TextBoxWidget : new {
text = alt_text ,
face = self.image_alt_face ,
fgcolor = self.image_alt_fgcolor ,
width = image.width ,
-- don't draw over status_text if any
height = math.max ( 0 , image.height - status_height ) ,
}
alt_widget : paintTo ( self._bb , self.width - image.width , 0 )
alt_widget : free ( )
end
if do_schedule_update then
if self.image_update_action then
-- Cancel any previous one, if we changed page quickly
UIManager : unschedule ( self.image_update_action )
end
-- Remember on which page we were launched, so we can
-- abort if page has changed
local scheduled_for_linenum = start_row_idx
self.image_update_action = function ( )
self.image_update_action = nil
if scheduled_for_linenum ~= self.virtual_line_num then
return -- no more on this page
end
local dismissed = image.load_bb_func ( ) -- will update self.bb (or not if failure)
if dismissed then
-- If dismissed, the dismiss event may be resent, we
-- may soon just go display another page. So delay this update a
-- bit to see if that happened
UIManager : scheduleIn ( 0.1 , function ( )
if scheduled_for_linenum == self.virtual_line_num then
-- we are still on the same page
self : update ( true )
UIManager : setDirty ( " all " , function ( )
-- return "ui", self.dimen
-- We can refresh only the image area, even if we have just
-- re-rendered the whole textbox as the text has been
-- rendered just the same as it was
return " ui " , Geom : new {
x = self.dimen . x + self.width - image.width ,
y = self.dimen . y ,
w = image.width ,
h = image.height ,
}
end )
end
end )
else
-- Image loaded (or not if failure): call us again
-- with scheduled_update = true so we can draw what we got
self : update ( true )
UIManager : setDirty ( " all " , function ( )
-- return "ui", self.dimen
-- We can refresh only the image area, even if we have just
-- re-rendered the whole textbox as the text has been
-- rendered just the same as it was
return " ui " , Geom : new {
x = self.dimen . x + self.width - image.width ,
y = self.dimen . y ,
w = image.width ,
h = image.height ,
}
end )
end
end
-- Wrap it with Trapper, as load_bb_func may be using some of its
-- dismissable methods
local Trapper = require ( " ui/trapper " )
UIManager : scheduleIn ( 0.1 , function ( ) Trapper : wrap ( self.image_update_action ) end )
end
end
-- Return the position of the cursor corresponding to `self.charpos`,
@ -323,9 +575,49 @@ function TextBoxWidget:getAllLineCount()
return # self.vertical_string_list
end
function TextBoxWidget : update ( scheduled_update )
self : free ( )
-- We set this flag so :_renderText() can know we were called from a
-- scheduled update and so not schedule another one
self.scheduled_update = scheduled_update
self : _renderText ( self.virtual_line_num , self.virtual_line_num + self : getVisLineCount ( ) - 1 )
self.scheduled_update = nil
end
function TextBoxWidget : onTapImage ( arg , ges )
if self.line_num_to_image and self.line_num_to_image [ self.virtual_line_num ] then
local image = self.line_num_to_image [ self.virtual_line_num ]
local tap_x = ges.pos . x - self.dimen . x
local tap_y = ges.pos . y - self.dimen . y
-- Check that this tap is on this image
if tap_x > self.width - image.width and tap_x < self.width and
tap_y > 0 and tap_y < image.height then
logger.dbg ( " tap on image " )
if image.bb then
-- Toggle between image and alt_text
self.image_show_alt_text = not self.image_show_alt_text
self : update ( )
UIManager : setDirty ( " all " , function ( )
-- return "ui", self.dimen
-- We can refresh only the image area, even if we have just
-- re-rendered the whole textbox as the text has been
-- rendered just the same as it was
return " ui " , Geom : new {
x = self.dimen . x + self.width - image.width ,
y = self.dimen . y ,
w = image.width ,
h = image.height ,
}
end )
return true
end
end
end
end
-- TODO: modify `charpos` so that it can render the cursor
function TextBoxWidget : scrollDown ( )
self.image_show_alt_text = nil -- reset image bb/alt state
local visible_line_count = self : getVisLineCount ( )
if self.virtual_line_num + visible_line_count <= # self.vertical_string_list then
self : free ( )
@ -337,6 +629,7 @@ end
-- TODO: modify `charpos` so that it can render the cursor
function TextBoxWidget : scrollUp ( )
self.image_show_alt_text = nil
local visible_line_count = self : getVisLineCount ( )
if self.virtual_line_num > 1 then
self : free ( )
@ -384,6 +677,15 @@ function TextBoxWidget:paintTo(bb, x, y)
end
function TextBoxWidget : free ( )
logger.dbg ( " TextBoxWidget:free called " )
-- :free() is called when our parent widget is closing, and
-- here whenever :_renderText() is being called, to display
-- a new page: cancel any scheduled image update, as it
-- is no more related to current page
if self.image_update_action then
logger.dbg ( " TextBoxWidget:free: cancelling self.image_update_action " )
UIManager : unschedule ( self.image_update_action )
end
if self._bb then
self._bb : free ( )
self._bb = nil
@ -473,6 +775,41 @@ function TextBoxWidget:onHoldReleaseText(callback, ges)
local hold_duration = TimeVal.now ( ) - self.hold_start_tv
hold_duration = hold_duration.sec + hold_duration.usec / 1000000
-- If page contains an image, check if Hold is on this image and deal
-- with it directly
if self.line_num_to_image and self.line_num_to_image [ self.virtual_line_num ] then
local image = self.line_num_to_image [ self.virtual_line_num ]
if hold_end_x > self.width - image.width and hold_end_y < image.height then
-- Only if low-res image is loaded, so we have something to display
-- if high-res loading is not implemented or if its loading fails
if image.bb then
logger.dbg ( " hold on image " )
local load_and_show_image = function ( )
if not image.hi_bb and image.load_bb_func then
image.load_bb_func ( true ) -- load high res image if implemented
end
-- display hi_bb, or low-res bb if hi_bb has not been
-- made (if not implemented, or failed, or dismissed)
local ImageViewer = require ( " ui/widget/imageviewer " )
local imgviewer = ImageViewer : new {
image = image.hi_bb or image.bb , -- fallback to low-res if high-res failed
image_disposable = false , -- we may re-use our bb if called again
with_title_bar = true ,
title_text = image.title ,
caption = image.caption ,
fullscreen = true ,
}
UIManager : show ( imgviewer )
end
-- Wrap it with Trapper, as load_bb_func may be using some of its
-- dismissable methods
local Trapper = require ( " ui/trapper " )
UIManager : scheduleIn ( 0.1 , function ( ) Trapper : wrap ( load_and_show_image ) end )
-- And we return without calling the "Hold on text" callback
return
end
end
end
-- Swap start and end if needed
local x0 , y0 , x1 , y1
-- first, sort by y/line_num