TextBoxWidget: add new properties, use them in Menu

- height_adjust: if true, reduce height to a multiple of
  line_height (for nicer centering)
- height_overflow_show_ellipsis: if height overflow, append
  ellipsis to last shown line
(Implemented in both use_xtext and legacy code path.)

Use them in Menu.lua to clean up/shorten the code used for multiline
menu items by delegating the work to TextBoxWidget, or using
TextWidget when we end up needing only a single line.
pull/5605/head
poire-z 5 years ago
parent e699a1ee22
commit dc8696bd34

@ -179,6 +179,16 @@ function FileManagerMenu:setUpdateItemTable()
}
UIManager:show(items_font)
end
},
{
text = _("Reduce font size to show more text"),
keep_menu_open = true,
checked_func = function()
return G_reader_settings:isTrue("items_multilines_show_more_text")
end,
callback = function()
G_reader_settings:flipNilOrFalse("items_multilines_show_more_text")
end
}
}
}

@ -19,7 +19,6 @@ local InputContainer = require("ui/widget/container/inputcontainer")
local LeftContainer = require("ui/widget/container/leftcontainer")
local Math = require("optmath")
local OverlapGroup = require("ui/widget/overlapgroup")
local RenderText = require("ui/rendertext")
local RightContainer = require("ui/widget/container/rightcontainer")
local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget")
@ -174,6 +173,32 @@ function MenuItem:init()
}
end
local max_item_height = self.dimen.h - 2 * self.linesize
-- We want to show at least one line, so cap the provided font sizes
local max_font_size = TextBoxWidget:getFontSizeToFitHeight(max_item_height, 1)
if self.font_size > max_font_size then
self.font_size = max_font_size
end
if self.infont_size > max_font_size then
self.infont_size = max_font_size
end
local multilines_show_more_text = G_reader_settings:isTrue("items_multilines_show_more_text")
if not self.single_line and not multilines_show_more_text then
-- For non single line menus (File browser, Bookmarks), if the
-- user provided font size is large and would not allow showing
-- more than one line in our item height, just switch to single
-- line mode. This allows, when truncating, to take the full
-- width and cut inside a word to add the ellipsis - while in
-- multilines modes, with TextBoxWidget, words are wrapped to
-- follow line breaking rules, and the ellipsis might be placed
-- way earlier than the full width.
local min_font_size_2_lines = TextBoxWidget:getFontSizeToFitHeight(max_item_height, 2)
if self.font_size > min_font_size_2_lines then
self.single_line = true
end
end
-- State button and indentation for tree expand/collapse (for TOC)
local state_button_width = self.state_size.w or 0
local state_button = self.state or HorizontalSpan:new{
@ -191,6 +216,11 @@ function MenuItem:init()
}
}
-- Font for main text (may have its size decreased to make text fit)
self.face = Font:getFace(self.font, self.font_size)
-- Font for "mandatory" on the right
self.info_face = Font:getFace(self.infont, self.infont_size)
-- "mandatory" is the text on the right: file size, page number...
-- Padding before mandatory
local text_mandatory_padding = 0
@ -201,37 +231,36 @@ function MenuItem:init()
text_ellipsis_mandatory_padding = Size.span.horizontal_small
end
local mandatory = self.mandatory and ""..self.mandatory or ""
local mandatory_widget = TextWidget:new{
text = mandatory,
face = self.info_face,
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local mandatory_w = mandatory_widget:getWidth()
-- Font for main text (may have its size decreased to make text fit)
self.face = Font:getFace(self.font, self.font_size)
-- Font for "mandatory" on the right
self.info_face = Font:getFace(self.infont, self.infont_size)
local available_width = self.content_width - state_button_width - text_mandatory_padding - mandatory_w
local item_name
local mandatory_widget
-- Whether we show text on a single or multiple lines, we don't want it shortened
-- because of some \n that would push the following text on another line that would
-- overflow and not be displayed, or show a tofu char when displayed by TextWidget:
-- get rid of any \n (which could be found in highlighted text in bookmarks).
local text = self.text:gsub("\n", " ")
if self.single_line then -- items only in single line
mandatory_widget = TextWidget:new{
text = mandatory,
face = self.info_face,
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local mandatory_w = mandatory_widget:getWidth()
-- No font size change: text will be truncated if it overflows
-- (we give it a little more room if truncated for better visual
-- feeling - which might make it no more truncated, but well...)
local text_max_width_base = self.content_width - state_button_width - mandatory_w
local text_max_width = text_max_width_base - text_mandatory_padding
local text_max_width_if_ellipsis = text_max_width_base - text_ellipsis_mandatory_padding
item_name = TextWidget:new{
text = self.text,
text = text,
face = self.face,
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local w = item_name:getWidth()
if w > text_max_width then
if w > available_width then
-- We give it a little more room if truncated for better visual
-- feeling (which might make it no more truncated, but well...)
local text_max_width_if_ellipsis = available_width + text_mandatory_padding - text_ellipsis_mandatory_padding
item_name:setMaxWidth(text_max_width_if_ellipsis)
end
if self.align_baselines then -- Align baselines of text and mandatory
@ -250,175 +279,79 @@ function MenuItem:init()
}
end
end
else
while true do
-- Free previously made widgets to avoid memory leaks
if mandatory_widget then
mandatory_widget:free()
end
mandatory_widget = TextWidget:new {
text = mandatory,
face = Font:getFace(self.infont, self.infont_size),
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local height = mandatory_widget:getSize().h
if height < self.dimen.h - 2 * self.linesize then -- we fit !
break
end
-- Don't go too low
if self.infont_size < 12 then
break;
else
-- If we don't fit, decrease font size
self.infont_size = self.infont_size - 1
end
end
self.info_face = Font:getFace(self.infont, self.infont_size)
local mandatory_w = RenderText:sizeUtf8Text(0, self.dimen.w, self.info_face, "" .. mandatory, true, self.bold).x
local max_item_height = self.dimen.h - 2 * self.linesize
local flag_fit = false
local flag_add_ellipsis = false
local num_lines, offset
local item_name_orig = nil
-- first: try to decrease number of lines in TextBoxWidget
-- this loop ends only when text fits or we have only one line of text
while true do
-- Free previously made widgets to avoid memory leaks
elseif multilines_show_more_text then
-- Multi-lines, with font size decrease if needed to show more of the text.
-- It would be costly/slow with use_xtext if we were to try all
-- font sizes from self.font_size to min_font_size (12).
-- So, we try to optimize the search of the best font size.
logger.dbg("multilines_show_more_text menu item font sizing start")
local function make_item_name(font_size)
if item_name then
item_name:free()
end
logger.dbg("multilines_show_more_text trying font size", font_size)
item_name = TextBoxWidget:new {
text = self.text,
face = Font:getFace(self.font, self.font_size),
width = self.content_width - mandatory_w - state_button_width - text_mandatory_padding,
text = text,
face = Font:getFace(self.font, font_size),
width = available_width,
alignment = "left",
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
if not item_name_orig then
-- remember original item_name
item_name_orig = item_name
offset = #item_name.charlist
end
local height = item_name:getSize().h
if height < max_item_height then -- we fit !
flag_fit = true
break
end
flag_add_ellipsis = true
num_lines = item_name:getAllLineCount()
if num_lines == 1 then -- widget doesn't fit and we have only one line of text
break
end
-- remove last line and try again to fit
offset = item_name.vertical_string_list[num_lines].offset - 1
-- remove ending "\n" (new line) to prevent infinity loop
if item_name.charlist[offset] == "\n" then
offset = offset - 1
end
self.text = table.concat(item_name.charlist, "", 1, offset)
-- return true if we fit
return item_name:getSize().h <= max_item_height
end
-- second: decrease font size (we have now only one line)
while true and not flag_fit do
-- Free previously made widgets to avoid memory leaks
if item_name then
local min_font_size = 12
-- First, try with specified font size: short text might fit
if not make_item_name(self.font_size) then
-- It doesn't, try with min font size: very long text might not fit
if not make_item_name(min_font_size) then
-- Does not fit with min font size: keep widget with min_font_size, but
-- impose a max height to show only the first lines up to where it fits
item_name:free()
end
self.font_size = self.font_size - 1
item_name = TextBoxWidget:new{
text = self.text,
face = Font:getFace(self.font, self.font_size),
width = self.content_width - mandatory_w - state_button_width - text_mandatory_padding,
alignment = "left",
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
local height = item_name:getSize().h
if height < max_item_height then -- we fit !
offset = #item_name.charlist
break
end
-- if text is too height with that really small font we don't show text at all
-- this shouldn't happen
if self.font_size <= 8 then
item_name = TextBoxWidget:new{
text = "",
face = Font:getFace(self.font, self.font_size),
width = self.content_width - mandatory_w - state_button_width - text_mandatory_padding,
alignment = "left",
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
flag_add_ellipsis = false
break
end
end
-- add ellipsis when text was truncated
if flag_add_ellipsis then
local text_last_line
-- when lines is more than 1 we see only for last visible line
if num_lines > 1 then
local offset_prev = item_name_orig.vertical_string_list[num_lines - 1].offset
text_last_line = table.concat(item_name_orig.charlist, "", offset_prev, offset)
else
text_last_line = self.text
end
local text_size = RenderText:sizeUtf8Text(0, self.content_width,
Font:getFace(self.font, self.font_size), text_last_line, true, self.bold).x
local ellipsis_size = RenderText:sizeUtf8Text(0, self.content_width,
Font:getFace(self.font, self.font_size), "", true, self.bold).x
local text_size_increase = text_size
local max_offset = #item_name_orig.charlist
-- try to add chars to better align
while item_name.width > text_size_increase + ellipsis_size and offset < max_offset
and item_name_orig.charlist[offset] ~= "\n" do
text_size_increase = text_size_increase + item_name_orig:getCharWidth(offset + 1)
if text_size_increase + ellipsis_size < item_name.width then
-- add one char to text
offset = offset + 1
end
end
-- remove chars when text is too long
while item_name.width <= text_size + ellipsis_size do
text_size = text_size - item_name:getCharWidth(offset)
-- remove one char from text
offset = offset - 1
end
if offset == max_offset then
-- when finally after manipulation we have all original text we don't need to add ellipsis
self.text = table.concat(item_name_orig.charlist, "", 1, offset)
item_name.height = max_item_height
item_name.height_adjust = true
item_name.height_overflow_show_ellipsis = true
item_name:init()
else
-- remove ending '\n' (new line) to prevent increase number of lines
if item_name_orig.charlist[offset] == "\n" then
offset = offset - 1
-- Text fits with min font size: try to find some larger
-- font size in between that make text fit, with some
-- binary search to limit the number of checks.
local bad_font_size = self.font_size
local good_font_size = min_font_size
local item_name_is_good = true
while true do
local test_font_size = math.floor((good_font_size + bad_font_size) / 2)
if test_font_size == good_font_size then -- +1 would be bad_font_size
if not item_name_is_good then
make_item_name(good_font_size)
end
break
end
if make_item_name(test_font_size) then
good_font_size = test_font_size
item_name_is_good = true
else
bad_font_size = test_font_size
item_name_is_good = false
end
end
-- add ellipsis to show that text was truncated
self.text = table.concat(item_name_orig.charlist, "", 1, offset) .. ""
end
if item_name then
item_name:free()
end
if item_name_orig then
item_name_orig:free()
end
--final item_name that fits
item_name = TextBoxWidget:new {
text = self.text,
face = Font:getFace(self.font, self.font_size),
width = self.content_width - mandatory_w - state_button_width - text_mandatory_padding,
alignment = "left",
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
end
self.face = Font:getFace(self.font, self.font_size)
else
-- Multi-lines, with fixed user provided font size
item_name = TextBoxWidget:new {
text = text,
face = self.face,
width = available_width,
height = max_item_height,
height_adjust = true,
height_overflow_show_ellipsis = true,
alignment = "left",
bold = self.bold,
fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
}
end
local text_container = LeftContainer:new{

@ -43,6 +43,8 @@ local TextBoxWidget = InputContainer:new{
fgcolor = Blitbuffer.COLOR_BLACK,
width = Screen:scaleBySize(400), -- in pixels
height = nil, -- nil value indicates unscrollable text widget
height_adjust = false, -- if true, reduce height to a multiple of line_height (for nicer centering)
height_overflow_show_ellipsis = false, -- if height overflow, append ellipsis to last shown line
top_line_num = nil, -- original virtual_line_num to scroll to
charpos = nil, -- idx of char to draw the cursor on its left (can exceed #charlist by 1)
@ -131,6 +133,15 @@ function TextBoxWidget:init()
self.text_height = self.lines_per_page * self.line_height_px
self.virtual_line_num = 1
else
if self.height_overflow_show_ellipsis and #self.vertical_string_list > self.lines_per_page then
self.line_with_ellipsis = self.lines_per_page
end
if self.height_adjust then
self.height = self.text_height
if #self.vertical_string_list < self.lines_per_page then
self.height = #self.vertical_string_list * self.line_height_px
end
end
-- Show the previous displayed area in case of re-init (focus/unfocus)
-- InputText may have re-created us, while providing the previous charlist,
-- charpos and top_line_num.
@ -448,7 +459,8 @@ function TextBoxWidget:_shapeLine(line)
-- Get glyphs, shaped and possibly substituted by Harfbuzz and re-ordered by FriBiDi.
-- We'll add to 'line' this table of glyphs, with some additional
-- computed x and advance keys
local xshaping = self._xtext:shapeLine(line.offset, line.end_offset)
local xshaping = self._xtext:shapeLine(line.offset, line.end_offset,
line.idx_to_substitute_with_ellipsis)
-- logger.dbg(xshaping)
-- We get an array of tables looking like this:
-- [1] = {
@ -594,6 +606,23 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx)
if self.use_xtext then
for i = start_row_idx, end_row_idx do
local line = self.vertical_string_list[i]
if self.line_with_ellipsis and i == self.line_with_ellipsis and not line.ellipsis_added then
-- Requested to add an ellipsis on this line
local ellipsis_width = RenderText:getEllipsisWidth(self.face)
-- no bold: xtext does synthetized bold with normal metrics
if line.width + ellipsis_width > line.targeted_width then
-- The ellipsis would overflow: we need to re-makeLine()
-- this line with a smaller targeted_width
line = self._xtext:makeLine(line.offset, line.targeted_width - ellipsis_width)
self.vertical_string_list[i] = line -- replace the former one
end
if line.end_offset and line.end_offset < #self._xtext then
-- We'll have shapeLine add the ellipsis to the returned glyphs
line.end_offset = line.end_offset + 1
line.idx_to_substitute_with_ellipsis = line.end_offset
end
line.ellipsis_added = true -- No need to redo it next time
end
self:_shapeLine(line)
if line.xglyphs then -- non-empty line
for __, xglyph in ipairs(line.xglyphs) do
@ -628,7 +657,19 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx)
end
-- Note: we use kerning=true in all RenderText calls
-- (But kerning should probably not be used with monospaced fonts.)
RenderText:renderUtf8Text(self._bb, pen_x, y, self.face, self:_getLineText(line), true, self.bold, self.fgcolor, nil, self:_getLinePads(line))
local line_text = self:_getLineText(line)
if self.line_with_ellipsis and i == self.line_with_ellipsis then
-- Requested to add an ellipsis on this line
local ellipsis_width = RenderText:getEllipsisWidth(self.face, self.bold)
if line.width + ellipsis_width > self.width then
-- We could try to find the last break point (space, CJK) to
-- truncate there and add the ellipsis, but well...
line_text = RenderText:truncateTextByWidth(line_text, self.face, self.width, true, self.bold)
else
line_text = line_text .. ""
end
end
RenderText:renderUtf8Text(self._bb, pen_x, y, self.face, line_text, true, self.bold, self.fgcolor, nil, self:_getLinePads(line))
y = y + self.line_height_px
end
@ -813,6 +854,25 @@ function TextBoxWidget:getVisibleHeightRatios()
return low, high
end
-- Helper function to be used before intanstiating a TextBoxWidget instance
function TextBoxWidget:getFontSizeToFitHeight(height_px, nb_lines, line_height_em)
-- Get a font size that would fit nb_lines in height_px.
-- A font with the returned size should then be provided
-- to TextBoxWidget:new() (as well as the line_height_em given
-- here, as the line_height= property, if not the default).
if not nb_lines then
nb_lines = 1 -- default to 1 line
end
if not line_height_em then
line_height_em = self.line_height -- (TextBoxWidget default above: 0.3)
end
-- We do the revert of what's done in :init():
-- self.line_height_px = Math.round( (1 + self.line_height) * self.face.size )
local font_size = height_px / nb_lines / (1 + line_height_em)
font_size = font_size * 1000000 / Screen:scaleBySize(1000000) -- invert scaleBySize
return math.floor(font_size)
end
function TextBoxWidget:getCharPos()
-- returns virtual_line_num too
return self.charpos, self.virtual_line_num

Loading…
Cancel
Save