From d601eabc4ad516f07266e6a809c3322b8b83933c Mon Sep 17 00:00:00 2001 From: poire-z Date: Sat, 21 Apr 2018 22:16:14 +0200 Subject: [PATCH] TextBoxWidget: optimize memory usage (#3904) By reducing the number of data structures, and avoiding nested tables, which seemed to impose a lot of work on the garbage collector. --- frontend/ui/widget/menu.lua | 4 +- frontend/ui/widget/textboxwidget.lua | 148 +++++++++++++++------------ 2 files changed, 83 insertions(+), 69 deletions(-) diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index 2a240e130..52e8dceac 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -273,14 +273,14 @@ function MenuItem:init() if item_name.vertical_string_list[lines + 1] then offset = item_name.vertical_string_list[lines + 1].offset - 2 else -- shouldn't happen, but just in case - offset = #item_name.char_width_list + offset = #item_name.charlist end local ellipsis_size = RenderText:sizeUtf8Text(0, self.content_width, Font:getFace(self.font, self.font_size), "…", true, self.bold).x local removed_char_width= 0 while removed_char_width < ellipsis_size do -- the width of each char has already been calculated by TextBoxWidget - removed_char_width = removed_char_width + item_name.char_width_list[offset].width + removed_char_width = removed_char_width + item_name:geCharWidth(offset) offset = offset - 1 end self.text = table.concat(item_name.charlist, '', 1, offset) .. "…" diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index 83308187a..efe6c27f8 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -33,9 +33,10 @@ local Screen = require("device").screen local TextBoxWidget = InputContainer:new{ text = nil, - charlist = nil, charpos = nil, - char_width_list = nil, -- list of widths of the chars in `charlist`. + charlist = nil, -- idx => char + char_width = nil, -- char => width + idx_pad = nil, -- idx => pad for char at idx, if non zero 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) @@ -128,18 +129,15 @@ function TextBoxWidget:_evalCharWidthList() self.charlist = util.splitToChars(self.text) 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 = 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 + -- get width of each distinct char + local char_width = {} + for _, c in ipairs(self.charlist) do + if not char_width[c] then + char_width[c] = RenderText:sizeUtf8Text(0, Screen:getWidth(), self.face, c, true, self.bold).x end - table.insert(self.char_width_list, {char = v, width = w, pad = 0}) - -- pad will be updated if we do text justification end + self.char_width = char_width + self.idx_pad = {} end -- Split the text into logical lines to fit into the text box. @@ -147,9 +145,9 @@ function TextBoxWidget:_splitCharWidthList() self.vertical_string_list = {} local idx = 1 - local size = #self.char_width_list + local size = #self.charlist local ln = 1 - local offset, cur_line_width, cur_line_text + local offset, end_offset, cur_line_width local lines_per_page if self.height then @@ -194,98 +192,99 @@ function TextBoxWidget:_splitCharWidthList() end end - offset = idx - -- Appending chars until the accumulated width exceeds `targeted_width`, + -- end_offset will be the idx of char at end of line + offset = idx -- idx of char at start of line + + -- We append 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 - local char_pads = nil while idx <= size do - if self.char_width_list[idx].char == "\n" then + if self.charlist[idx] == "\n" then hard_newline = true break end - cur_line_width = cur_line_width + self.char_width_list[idx].width + cur_line_width = cur_line_width + self.char_width[self.charlist[idx]] if cur_line_width > targeted_width then break else idx = idx + 1 end end if cur_line_width <= targeted_width then -- a hard newline or end of string - cur_line_text = table.concat(self.charlist, "", offset, idx - 1) + end_offset = idx - 1 else -- Backtrack the string until the length fit into one line. -- We'll give next and prev chars to isSplittable() for a wiser decision - local c = self.char_width_list[idx].char - local next_c = idx+1 <= size and self.char_width_list[idx+1].char or false - local prev_c = idx-1 >= 1 and self.char_width_list[idx-1].char or false + local c = self.charlist[idx] + local next_c = idx+1 <= size and self.charlist[idx+1] or false + local prev_c = idx-1 >= 1 and self.charlist[idx-1] or false local adjusted_idx = idx local adjusted_width = cur_line_width while adjusted_idx > offset and not util.isSplittable(c, next_c, prev_c) do - adjusted_width = adjusted_width - self.char_width_list[adjusted_idx].width + adjusted_width = adjusted_width - self.char_width[self.charlist[adjusted_idx]] adjusted_idx = adjusted_idx - 1 next_c = c c = prev_c - prev_c = adjusted_idx-1 >= 1 and self.char_width_list[adjusted_idx-1].char or false + prev_c = adjusted_idx-1 >= 1 and self.charlist[adjusted_idx-1] or false end if adjusted_idx == offset or adjusted_idx == idx then - -- either a very long english word ocuppying more than one line, + -- either a very long english word occupying more than one line, -- or the excessive char is itself splittable: -- we let that excessive char for next line if adjusted_idx == offset then -- let the fact a long word was splitted be known self.has_split_inside_word = true end - cur_line_text = table.concat(self.charlist, "", offset, idx - 1) - cur_line_width = cur_line_width - self.char_width_list[idx].width + end_offset = idx - 1 + cur_line_width = cur_line_width - self.char_width[self.charlist[idx]] elseif c == " " then -- we backtracked and we're below max width, but the last char -- is a space, we can ignore it - cur_line_text = table.concat(self.charlist, "", offset, adjusted_idx - 1) - cur_line_width = adjusted_width - self.char_width_list[adjusted_idx].width + end_offset = adjusted_idx - 1 + cur_line_width = adjusted_width - self.char_width[self.charlist[adjusted_idx]] idx = adjusted_idx + 1 else -- we backtracked and we're below max width, we can leave the -- splittable char on this line - cur_line_text = table.concat(self.charlist, "", offset, adjusted_idx) + end_offset = adjusted_idx cur_line_width = adjusted_width idx = adjusted_idx + 1 end if self.justified then -- 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 + -- we record in idx_pad the nb of pixels to add to each char + -- to make the whole line justified. This also helps hold + -- position accuracy. local fill_width = targeted_width - cur_line_width if fill_width > 0 then - local _, nbspaces = string.gsub(cur_line_text, " ", "") + local nbspaces = 0 + for sidx = offset, end_offset do + if self.charlist[sidx] == " " then + nbspaces = nbspaces + 1 + end + end if nbspaces > 0 then -- width added to all spaces local space_add_w = math.floor(fill_width / nbspaces) -- nb of spaces to which we'll add 1 more pixel local space_add1_nb = fill_width - space_add_w * nbspaces - char_pads = {} - for cidx = offset, idx-1 do - local pad = 0 - if self.char_width_list[cidx].char == " " then + for cidx = offset, end_offset do + local pad + if self.charlist[cidx] == " " then pad = space_add_w if space_add1_nb > 0 then pad = pad + 1 space_add1_nb = space_add1_nb - 1 end - -- Update pad info, help for hold position accuracy - self.char_width_list[cidx].pad = pad + if pad > 0 then self.idx_pad[cidx] = pad end end - table.insert(char_pads, pad) end else -- very long word, or CJK text with no space -- pad first chars with 1 pixel - char_pads = {} - for cidx = offset, idx-1 do - local pad = 0 + for cidx = offset, end_offset do if fill_width > 0 then - pad = 1 + self.idx_pad[cidx] = 1 fill_width = fill_width - 1 - -- Update pad info, help for hold position accuracy - self.char_width_list[cidx].pad = pad + else + break end - table.insert(char_pads, pad) end end end @@ -293,19 +292,18 @@ function TextBoxWidget:_splitCharWidthList() end -- endif cur_line_width > targeted_width if cur_line_width < 0 then break end self.vertical_string_list[ln] = { - text = cur_line_text, offset = offset, + end_offset = end_offset, width = cur_line_width, - char_pads = char_pads, } if hard_newline then idx = idx + 1 - -- FIXME: reuse newline entry - self.vertical_string_list[ln+1] = {text = "", offset = idx, width = 0} + -- end_offset = nil means no text + self.vertical_string_list[ln+1] = {offset = idx, end_offset = nil, 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 + if idx <= size and self.charlist[idx] == " " then idx = idx + 1 end end @@ -314,6 +312,24 @@ function TextBoxWidget:_splitCharWidthList() end end +function TextBoxWidget:_getLineText(vertical_string) + if not vertical_string.end_offset then return "" end + return table.concat(self.charlist, "", vertical_string.offset, vertical_string.end_offset) +end + +function TextBoxWidget:_getLinePads(vertical_string) + if not vertical_string.end_offset then return end + local pads = {} + for idx = vertical_string.offset, vertical_string.end_offset do + table.insert(pads, self.idx_pad[idx] or 0) + end + return pads +end + +function TextBoxWidget:geCharWidth(idx) + return self.char_width[self.charlist[idx]] +end + function TextBoxWidget:_renderText(start_row_idx, end_row_idx) local font_height = self.face.size if start_row_idx < 1 then start_row_idx = 1 end @@ -349,7 +365,7 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx) 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) + RenderText:renderUtf8Text(self._bb, pen_x, y, self.face, self:_getLineText(line), true, self.bold, self.fgcolor, nil, self:_getLinePads(line)) y = y + self.line_height_px end @@ -520,7 +536,7 @@ function TextBoxWidget:_findCharPos() local x = 0 local offset = self.vertical_string_list[ln].offset while offset < self.charpos do - x = x + self.char_width_list[offset].width + self.char_width_list[offset].pad + x = x + self.char_width[self.charlist[offset]] + (self.idx_pad[offset] or 0) offset = offset + 1 end return x + 1, (ln - 1) * self.line_height_px -- offset `x` by 1 to avoid overlap @@ -548,13 +564,13 @@ function TextBoxWidget:moveCursor(x, y) x = self.width end local offset = self.vertical_string_list[ln].offset - local idx = ln == #self.vertical_string_list and #self.char_width_list or self.vertical_string_list[ln + 1].offset - 1 + local idx = ln == #self.vertical_string_list and #self.charlist or self.vertical_string_list[ln + 1].offset - 1 while offset <= idx do - w = w + self.char_width_list[offset].width + self.char_width_list[offset].pad + w = w + self.char_width[self.charlist[offset]] + (self.idx_pad[offset] or 0) if w > x then break else offset = offset + 1 end end if w > x then - local w_prev = w - self.char_width_list[offset].width - self.char_width_list[offset].pad + local w_prev = w - self.char_width[self.charlist[offset]] - (self.idx_pad[offset] or 0) if x - w_prev < w - x then -- the previous one is more closer w = w_prev else @@ -722,7 +738,7 @@ function TextBoxWidget:onHoldWord(callback, ges) 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 + char_end = #self.charlist + 1 else char_end = self.vertical_string_list[line_num+1].offset end @@ -730,14 +746,13 @@ function TextBoxWidget:onHoldWord(callback, ges) 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 + c.pad + char_probe_x = char_probe_x + self.char_width[self.charlist[idx]] + (self.idx_pad[idx] or 0) if char_probe_x > x then -- ignore spaces - if c.char == " " then break end + if self.charlist[idx] == " " then break end -- now find which word the character is in - local words = util.splitToWords(line.text) + local words = util.splitToWords(self:_getLineText(line)) local probe_idx = char_start for _, w in ipairs(words) do -- +1 for word separtor @@ -900,7 +915,7 @@ function TextBoxWidget:_findWordEdge(x, y, side) 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 + char_end = #self.charlist + 1 else char_end = self.vertical_string_list[line_num+1].offset end @@ -909,12 +924,11 @@ function TextBoxWidget:_findWordEdge(x, y, side) 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 + c.pad + char_probe_x = char_probe_x + self.char_width[self.charlist[idx]] + (self.idx_pad[idx] or 0) 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) + local words = util.splitToWords(self:_getLineText(line)) -- words may contain separators (space, punctuation) : we don't -- discriminate here, it's the caller job to clean what was -- selected