From 611c423727bcc76b0211a42286e5cc0c4572b57b Mon Sep 17 00:00:00 2001 From: poire-z Date: Tue, 18 Feb 2020 13:24:10 +0100 Subject: [PATCH] TextBoxWidget: handle tabs and tabstops (#5870) Don't display a tofu glyph when meeting a tab (none of our fonts have a glyph for it). New parameter: TextBoxWidget.tabstop_nb_space_width, that defaults to 8, to ensure tabstops in the usual left aligmnent (or right when para is RTL). --- frontend/ui/widget/textboxwidget.lua | 117 ++++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 11 deletions(-) diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index b1017aac0..961d5b7e3 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -97,6 +97,9 @@ local TextBoxWidget = InputContainer:new{ -- used as a weak hint about direction) alignment_strict = false, -- true to force the alignemnt set by the alignment= attribute. -- When false, specified alignment is inverted when para direction is RTL + tabstop_nb_space_width = 8, -- unscaled_size_check: ignore + -- width of tabstops, as a factor of the width of a space + -- (set to 0 to disable any tab handling and display a tofu glyph) _xtext = nil, -- for internal use _alt_color_for_rtl = nil, -- (for debugging) draw LTR glyphs in black, RTL glyphs in gray } @@ -231,6 +234,10 @@ function TextBoxWidget:_measureWithXText() -- expected, got userdata", so we know) end self._xtext:measure() + if self.tabstop_nb_space_width > 0 and not self._tabstop_width then + local space_width = RenderText:sizeUtf8Text(0, false, self.face, " ").x + self._tabstop_width = self.tabstop_nb_space_width * space_width + end end -- Split the text into logical lines to fit into the text box. @@ -286,7 +293,7 @@ function TextBoxWidget:_splitToLines() if self.use_xtext then -- All of what's done below when use_xtext=false is done by the C++ module. - local line = self._xtext:makeLine(offset, targeted_width) + local line = self._xtext:makeLine(offset, targeted_width, false, self._tabstop_width) -- logger.dbg("makeLine", ln, line) -- We get a line such as this: -- { @@ -570,6 +577,87 @@ function TextBoxWidget:_shapeLine(line) end end + if xshaping.has_tabs and self.tabstop_nb_space_width > 0 then + -- Try to handle tabs: we got offset and end_offset to target the + -- expected width with tabstops applied on the logical order string. + -- We can really handle them correctly only with: + -- - pure LTR text and alignment=left + -- - pure RTL text and alignment=right + -- and with some possibly uneven spacing when text is justified. + -- Hopefully, we shouldn't use right or center with external text, + -- and our internal text is probably tab-free. + -- Note that tab is a Unicode SS (Segment Separator), so hopefully, + -- it seems to always get back the main direction of the paragraph + -- if text is Bidi - so our tabstops always fly in the main + -- paragraph direction. + -- When we can't do well, we let the tab char have its tofu glyph + -- width (which seems to be around the width of 4 spaces with our + -- fonts), and we just avoid displaying the glyph. So, there will + -- be enough spacing, but no tabstop alignment. + if alignment == "left" and not xshaping.para_is_rtl then + local last_tab = 0 + local pen_x = 0 + for i=1, #xshaping, 1 do + local xglyph = xshaping[i] + if xglyph.is_tab then + last_tab = i + local nb_tabstops_passed_by = math.floor(pen_x / self._tabstop_width) + local new_pen_x = (nb_tabstops_passed_by + 1) * self._tabstop_width + local this_tab_width = new_pen_x - pen_x + xshaping.width = xshaping.width - xglyph.x_advance + this_tab_width + xglyph.x_advance = this_tab_width + end + pen_x = pen_x + xglyph.x_advance + end + if last_tab > 0 and self.justified and line.can_be_justified then + -- Remove all can_extend before (on the left of) last tab + -- so justification does not affect tabstops + for i=1, last_tab, 1 do + local xglyph = xshaping[i] + if xglyph.can_extend then + xglyph.can_extend = false + xshaping.nb_can_extend = xshaping.nb_can_extend - 1 + end + if xglyph.can_extend_fallback then + xglyph.can_extend_fallback = false + xshaping.nb_can_extend_fallback = xshaping.nb_can_extend_fallback - 1 + end + end + end + elseif alignment == "right" and xshaping.para_is_rtl then + -- Similar, but scanning and using a pen_x from the right + local last_tab = 0 + local pen_x = 0 + for i=#xshaping, 1, -1 do + local xglyph = xshaping[i] + if xglyph.is_tab then + last_tab = i + local nb_tabstops_passed_by = math.floor(pen_x / self._tabstop_width) + local new_pen_x = (nb_tabstops_passed_by + 1) * self._tabstop_width + local this_tab_width = new_pen_x - pen_x + xshaping.width = xshaping.width - xglyph.x_advance + this_tab_width + xglyph.x_advance = this_tab_width + end + pen_x = pen_x + xglyph.x_advance + end + if last_tab > 0 and self.justified and line.can_be_justified then + -- Remove all can_extend before (on the right of) last tab + -- so justification does not affect tabstops + for i=#xshaping, last_tab, -1 do + local xglyph = xshaping[i] + if xglyph.can_extend then + xglyph.can_extend = false + xshaping.nb_can_extend = xshaping.nb_can_extend - 1 + end + if xglyph.can_extend_fallback then + xglyph.can_extend_fallback = false + xshaping.nb_can_extend_fallback = xshaping.nb_can_extend_fallback - 1 + end + end + end + end + end + local pen_x = 0 -- when alignment == "left" if alignment == "center" then pen_x = (line.targeted_width - line.width)/2 or 0 @@ -631,6 +719,11 @@ function TextBoxWidget:_shapeLine(line) -- We don't update/decrease prev_cluster_start_xglyph.x0, even if one of its glyph -- has a backward advance that go back the 1st glyph x0, to not mess positionning. end + if xglyph.is_tab then + xglyph.no_drawing = true + -- Note that xglyph.glyph=0 when no glyph was found in any font, + -- if we ever want to not draw them + end end line.x_end = pen_x line.xglyphs = xshaping @@ -669,7 +762,7 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx) if line.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) + line = self._xtext:makeLine(line.offset, line.targeted_width - ellipsis_width, false, self._tabstop_width) self.vertical_string_list[i] = line -- replace the former one end if line.end_offset and line.end_offset < #self._xtext then @@ -682,16 +775,18 @@ function TextBoxWidget:_renderText(start_row_idx, end_row_idx) self:_shapeLine(line) if line.xglyphs then -- non-empty line for __, xglyph in ipairs(line.xglyphs) do - local face = self.face.getFallbackFont(xglyph.font_num) -- callback (not a method) - local glyph = RenderText:getGlyphByIndex(face, xglyph.glyph, self.bold) - local color = self.fgcolor - if self._alt_color_for_rtl then - color = xglyph.is_rtl and Blitbuffer.COLOR_DARK_GRAY or Blitbuffer.COLOR_BLACK + if not xglyph.no_drawing then + local face = self.face.getFallbackFont(xglyph.font_num) -- callback (not a method) + local glyph = RenderText:getGlyphByIndex(face, xglyph.glyph, self.bold) + local color = self.fgcolor + if self._alt_color_for_rtl then + color = xglyph.is_rtl and Blitbuffer.COLOR_DARK_GRAY or Blitbuffer.COLOR_BLACK + end + self._bb:colorblitFrom(glyph.bb, + xglyph.x0 + glyph.l + xglyph.x_offset, + y - glyph.t + xglyph.y_offset, + 0, 0, glyph.bb:getWidth(), glyph.bb:getHeight(), color) end - self._bb:colorblitFrom(glyph.bb, - xglyph.x0 + glyph.l + xglyph.x_offset, - y - glyph.t + xglyph.y_offset, - 0, 0, glyph.bb:getWidth(), glyph.bb:getHeight(), color) end end y = y + self.line_height_px