diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index 6453bf879..1d8537e52 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -447,15 +447,46 @@ end -- XText: shape a line into positionned glyphs function TextBoxWidget:_shapeLine(line) -- line is an item from self.vertical_string_list - if not line.end_offset then - return -- empty line (hard newline at end of file) - end - if line.end_offset < line.offset then - return -- empty line (hard newline while not at end of file) - end - if line.xglyphs then + if line._shaped then return -- already done end + line._shaped = true + if not line.end_offset or line.end_offset < line.offset then + -- Empty line (first check above is for hard newline at end of file, + -- second check is for hard newline while not at end of file). + -- We need to set a direction on this line, so the cursor can be + -- positionned accordingly, on the left or on the right of the line + -- (for convenience, we have an empty line inherit the direction + -- of the previous line if non-empty) + local offset = line.offset + if not line.end_offset then -- last line with offset=#text+1 + if offset > 1 then -- non empty text: get it from last char + offset = offset - 1 + else + offset = nil -- no text: get _xtext specified or default direction + end + end + local para_dir_rtl, prev_char_para_dir_rtl = self._xtext:getParaDirection(offset) + line.para_is_rtl = para_dir_rtl or prev_char_para_dir_rtl + -- We also need to set x_start & x_end (similar to how we do it below) + local alignment = self.alignment + if not self.alignment_strict and line.para_is_rtl then + if alignment == "left" then + alignment = "right" + elseif alignment == "right" then + alignment = "left" + end + end + local pen_x = 0 -- when alignment == "left" + if alignment == "center" then + pen_x = line.targeted_width / 2 + elseif alignment == "right" then + pen_x = line.targeted_width + end + line.x_start = pen_x + line.x_end = pen_x + return + end -- 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 @@ -469,6 +500,7 @@ function TextBoxWidget:_shapeLine(line) -- ["can_extend"] = false, -- ["can_extend_fallback"] = false, -- ["is_rtl"] = false, + -- ["bidi_level"] = 0, -- ["text_index"] = 1, -- ["glyph"] = 68, -- ["font_num"] = 0, @@ -483,6 +515,7 @@ function TextBoxWidget:_shapeLine(line) -- ["can_extend"] = false, -- ["can_extend_fallback"] = false, -- ["is_rtl"] = true, + -- ["bidi_level"] = 1, -- ["text_index"] = 8, -- ["glyph"] = 1292, -- ["font_num"] = 3, @@ -496,6 +529,7 @@ function TextBoxWidget:_shapeLine(line) -- ["can_extend"] = false, -- ["can_extend_fallback"] = false, -- ["is_rtl"] = true, + -- ["bidi_level"] = 1, -- ["text_index"] = 8, -- ["glyph"] = 1321, -- ["font_num"] = 3, @@ -549,6 +583,7 @@ function TextBoxWidget:_shapeLine(line) end end + line.x_start = pen_x local prev_cluster_start_xglyph for i, xglyph in ipairs(xshaping) do xglyph.x0 = pen_x @@ -568,7 +603,7 @@ function TextBoxWidget:_shapeLine(line) -- with advance or zero-advance...), glyphs may not always be fine to position -- the cursor caret. For X/Y/Charpos positionning/guessing, we'll ignore -- glyphs that are not cluster_start, and we build here the full cluster x0/x1/w - -- by mergin them from all glyphs part of this cluster + -- by merging them from all glyphs part of this cluster if xglyph.is_cluster_start then prev_cluster_start_xglyph = xglyph else @@ -580,7 +615,10 @@ function TextBoxWidget:_shapeLine(line) -- has a backward advance that go back the 1st glyph x0, to not mess positionning. end end + line.x_end = pen_x line.xglyphs = xshaping + -- (Copy para_is_rtl up into 'line', where empty lines without xglyphs have it) + line.para_is_rtl = line.xglyphs.para_is_rtl --- @todo Should we drop these when no more displayed in the page to reclaim memory, -- at the expense of recomputing it when back to this page? end @@ -1116,18 +1154,36 @@ function TextBoxWidget:_getXYForCharPos(charpos) end end local y = (ln - self.virtual_line_num) * self.line_height_px + -- Find the x offset in the current line. - local x = 0 if self.use_xtext then local line = self.vertical_string_list[ln] self:_shapeLine(line) + local x = line.x_start -- used if empty line (line.x_start = line.x_end) if line.xglyphs then -- non-empty line + -- If charpos is the end of the logical order line, it may not be at end of + -- visual line (it might be at start, or even in the middle, with bidi!) + local is_after_last_char = charpos > line.end_offset + if is_after_last_char then + -- Find the last char that is really part of this line + charpos = line.end_offset + end for i, xglyph in ipairs(line.xglyphs) do if xglyph.is_cluster_start then -- ignore non-start cluster glyphs if charpos >= xglyph.text_index and charpos < xglyph.text_index + xglyph.cluster_len then + -- Correct glyph found + if is_after_last_char then + -- Draw on the right of this glyph if LTR, on the left if RTL + if xglyph.is_rtl then + x = xglyph.x0 + else + x = xglyph.x1 + end + break + end --- @todo Be more clever with RTL, and at bidi boundaries, - -- may be depending on line.xglyphs.para_is_rtl. + -- may be depending on line.para_is_rtl and xglyph.bidi_level if xglyph.is_rtl then x = xglyph.x1 -- draw cursor on the right of this RTL glyph else @@ -1146,8 +1202,6 @@ function TextBoxWidget:_getXYForCharPos(charpos) break end x = xglyph.x1 - --- @todo When line.xglyphs.para_is_rtl and no x found, it should - -- be the first line glyph's x0 end end end @@ -1157,6 +1211,7 @@ function TextBoxWidget:_getXYForCharPos(charpos) -- Only when not self.use_xtext: + local x = 0 local offset = self.vertical_string_list[ln].offset local nbchars = #self.charlist while offset < charpos do @@ -1185,13 +1240,6 @@ function TextBoxWidget:getCharPosAtXY(x, y) elseif ln > #self.vertical_string_list then return #self.charlist + 1 -- return end of last line end - if x > self.vertical_string_list[ln].width then -- no need to loop thru chars - local pos = self.vertical_string_list[ln].end_offset - if not pos then -- empty last line - return self.vertical_string_list[ln].offset - end - return pos + 1 -- after last char - end local idx = self.vertical_string_list[ln].offset local end_offset = self.vertical_string_list[ln].end_offset if not end_offset then -- empty line @@ -1201,7 +1249,21 @@ function TextBoxWidget:getCharPosAtXY(x, y) if self.use_xtext then local line = self.vertical_string_list[ln] self:_shapeLine(line) - --- @todo Probably some specific/inverted work if line.xglyphs.para_is_rtl + -- If before start of line or after end of line, no need to loop thru chars + -- (we return line.end_offset+1 to be after last char) + if x <= line.x_start then + if line.para_is_rtl then + return line.end_offset and line.end_offset + 1 or line.offset + else + return line.offset + end + elseif x > line.x_end then + if line.para_is_rtl then + return line.offset + else + return line.end_offset and line.end_offset + 1 or line.offset + end + end if line.xglyphs then -- non-empty line for i, xglyph in ipairs(line.xglyphs) do if xglyph.is_cluster_start then -- ignore non-start cluster glyphs @@ -1232,6 +1294,13 @@ function TextBoxWidget:getCharPosAtXY(x, y) -- Only when not self.use_xtext: + if x > self.vertical_string_list[ln].width then -- no need to loop thru chars + local pos = self.vertical_string_list[ln].end_offset + if not pos then -- empty last line + return self.vertical_string_list[ln].offset + end + return pos + 1 -- after last char + end local w = 0 local w_prev while idx <= end_offset do