[vte/wip/egmont/bidi: 2/21] BiDi work up to Feb 2019 squashed
- From: Egmont Koblinger <egmontkob src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [vte/wip/egmont/bidi: 2/21] BiDi work up to Feb 2019 squashed
- Date: Fri, 31 May 2019 17:23:49 +0000 (UTC)
commit ce37d96b3bc256efb2e435803267a185162e7a7a
Author: Egmont Koblinger <egmont gmail com>
Date: Fri Aug 17 18:43:19 2018 +0200
BiDi work up to Feb 2019 squashed
BIDI-STATUS | 64 +++++
doc/bidi.txt | 205 ++++++++++++++
meson.build | 9 +
meson_options.txt | 7 +
src/bidi.cc | 724 ++++++++++++++++++++++++++++++++++++++++++++++++++
src/bidi.hh | 124 +++++++++
src/meson.build | 3 +
src/ring.cc | 10 +
src/ring.hh | 1 +
src/vte.cc | 295 +++++++++++++++++---
src/vte/vteterminal.h | 2 +-
src/vtedefines.hh | 3 +
src/vtedraw.cc | 7 +
src/vtedraw.hh | 2 +
src/vtegtk.cc | 6 +
src/vteinternal.hh | 11 +-
src/vteunistr.cc | 33 +++
src/vteunistr.h | 23 ++
18 files changed, 1483 insertions(+), 46 deletions(-)
---
diff --git a/BIDI-STATUS b/BIDI-STATUS
new file mode 100644
index 00000000..1a29c4f4
--- /dev/null
+++ b/BIDI-STATUS
@@ -0,0 +1,64 @@
+Done:
+- Implicit (level 1) and Explicit (a small subset only) modes (SM / RM 8).
+- Overall LTR or RTL direction (SPD 0 / 3 or SCP 1 / 2).
+- Possibility to autodetect paragraph direction (DECSET / DECRST 2501).
+- Possibility to make box drawing characters mirrorable (DECSET / DECRST 2500).
+- I-Beam cursor shows the character's resolved directionality when the
+ paragraph has a foreign directionality character.
+- Mouse highlighting, text copying (logical in normal modes, visual in
+ rectangle mode).
+- Mouse reporting.
+- Regex match and explicit hyperlink underlining on hover.
+- VTE_DEBUG=bidi highlights characters with resolved RTL directionality.
+- Arabic shaping using Unicode presentation forms.
+- Test file.
+- Configure flag.
+- Keyboard arrow swapping.
+
+Bugs:
+- The way the modes apply to paragraphs, and what happens when a paragraph
+ is split or two paragraphs are joined is just a first hack, needs to be
+ reviewed, adjusted, fixed properly.
+- SPD should also update all the onscreen lines.
+
+Missing from first release:
+- The entire screen is always invalidated. Have some more fine grained
+ solution (see also vte #26).
+- Probably we'd need to make Erase in Display (Below or All) sequences turn
+ the previous line to hard wrapped, requiring to keep one more line in the
+ ring's writable area.
+- Design doc review & publishing, public tracking issue.
+- Brief summary about the design and implemented features in the source tree.
+- Code cleanup and review, of course.
+
+Planned future improvements:
+- Real shaping (harfbuzz?).
+- Right-align RTL glyphs.
+- Implicit mode level 2 (handling BiDi control characters).
+- Mirror the glyphs that don't have mirrored counterpart.
+- Play with other possibilities for placing the cursor (especially when it's
+ at the end of the logical line).
+- Play with better placement of the preedit box.
+- Remember some lines that are no longer user-accessible as they scroll out,
+ to properly BiDi the still remaining part of that paragraph.
+- Possibility for default RTL directionality?
+- API (for what exactly)?
+
+Not planned at all:
+- Operating on the presentation component (DCSM reset).
+- BiDi in explicit mode by transferring the embedding levels (SDS, SRS...).
+- Escape sequences that modify the emulation behavior (SIMD...).
+
+Useful aliases:
+ alias ltr='echo -ne "\e[1 k"'
+ alias rtl='echo -ne "\e[2 k"'
+ alias implicit='echo -ne "\e[8h"'
+ alias explicit='echo -ne "\e[8l"'
+ alias bidi='echo -ne "\e[8h"' # same as implicit
+ alias nobidi='echo -ne "\e[8l"' # same as explicit
+ alias box-mirror='echo -ne "\e[?2500h"'
+ alias box-normal='echo -ne "\e[?2500l"'
+ alias auto='echo -ne "\e[?2501h"'
+ alias noauto='echo -ne "\e[?2501l"'
+ alias kbdswap='echo -ne "\e[?1243h"'
+ alias nokbdswap='echo -ne "\e[?1243l"'
diff --git a/doc/bidi.txt b/doc/bidi.txt
new file mode 100644
index 00000000..a8145824
--- /dev/null
+++ b/doc/bidi.txt
@@ -0,0 +1,205 @@
+ ╔════════════════════════════╗
+═════════════════════════╣ BiDi test – for 80 columns ╠═════════════════════════
+ ╚════════════════════════════╝
+
+[01mIn the text examples, the subsection title sometimes shows the wire order,
+transcribed to English (“Shalom” abbreviated to occupy the same width).[m
+[32mThis is followed by the reference rendering, using similar LTR glyphs.[m
+[36mFinally the actual rendering which should match the line above.[m
+
+All words, except for subsection titles under explicit modes, should show up
+in human readable order.
+
+At box tests the reference rendering is upside down, so you should get nice
+squares everywhere.
+
+ ┌──────────────┐
+────────────────────────────────┤ Implicit LTR ├────────────────────────────────
+ └──────────────┘
+[01m⸤Hello⸣ ⸤Shlm⸣[m
+[32m⸤Hello⸣ ⸤oi7w⸣[m
+[36m⸤Hello⸣ ⸤שָׁלוֹם⸣[m
+
+[01mHello01 ⸤Hello02⸣ Hello03 Shlm01 ⸤Shlm02⸣ Shlm03[m
+[32mHello01 ⸤Hello02⸣ Hello03 03oi7w ⸢02oi7w⸥ 01oi7w[m
+[36mHello01 ⸤Hello02⸣ Hello03 שָׁלוֹם01 ⸤שָׁלוֹם02⸣ שָׁלוֹם03[m
+
+[01m⸤Shlm⸣ ⸤Hello⸣[m
+[32m⸤oi7w⸣ ⸤Hello⸣[m
+[36m⸤שָׁלוֹם⸣ ⸤Hello⸣[m
+
+[01mShlm01 ⸤Shlm02⸣ Shlm03 Hello01 ⸤Hello02⸣ Hello03[m
+[32m03oi7w ⸢02oi7w⸥ 01oi7w Hello01 ⸤Hello02⸣ Hello03[m
+[36mשָׁלוֹם01 ⸤שָׁלוֹם02⸣ שָׁלוֹם03 Hello01 ⸤Hello02⸣ Hello03[m
+
+[01mParagraph wrapping at foreign word[m
+[32mLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor w[m
+[32moi7 incididunt ut labore et dolore magna aliqua.[m
+[36mLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor שָׁלוֹם incididunt ut
labore et dolore magna aliqua.[m
+
+[01mDouble wide[m
+[32ma<z n>x[m
+[36ma<z א<ת[m
+
+[01mBox[m
+[32m┏━┓ a╔═╗z n┌─┐x ╭─╮[m
+[36m┗━┛ a╚═╝z א┘─└ת ╰─╯[m
+
+[01mBox in mirrored mode[m
+[32m┏━┓ a╔═╗z n┌─┐x ╭─╮[m[?2500h
+[36m┗━┛ a╚═╝z א└─┘ת ╰─╯[m[?2500l
+
+ ┌──────────────┐
+────────────────────────────────┤ Implicit RTL ├────────────────────────────────
+ └──────────────┘
+[01m⸤Hello⸣ ⸤Shlm⸣[m
+ [32m⸢oi7w⸥ ⸢Hello⸥[m
+[36m[2 k⸤Hello⸣ ⸤שָׁלוֹם⸣[1 k[m
+
+[01mHello01 ⸤Hello02⸣ Hello03 Shlm01 ⸤Shlm02⸣ Shlm03[m
+ [32m03oi7w ⸢02oi7w⸥ 01oi7w Hello01 ⸤Hello02⸣ Hello03[m
+[36m[2 kHello01 ⸤Hello02⸣ Hello03 שָׁלוֹם01 ⸤שָׁלוֹם02⸣ שָׁלוֹם03[1 k[m
+
+[01m⸤Shlm⸣ ⸤Hello⸣[m
+ [32m⸢Hello⸥ ⸢oi7w⸥[m
+[36m[2 k⸤שָׁלוֹם⸣ ⸤Hello⸣[1 k[m
+
+[01mShlm01 ⸤Shlm02⸣ Shlm03 Hello01 ⸤Hello02⸣ Hello03[m
+ [32mHello01 ⸤Hello02⸣ Hello03 03oi7w ⸢02oi7w⸥ 01oi7w[m
+[36m[2 kשָׁלוֹם01 ⸤שָׁלוֹם02⸣ שָׁלוֹם03 Hello01 ⸤Hello02⸣ Hello03[1 k[m
+
+[01mParagraph with wrong direction (should look broken)[m
+[32mw Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor[m
+ [32m.incididunt ut labore et dolore magna aliqua oi7[m
+[36m[2 kLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor שָׁלוֹם incididunt
ut labore et dolore magna aliqua.[1 k[m
+
+[01mDouble wide[m
+ [32ma<z n>x[m
+[36m[2 kא<ת a<z[1 k[m
+
+[01mOld Hungarian Rovásírás[m
+(font at https://github.com/OldHungarian/old-hungarian-font)
+ [32mΛᛩHTΛᛩMↄH ↄ4TH4Λↄ¤ᛝ +ΛↃ[m
+[36m[2 k𐲛𐳖𐳇 𐲏𐳪𐳙𐳍𐳀𐳢𐳐𐳀𐳙 𐲢𐳛𐳮𐳁𐳤𐳑𐳢𐳁𐳤[1 k[m
+
+[01mBox[m
+ [32m╭─╮ a┌─┐z n╔═╗x ┏━┓[m[2 k
+[36m┛━┗ א╝═╚ת a└─┘z ╯─╰[m[1 k
+
+[01mBox in mirrored mode[m
+ [32m╭─╮ a┌─┐z n╔═╗x ┏━┓[m[2 k[?2500h
+[36m┗━┛ א╚═╝ת a└─┘z ╰─╯[m[?2500l[1 k
+
+ ┌──────────────────────────────┐
+────────────────────────┤ Implicit auto (LTR fallback) ├────────────────────────
+ └──────────────────────────────┘
+[01m⸤Hello⸣ ⸤Shlm⸣[m
+[32m⸤Hello⸣ ⸤oi7w⸣[m
+[36m[?2501h⸤Hello⸣ ⸤שָׁלוֹם⸣[?2501l[m
+
+[01m⸤Shlm⸣ ⸤Hello⸣[m
+ [32m⸢Hello⸥ ⸢oi7w⸥[m
+[36m[?2501h⸤שָׁלוֹם⸣ ⸤Hello⸣[?2501l[m
+
+[01mParagraph wrapping at foreign word[m
+[32mLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor w[m
+[32moi7 incididunt ut labore et dolore magna aliqua.[m
+[36m[?2501hLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor שָׁלוֹם
incididunt ut labore et dolore magna aliqua.[?2501l[m
+
+[01mBox[m
+[32m┏━┓ a╔═╗z n┌─┐x ╭─╮[m[?2501h
+[36m┗━┛ a╚═╝z א┘─└ת ╰─╯[m[?2501l
+
+[01mBox in mirrored mode[m
+[32m┏━┓ a╔═╗z n┌─┐x ╭─╮[m[?2501h[?2500h
+[36m┗━┛ a╚═╝z א└─┘ת ╰─╯[m[?2500l[?2501l
+
+ ┌──────────────────────────────┐
+────────────────────────┤ Implicit auto (RTL fallback) ├────────────────────────
+ └──────────────────────────────┘
+[01m⸤Hello⸣ ⸤Shlm⸣[m
+[32m⸤Hello⸣ ⸤oi7w⸣[m
+[36m[2 k[?2501h⸤Hello⸣ ⸤שָׁלוֹם⸣[?2501l[1 k[m
+
+[01m⸤Shlm⸣ ⸤Hello⸣[m
+ [32m⸢Hello⸥ ⸢oi7w⸥[m
+[36m[2 k[?2501h⸤שָׁלוֹם⸣ ⸤Hello⸣[?2501l[1 k[m
+
+[01mParagraph wrapping at foreign word[m
+[32mLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor w[m
+[32moi7 incididunt ut labore et dolore magna aliqua.[m
+[36m[2 k[?2501hLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor שָׁלוֹם
incididunt ut labore et dolore magna aliqua.[?2501l[1 k[m
+
+[01mBox[m
+ [32m╭─╮ a┌─┐z n╔═╗x ┏━┓[m[2 k[?2501h
+[36m┛━┗ א╝═╚ת a└─┘z ╯─╰[m[?2501l[1 k
+
+[01mBox in mirrored mode[m
+ [32m╭─╮ a┌─┐z n╔═╗x ┏━┓[m[2 k[?2501h[?2500h
+[36m┗━┛ א╚═╝ת a└─┘z ╰─╯[m[?2500l[?2501l[1 k
+
+ ┌──────────────┐
+────────────────────────────────┤ Explicit LTR ├────────────────────────────────
+ └──────────────┘
+[01m⸤Hello⸣ ⸤mlhS⸣[m
+[32m⸤Hello⸣ ⸤oi7w⸣[m
+[36m[8l⸤Hello⸣ ⸤םוֹלשָׁ⸣[8h[m
+
+[01m⸤mlhS⸣ ⸤Hello⸣[m
+[32m⸤oi7w⸣ ⸤Hello⸣[m
+[36m[8l⸤םוֹלשָׁ⸣ ⸤Hello⸣[8h[m
+
+[01mBox[m
+[32m┏━┓ a╔═╗z n┌─┐x ╭─╮[m[8l
+[36m┗━┛ a╚═╝z ת└─┘א ╰─╯[m[8h
+
+[01mBox in mirrored mode[m
+[32m┏━┓ a╔═╗z n┌─┐x ╭─╮[m[8l[?2500h
+[36m┗━┛ a╚═╝z ת└─┘א ╰─╯[m[?2500l[8h
+
+ ┌──────────────┐
+────────────────────────────────┤ Explicit RTL ├────────────────────────────────
+ └──────────────┘
+[01m⸤olleH⸣ ⸤Shlm⸣[m
+ [32m⸢oi7w⸥ ⸢Hello⸥[m
+[36m[8l[2 k⸤olleH⸣ ⸤שָׁלוֹם⸣[1 k[8h[m
+
+[01m⸤Shlm⸣ ⸤olleH⸣[m
+ [32m⸢Hello⸥ ⸢oi7w⸥[m
+[36m[8l[2 k⸤שָׁלוֹם⸣ ⸤olleH⸣[1 k[8h[m
+
+[01mFullwidth characters with underlines[m
+ [32m[4m[Lorem[24m ipsum dolor sit [4:3mamet)[m
+[36m[8l[2 k[4:3m(tema[24m tis rolod muspi [4mmeroL][1 k[8h[m
+
+[01mBox[m
+ [32m╭─╮ a┌─┐z n╔═╗x ┏━┓[m[8l[2 k
+[36m┛━┗ א╝═╚ת z┘─└a ╯─╰[m[1 k[8h
+
+[01mBox in mirrored mode[m
+ [32m╭─╮ a┌─┐z n╔═╗x ┏━┓[m[8l[2 k[?2500h
+[36m┗━┛ א╚═╝ת z└─┘a ╰─╯[m[?2500l[1 k[8h
+
+ ┌────────────┐
+─────────────────────────────────┤ Misc tests ├─────────────────────────────────
+ └────────────┘
+[01mAttributes – bold ("lo S"), magenta ("hl")[m
+[32mHel[1mlo[0;32m o[35mi7[32;1mw[m
+[36mHel[1mlo שָׁ[0;35mלו[36mֹם[m
+
+[01mNumbers are simply LTR – "jumps over 123 456 789 Shlm!"[m
+[32mThe quick brown fox jumps over the lazy dog The quick brown fox jumps over 123 4[m
+[32m56 789 oi7w![m
+[36mThe quick brown fox jumps over the lazy dog The quick brown fox jumps over 123 456 789 שָׁלוֹם![m
+
+[01mNumbers are inside RTL – "jumps Shlm 123 456 789 Shlm!"[m
+[32mThe quick brown fox jumps over the lazy dog The quick brown fox jumps 4 123 oi7w[m
+[32moi7w 789 56![m
+[36mThe quick brown fox jumps over the lazy dog The quick brown fox jumps שָׁלוֹם 123 456 789 שָׁלוֹם![m
+
+[01mMirroring across linebreak – "jumps Shlm <[<[<[<[ Shlm!"[m
+[32mThe quick brown fox jumps over the lazy dog The quick brown fox jumps >]>]> oi7w[m
+[32moi7w ]>]![m
+[36mThe quick brown fox jumps over the lazy dog The quick brown fox jumps שָׁלוֹם <[<[<[<[ שָׁלוֹם![m
+
+────────────────────────────────────────────────────────────────────────────────
diff --git a/meson.build b/meson.build
index 3760b3f8..451195b9 100644
--- a/meson.build
+++ b/meson.build
@@ -34,6 +34,7 @@ project(
gtk3_req_version = '3.8.0'
gtk4_req_version = '4.0.0'
+fribidi_req_version = '1.0.0'
gio_req_version = '2.40.0'
glib_req_version = '2.40.0'
gnutls_req_version = '3.2.7'
@@ -109,6 +110,7 @@ config_h = configuration_data()
config_h.set_quoted('GETTEXT_PACKAGE', vte_gettext_domain)
config_h.set_quoted('VERSION', vte_version)
config_h.set('VTE_DEBUG', enable_debug)
+config_h.set('WITH_FRIBIDI', get_option('fribidi'))
config_h.set('WITH_GNUTLS', get_option('gnutls'))
config_h.set('WITH_ICONV', get_option('iconv'))
@@ -373,6 +375,12 @@ pcre2_dep = dependency('libpcre2-8', version: '>=' + pcre2_req_version)
pthreads_dep = dependency('threads')
zlib_dep = dependency('zlib')
+if get_option('fribidi')
+ fribidi_dep = dependency('fribidi', version: '>=' + fribidi_req_version)
+else
+ fribidi_dep = dependency('', required: false)
+endif
+
if get_option('gnutls')
gnutls_dep = dependency('gnutls', version: '>=' + gnutls_req_version)
else
@@ -443,6 +451,7 @@ output += '\n'
output += ' Coverage: ' + get_option('b_coverage').to_string() + '\n'
output += ' Debug: ' + enable_debug.to_string() + '\n'
output += ' Docs: ' + get_option('docs').to_string() + '\n'
+output += ' FRIBIDI: ' + get_option('fribidi').to_string() + '\n'
output += ' GNUTLS: ' + get_option('gnutls').to_string() + '\n'
output += ' GTK+ 3.0: ' + get_option('gtk3').to_string() + '\n'
output += ' GTK+ 4.0: ' + get_option('gtk4').to_string() + '\n'
diff --git a/meson_options.txt b/meson_options.txt
index b8a11a84..d29c66a3 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -34,6 +34,13 @@ option(
description: 'Enable GObject Introspection',
)
+option(
+ 'fribidi',
+ type: 'boolean',
+ value: true,
+ description: 'Enable FriBidi support',
+)
+
option(
'gnutls',
type: 'boolean',
diff --git a/src/bidi.cc b/src/bidi.cc
new file mode 100644
index 00000000..8bdef69d
--- /dev/null
+++ b/src/bidi.cc
@@ -0,0 +1,724 @@
+/*
+ * Copyright © 2018–2019 Egmont Koblinger
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <config.h>
+
+#ifdef WITH_FRIBIDI
+#include <fribidi.h>
+#endif
+
+#include "bidi.hh"
+#include "debug.h"
+#include "vtedefines.hh"
+#include "vteinternal.hh"
+
+#ifdef WITH_FRIBIDI
+static_assert (sizeof (gunichar) == sizeof (FriBidiChar), "Whoooo");
+#endif
+
+using namespace vte::base;
+
+BidiRow::BidiRow()
+{
+ m_width = 0;
+
+ /* These will be initialized / allocated on demand, when some RTL is encountered. */
+ m_width_alloc = 0;
+ m_log2vis = nullptr;
+ m_vis2log = nullptr;
+ m_vis_rtl = nullptr;
+ m_vis_shaped_char = nullptr;
+}
+
+BidiRow::~BidiRow()
+{
+ g_free (m_log2vis);
+ g_free (m_vis2log);
+ g_free (m_vis_rtl);
+ g_free (m_vis_shaped_char);
+}
+
+void BidiRow::set_width(vte::grid::column_t width)
+{
+ if (G_UNLIKELY (width > m_width_alloc)) {
+ if (m_width_alloc == 0) {
+ m_width_alloc = 128;
+ }
+ while (width > m_width_alloc) {
+ m_width_alloc *= 2;
+ }
+ m_log2vis = (vte::grid::column_t *) g_realloc (m_log2vis, sizeof (vte::grid::column_t) *
m_width_alloc);
+ m_vis2log = (vte::grid::column_t *) g_realloc (m_vis2log, sizeof (vte::grid::column_t) *
m_width_alloc);
+ m_vis_rtl = (guint8 *) g_realloc (m_vis_rtl, sizeof (guint8) * m_width_alloc);
+ m_vis_shaped_char = (gunichar *) g_realloc (m_vis_shaped_char, sizeof (gunichar) *
m_width_alloc);
+ }
+
+ m_width = width;
+}
+
+/* Converts from logical to visual column. Offscreen columns are mirrored
+ * for RTL lines, e.g. (assuming 80 columns) -1 <=> 80, -2 <=> 81 etc. */
+vte::grid::column_t BidiRow::log2vis(vte::grid::column_t col) const
+{
+ if (col >= 0 && col < m_width) {
+ return m_log2vis[col];
+ } else {
+ return m_base_rtl ? m_width - 1 - col : col;
+ }
+}
+
+/* Converts from visual to logical column. Offscreen columns are mirrored
+ * for RTL lines, e.g. (assuming 80 columns) -1 <=> 80, -2 <=> 81 etc. */
+vte::grid::column_t BidiRow::vis2log(vte::grid::column_t col) const
+{
+ if (col >= 0 && col < m_width) {
+ return m_vis2log[col];
+ } else {
+ return m_base_rtl ? m_width - 1 - col : col;
+ }
+}
+
+/* Whether the cell at the given visual position has RTL directionality.
+ * For offscreen columns the line's base direction is returned. */
+bool BidiRow::vis_is_rtl(vte::grid::column_t col) const
+{
+ if (col >= 0 && col < m_width) {
+ return m_vis_rtl[col];
+ } else {
+ return m_base_rtl;
+ }
+}
+
+/* Whether the cell at the given logical position has RTL directionality.
+ * For offscreen columns the line's base direction is returned. */
+bool BidiRow::log_is_rtl(vte::grid::column_t col) const
+{
+ if (col >= 0 && col < m_width) {
+ col = m_log2vis[col];
+ return m_vis_rtl[col];
+ } else {
+ return m_base_rtl;
+ }
+}
+
+/* Get the shaped character (vteunistr) for the given visual position.
+ *
+ * The unshaped character (vteunistr) needs to be passed to this method because
+ * the BiDi component may not store it if no shaping was required, and does not
+ * store combining accents. This method takes care of preserving combining accents.
+ */
+vteunistr
+BidiRow::vis_get_shaped_char(vte::grid::column_t col, vteunistr s) const
+{
+ if (col >= m_width || m_vis_shaped_char[col] == 0)
+ return s;
+
+ return _vte_unistr_replace_base(s, m_vis_shaped_char[col]);
+}
+
+/* Whether the line's base direction is RTL. */
+bool BidiRow::base_is_rtl() const
+{
+ return m_base_rtl;
+}
+
+/* Whether the paragraph contains a foreign directionality character.
+ * This is used in the cursor, showing the character's directionality. */
+bool BidiRow::has_foreign() const
+{
+ return m_has_foreign;
+}
+
+
+RingView::RingView()
+{
+ m_ring = nullptr;
+
+ m_start = m_len = m_width = 0;
+ m_height_alloc = 32;
+
+ m_bidirows = (BidiRow **) g_malloc (sizeof (BidiRow *) * m_height_alloc);
+ for (int i = 0; i < m_height_alloc; i++) {
+ m_bidirows[i] = new BidiRow();
+ }
+
+ m_invalid = true;
+}
+
+RingView::~RingView()
+{
+ for (int i = 0; i < m_height_alloc; i++) {
+ delete m_bidirows[i];
+ }
+ g_free (m_bidirows);
+}
+
+void RingView::set_ring(Ring *ring)
+{
+ if (ring == m_ring)
+ return;
+
+ m_ring = ring;
+ m_invalid = true;
+}
+
+void RingView::set_width(vte::grid::column_t width)
+{
+ if (width == m_width)
+ return;
+
+ m_width = width;
+ m_invalid = true;
+}
+
+void RingView::set_rows(vte::grid::row_t start, vte::grid::row_t len)
+{
+ if (start == m_start && len == m_len)
+ return;
+
+ if (G_UNLIKELY (len > m_height_alloc)) {
+ int i = m_height_alloc;
+ while (len > m_height_alloc) {
+ m_height_alloc *= 2;
+ }
+ m_bidirows = (BidiRow **) g_realloc (m_bidirows, sizeof (BidiRow *) * m_height_alloc);
+ for (; i < m_height_alloc; i++) {
+ m_bidirows[i] = new BidiRow();
+ }
+ }
+
+ m_start = start;
+ m_len = len;
+ m_invalid = true;
+}
+
+void RingView::maybe_update()
+{
+ if (!m_invalid)
+ return;
+
+ vte::grid::row_t i = m_start;
+ const VteRowData *row_data = m_ring->index_safe(m_start);
+
+ if (row_data && (row_data->attr.bidi_flags & VTE_BIDI_IMPLICIT)) {
+ i = find_paragraph(m_start);
+ if (i == -1) {
+ i = explicit_paragraph(m_start, row_data->attr.bidi_flags & VTE_BIDI_RTL);
+ }
+ }
+ while (i < m_start + m_len) {
+ i = paragraph(i);
+ }
+
+ m_invalid = false;
+}
+
+BidiRow const* RingView::get_row_map(vte::grid::row_t row) const
+{
+ g_assert_cmpint (row, >=, m_start);
+ g_assert_cmpint (row, <, m_start + m_len);
+ g_assert_false (m_invalid);
+
+ return m_bidirows[row - m_start];
+}
+
+BidiRow* RingView::get_row_map_writable(vte::grid::row_t row) const
+{
+ g_assert_cmpint (row, >=, m_start);
+ g_assert_cmpint (row, <, m_start + m_len);
+
+ return m_bidirows[row - m_start];
+}
+
+/* Set up the mapping according to explicit mode for a given line. */
+void RingView::explicit_line(vte::grid::row_t row, bool rtl)
+{
+ int i;
+
+ if (G_UNLIKELY (row < m_start || row >= m_start + m_len))
+ return;
+
+ BidiRow *bidirow = get_row_map_writable(row);
+ bidirow->m_base_rtl = rtl;
+ bidirow->m_has_foreign = false;
+
+ if (G_UNLIKELY (rtl)) {
+ bidirow->set_width(m_width);
+ for (i = 0; i < m_width; i++) {
+ bidirow->m_log2vis[i] = bidirow->m_vis2log[i] = m_width - 1 - i;
+ bidirow->m_vis_rtl[i] = true;
+ bidirow->m_vis_shaped_char[i] = 0;
+ }
+ } else {
+ /* Shortcut: bidirow->m_width == 0 might denote a fully LTR line,
+ * m_width_alloc might even be 0 along with log2vis and friends being nullptr in this case.
*/
+ bidirow->set_width(0);
+ }
+}
+
+/* Set up the mapping according to explicit mode, for all the lines
+ * of a paragraph beginning at the given line.
+ * Returns the row number after the paragraph or viewport (whichever ends first). */
+vte::grid::row_t RingView::explicit_paragraph(vte::grid::row_t row, bool rtl)
+{
+ const VteRowData *row_data;
+
+ while (row < m_start + m_len) {
+ explicit_line(row, rtl);
+
+ row_data = m_ring->index_safe(row++);
+ if (row_data == nullptr || !row_data->attr.soft_wrapped)
+ break;
+ }
+ return row;
+}
+
+/* For the given row, find the first row of its paragraph.
+ * Returns -1 if have to walk backwards too much. */
+/* FIXME this could be much cheaper, we don't need to read the actual rows (text_stream),
+ * we only need the soft_wrapped flag which is stored in row_stream. Needs method in ring. */
+vte::grid::row_t RingView::find_paragraph(vte::grid::row_t row)
+{
+ vte::grid::row_t row_stop = row - VTE_BIDI_PARAGRAPH_LENGTH_MAX;
+ const VteRowData *row_data;
+
+ while (row-- > row_stop) {
+ if (row < _vte_ring_delta(m_ring))
+ return row + 1;
+ row_data = m_ring->index_safe(row);
+ if (row_data == nullptr || !row_data->attr.soft_wrapped)
+ return row + 1;
+ }
+ return -1;
+}
+
+/* Figure out the mapping for the paragraph starting at the given row.
+ * Returns the row number after the paragraph or viewport (whichever ends first). */
+vte::grid::row_t RingView::paragraph(vte::grid::row_t row)
+{
+ const VteRowData *row_data = m_ring->index_safe(row);
+ if (row_data == nullptr) {
+ return explicit_paragraph(row, false);
+ }
+
+#ifndef WITH_FRIBIDI
+ return explicit_paragraph(row, row_data->attr.bidi_flags & VTE_BIDI_RTL);
+#else
+ const VteCell *cell;
+ bool rtl;
+ bool autodir;
+ FriBidiParType pbase_dir;
+ FriBidiLevel level;
+ FriBidiChar *fribidi_chars;
+ FriBidiCharType *fribidi_chartypes;
+ FriBidiBracketType *fribidi_brackettypes;
+ FriBidiJoiningType *fribidi_joiningtypes;
+ FriBidiLevel *fribidi_levels;
+ FriBidiStrIndex *fribidi_map;
+ FriBidiStrIndex *fribidi_to_term;
+
+ BidiRow *bidirow;
+
+ if (!(row_data->attr.bidi_flags & VTE_BIDI_IMPLICIT)) {
+ return explicit_paragraph(row, row_data->attr.bidi_flags & VTE_BIDI_RTL);
+ }
+
+ rtl = row_data->attr.bidi_flags & VTE_BIDI_RTL;
+ autodir = row_data->attr.bidi_flags & VTE_BIDI_AUTO;
+
+ int lines[VTE_BIDI_PARAGRAPH_LENGTH_MAX + 1]; /* offsets to the beginning of lines */
+ lines[0] = 0;
+ int line = 0; /* line number within the paragraph */
+ int count; /* total character count */
+ int row_orig = row;
+ int tl, tv; /* terminal logical and visual */
+ int fl, fv; /* fribidi logical and visual */
+ unsigned int col;
+
+ GArray *fribidi_chars_array = g_array_new (FALSE, FALSE, sizeof (FriBidiChar));
+ GArray *fribidi_map_array = g_array_new (FALSE, FALSE, sizeof (FriBidiStrIndex));
+ GArray *fribidi_to_term_array = g_array_new (FALSE, FALSE, sizeof (FriBidiStrIndex));
+
+ /* Extract the paragraph's contents, omitting unused and fragment cells. */
+
+ /* Example of what is going on, showing the most important steps:
+ *
+ * Let's take the string produced by this command:
+ * echo -e "\u0041\u05e9\u05b8\u05c1\u05dc\u05d5\u05b9\u05dd\u0031\u0032\uff1c\u05d0"
+ *
+ * This string consists of:
+ * - English letter A
+ * - Hebrew word Shalom:
+ * - Letter Shin: ש
+ * - Combining accent Qamats
+ * - Combining accent Shin Dot
+ * - Letter Lamed: ל
+ * - Letter Vav: ו
+ * - Combining accent Holam
+ * - Letter Final Mem: ם
+ * - Digits One and Two
+ * - Full-width less-than sign U+ff1c: <
+ * - Hebrew letter Alef: א
+ *
+ * Features of this example:
+ * - Overall LTR direction for convenience (set up by the leading English letter)
+ * - Combining accents within RTL
+ * - Double width character with RTL resolved direction
+ * - A mapping that is not its own inverse (due to the digits being LTR inside RTL inside LTR),
+ * to help catch if we'd look up something in the wrong direction
+ *
+ * Not demonstrated in this example:
+ * - Wrapping a paragraph to lines
+ * - Spacing marks
+ *
+ * Pre-BiDi (logical) order, using approximating glyphs ("Shalom" is "w7io", Alef is "x"):
+ * Aw7io12<x
+ *
+ * Post-BiDi (visual) order, using approximating glyphs ("Shalom" is "oi7w", note the mirrored
less-than):
+ * Ax>12oi7w
+ *
+ * Terminal's logical cells:
+ * [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
+ * row_data: A Shin+qam+dot Lam Vav+hol Mem One Two Less Less (cont) Alef
+ *
+ * Extracted to pass to FriBidi (combining accents get -1, double wides' continuation cells are
skipped):
+ * [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]
+ * fribidi_chars: A Shin qam dot Lam Vav hol Mem One Two Less Alef
+ * fribidi_map: 0 1 -1 -1 4 5 -1 7 8 9 10 11
+ * fribidi_to_term: 0 1 -1 -1 2 3 -1 4 5 6 7 9
+ *
+ * Embedding levels and other properties (shaping etc.) are looked up:
+ * [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]
+ * fribidi_levels: 0 1 1 1 1 1 1 1 2 2 1 1
+ *
+ * The steps above were per-paragraph. The steps below are per-line.
+ *
+ * After fribidi_reorder_line (only this array gets shuffled):
+ * [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]
+ * fribidi_map: 0 11 10 8 9 7 5 -1 4 1 -1 -1
+ *
+ * To get the visual order: walk in the new fribidi_map, and for each real entry look up the
+ * logical terminal column using fribidi_to_term:
+ * - map[0] is 0, to_term[0] is 0, hence visual column 0 belongs to logical column 0 (A)
+ * - map[1] is 11, to_term[11] is 9, hence visual column 1 belongs to logical column 9 (Alef)
+ * - map[2] is 10, to_term[10] is 7, row_data[7] is the "<" sign
+ * - this is a double wide character, we need to map the next two visual cells to two logical
cells
+ * - due to levels[10] being odd, this character has a resolved RTL direction
+ * - thus we map in reverse order: visual 2 <=> logical 8, visual 3 <=> logical 7
+ * - the glyph is also mirrorable, it'll be displayed accordingly
+ * - [3] -> 8 -> 5, so visual 4 <=> logical 5 (One)
+ * - [4] -> 9 -> 6, so visual 5 <=> logical 6 (Two)
+ * - [5] -> 7 -> 4, so visual 6 <=> logical 4 (Mem, the last, leftmost letter of Shalom)
+ * - [6] -> 5 -> 3, so visual 7 <=> logical 3 (Vav+hol)
+ * - [7] -> -1, skipped
+ * - [8] -> 4 -> 2, so visual 8 <=> logical 2 (Lam)
+ * - [9] -> 1 -> 1, so visual 9 <=> logical 1 (Shin+qam+dot, the first, rightmost letter of Shalom)
+ * - [10] -> -1, skipped
+ * - [11] -> -1, skipped
+ *
+ * Silly FriBidi API almost allows us to skip one level of indirection, by placing the to_term values
+ * in the map to be shuffled. However, we can't get the embedding levels then.
+ * TODO: File an issue for a better API.
+ */
+ while (row < _vte_ring_next(m_ring)) {
+ row_data = m_ring->index_safe(row);
+ if (row_data == nullptr)
+ break;
+
+ if (line == VTE_BIDI_PARAGRAPH_LENGTH_MAX) {
+ /* Overlong paragraph, bail out. */
+ g_array_free (fribidi_chars_array, TRUE);
+ g_array_free (fribidi_map_array, TRUE);
+ g_array_free (fribidi_to_term_array, TRUE);
+ return explicit_paragraph (row_orig, rtl);
+ }
+
+ /* A row_data might be longer, in case rewrapping is disabled and the window was narrowed.
+ * Truncate the logical data before applying BiDi. */
+ // FIXME what the heck to do if this truncation cuts a TAB or CJK in half???
+ for (tl = 0; tl < m_width && tl < row_data->len; tl++) {
+ auto prev_len = fribidi_chars_array->len;
+ FriBidiStrIndex val;
+
+ cell = _vte_row_data_get (row_data, tl);
+ if (cell->attr.fragment())
+ continue;
+
+ /* Extract the base character and combining accents.
+ * Convert mid-line erased cells to spaces.
+ * Note: see the static assert at the top of this file. */
+ _vte_unistr_append_to_gunichars (cell->c ? cell->c : ' ', fribidi_chars_array);
+ /* Make sure at least one character was produced. */
+ g_assert_cmpint (fribidi_chars_array->len, >, prev_len);
+
+ /* Track the base character, assign to it its current index in fribidi_chars.
+ * Don't track combining accents, assign -1's to them. */
+ val = prev_len;
+ g_array_append_val (fribidi_map_array, val);
+ val = tl;
+ g_array_append_val (fribidi_to_term_array, val);
+ prev_len++;
+ val = -1;
+ while (prev_len++ < fribidi_chars_array->len) {
+ g_array_append_val (fribidi_map_array, val);
+ g_array_append_val (fribidi_to_term_array, val);
+ }
+ }
+
+ lines[++line] = fribidi_chars_array->len;
+ row++;
+
+ if (!row_data->attr.soft_wrapped)
+ break;
+ }
+
+ if (line == 0) {
+ /* Beyond the end of the ring. */
+ g_array_free (fribidi_chars_array, TRUE);
+ g_array_free (fribidi_map_array, TRUE);
+ g_array_free (fribidi_to_term_array, TRUE);
+ return explicit_paragraph (row_orig, rtl);
+ }
+
+ /* Convenience stuff, we no longer need the auto-growing GArray wrapper. */
+ count = fribidi_chars_array->len;
+ fribidi_chars = (FriBidiChar *) fribidi_chars_array->data;
+ fribidi_map = (FriBidiStrIndex *) fribidi_map_array->data;
+ fribidi_to_term = (FriBidiStrIndex *) fribidi_to_term_array->data;
+
+ /* Run the BiDi algorithm on the paragraph to get the embedding levels. */
+ fribidi_chartypes = g_newa (FriBidiCharType, count);
+ fribidi_brackettypes = g_newa (FriBidiBracketType, count);
+ fribidi_joiningtypes = g_newa (FriBidiJoiningType, count);
+ fribidi_levels = g_newa (FriBidiLevel, count);
+
+ pbase_dir = autodir ? (rtl ? FRIBIDI_PAR_WRTL : FRIBIDI_PAR_WLTR)
+ : (rtl ? FRIBIDI_PAR_RTL : FRIBIDI_PAR_LTR );
+
+ fribidi_get_bidi_types (fribidi_chars, count, fribidi_chartypes);
+ fribidi_get_bracket_types (fribidi_chars, count, fribidi_chartypes, fribidi_brackettypes);
+ fribidi_get_joining_types (fribidi_chars, count, fribidi_joiningtypes);
+ level = fribidi_get_par_embedding_levels_ex (fribidi_chartypes, fribidi_brackettypes, count,
&pbase_dir, fribidi_levels);
+
+ if (level == 0) {
+ /* error */
+ g_array_free (fribidi_chars_array, TRUE);
+ g_array_free (fribidi_map_array, TRUE);
+ g_array_free (fribidi_to_term_array, TRUE);
+ return explicit_paragraph (row_orig, rtl);
+ }
+
+ /* Arabic shaping
+ *
+ * https://www.w3.org/TR/css-text-3/#word-break-shaping says:
+ * "When shaping scripts such as Arabic wrap [...] the characters must still be shaped (their
joining forms chosen)
+ * as if the word were still whole."
+ *
+ * Also, FriBidi's Arabic shaping methods, as opposed to fribidi_reorder_line(), don't take an
offset parameter.
+ * This is another weak sign that the desired behavior is to shape the entire paragraph before
splitting to lines.
+ *
+ * We only perform shaping in implicit mode, for two reasons:
+ *
+ * Following the CSS logic, I think the sensible behavior for a partially visible word (e.g. at the
margin of a
+ * text editor) is to use the joining/shaping form according to the entire word. Hence in explicit
mode it must be
+ * the responsibility of the BiDi-aware application and not the terminal emulator to perform
joining/shaping.
+ *
+ * And a technical limitation: FriBidi can only perform joining/shaping with the logical order as
input, not with
+ * the visual order. We'd need to find another API, or do ugly workarounds, which I'd rather not. */
+ fribidi_join_arabic (fribidi_chartypes, count, fribidi_levels, fribidi_joiningtypes);
+ fribidi_shape_arabic (FRIBIDI_FLAGS_ARABIC, fribidi_levels, count, fribidi_joiningtypes,
fribidi_chars);
+
+ g_assert_cmpint (pbase_dir, !=, FRIBIDI_PAR_ON);
+ /* For convenience, from now on this variable contains the resolved (i.e. possibly autodetected)
value. */
+ rtl = (pbase_dir == FRIBIDI_PAR_RTL || pbase_dir == FRIBIDI_PAR_WRTL);
+
+ if (!rtl && level == 1) {
+ /* Fast shortcut for LTR-only paragraphs. */
+ g_array_free (fribidi_chars_array, TRUE);
+ g_array_free (fribidi_map_array, TRUE);
+ g_array_free (fribidi_to_term_array, TRUE);
+ return explicit_paragraph (row_orig, false);
+ }
+
+ /* Reshuffle line by line. */
+ row = row_orig;
+ line = 0;
+ if (G_UNLIKELY (row < m_start)) {
+ line = m_start - row;
+ row = m_start;
+ }
+
+ while (row < _vte_ring_next(m_ring) && row < m_start + m_len) {
+ bidirow = get_row_map_writable(row);
+ bidirow->m_base_rtl = rtl;
+ bidirow->m_has_foreign = true;
+ bidirow->set_width(m_width);
+
+ row_data = m_ring->index_safe(row);
+ if (row_data == nullptr)
+ break;
+
+ level = fribidi_reorder_line (FRIBIDI_FLAGS_DEFAULT,
+ fribidi_chartypes,
+ lines[line + 1] - lines[line],
+ lines[line],
+ pbase_dir,
+ fribidi_levels,
+ NULL,
+ fribidi_map);
+
+ if (level == 0) {
+ /* error, what should we do? */
+ explicit_line (row, rtl);
+ bidirow->m_has_foreign = true;
+ goto next_line;
+ }
+
+ if (!rtl && level == 1) {
+ /* Fast shortcut for LTR-only lines. */
+ explicit_line (row, false);
+ bidirow->m_has_foreign = true;
+ goto next_line;
+ }
+
+ /* Copy to our realm. Proceed in visual order.*/
+ tv = 0;
+ if (rtl) {
+ /* Unused cells on the left for RTL paragraphs */
+ int unused = MAX(m_width - row_data->len, 0);
+ for (; tv < unused; tv++) {
+ bidirow->m_vis2log[tv] = m_width - 1 - tv;
+ bidirow->m_vis_rtl[tv] = true;
+ bidirow->m_vis_shaped_char[tv] = 0;
+ }
+ }
+ for (fv = lines[line]; fv < lines[line + 1]; fv++) {
+ /* Inflate fribidi's result by inserting fragments. */
+ fl = fribidi_map[fv];
+ if (fl == -1)
+ continue;
+ tl = fribidi_to_term[fl];
+ cell = _vte_row_data_get (row_data, tl);
+ g_assert (!cell->attr.fragment());
+ g_assert (cell->attr.columns() > 0);
+ if (FRIBIDI_LEVEL_IS_RTL(fribidi_levels[fl])) {
+ /* RTL character directionality. Map fragments in reverse order. */
+ for (col = 0; col < cell->attr.columns(); col++) {
+ bidirow->m_vis2log[tv + col] = tl + cell->attr.columns() - 1 - col;
+ bidirow->m_vis_rtl[tv + col] = true;
+ bidirow->m_vis_shaped_char[tv + col] = fribidi_chars[fl];
+ }
+ tv += cell->attr.columns();
+ tl += cell->attr.columns();
+ } else {
+ /* LTR character directionality. */
+ for (col = 0; col < cell->attr.columns(); col++) {
+ bidirow->m_vis2log[tv] = tl;
+ bidirow->m_vis_rtl[tv] = false;
+ bidirow->m_vis_shaped_char[tv] = fribidi_chars[fl];
+ tv++;
+ tl++;
+ }
+ }
+ }
+ if (!rtl) {
+ /* Unused cells on the right for LTR paragraphs */
+ g_assert_cmpint (tv, ==, MIN (row_data->len, m_width));
+ for (; tv < m_width; tv++) {
+ bidirow->m_vis2log[tv] = tv;
+ bidirow->m_vis_rtl[tv] = false;
+ bidirow->m_vis_shaped_char[tv] = 0;
+ }
+ }
+ g_assert_cmpint (tv, ==, m_width);
+
+ /* From vis2log create the log2vis mapping too.
+ * In debug mode assert that we have a bijective mapping. */
+ if (_vte_debug_on (VTE_DEBUG_BIDI)) {
+ for (tl = 0; tl < m_width; tl++) {
+ bidirow->m_log2vis[tl] = -1;
+ }
+ }
+
+ for (tv = 0; tv < m_width; tv++) {
+ bidirow->m_log2vis[bidirow->m_vis2log[tv]] = tv;
+ }
+
+ if (_vte_debug_on (VTE_DEBUG_BIDI)) {
+ for (tl = 0; tl < m_width; tl++) {
+ g_assert_cmpint (bidirow->m_log2vis[tl], !=, -1);
+ }
+ }
+
+next_line:
+ line++;
+ row++;
+
+ if (!row_data->attr.soft_wrapped)
+ break;
+ }
+
+ g_array_free (fribidi_chars_array, TRUE);
+ g_array_free (fribidi_map_array, TRUE);
+ g_array_free (fribidi_to_term_array, TRUE);
+
+ return row;
+#endif /* !WITH_FRIBIDI */
+}
+
+
+/* Find the mirrored counterpart of a codepoint, just like
+ * fribidi_get_mirror_char() or g_unichar_get_mirror_char() does.
+ * Two additions:
+ * - works with vteunistr, that is, preserves combining accents;
+ * - optionally mirrors box drawing characters.
+ */
+gboolean vte_bidi_get_mirror_char (vteunistr unistr, gboolean mirror_box_drawing, vteunistr *out)
+{
+ static const unsigned char mirrored_2500[0x80] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x11, 0x12,
0x13,
+ 0x0c, 0x0d, 0x0e, 0x0f, 0x18, 0x19, 0x1a, 0x1b, 0x14, 0x15, 0x16, 0x17, 0x24, 0x25, 0x26,
0x27,
+ 0x28, 0x29, 0x2a, 0x2b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x2c, 0x2e, 0x2d,
0x2f,
+ 0x30, 0x32, 0x31, 0x33, 0x34, 0x36, 0x35, 0x37, 0x38, 0x3a, 0x39, 0x3b, 0x3c, 0x3e, 0x3d,
0x3f,
+ 0x40, 0x41, 0x42, 0x44, 0x43, 0x46, 0x45, 0x47, 0x48, 0x4a, 0x49, 0x4b, 0x4c, 0x4d, 0x4e,
0x4f,
+ 0x50, 0x51, 0x55, 0x56, 0x57, 0x52, 0x53, 0x54, 0x5b, 0x5c, 0x5d, 0x58, 0x59, 0x5a, 0x61,
0x62,
+ 0x63, 0x5e, 0x5f, 0x60, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6e, 0x6d,
0x70,
+ 0x6f, 0x72, 0x71, 0x73, 0x76, 0x75, 0x74, 0x77, 0x7a, 0x79, 0x78, 0x7b, 0x7e, 0x7d, 0x7c,
0x7f };
+
+ gunichar base_ch = _vte_unistr_get_base (unistr);
+ gunichar base_ch_mirrored = base_ch;
+
+ if (G_UNLIKELY (base_ch >= 0x2500 && base_ch < 0x2580)) {
+ if (G_UNLIKELY (mirror_box_drawing))
+ base_ch_mirrored = 0x2500 + mirrored_2500[base_ch - 0x2500];
+ } else {
+#ifdef WITH_FRIBIDI
+ /* Prefer the FriBidi variant as that's more likely to be in sync with the rest of our BiDi
stuff. */
+ fribidi_get_mirror_char (base_ch, &base_ch_mirrored);
+#else
+ /* Fall back to glib, so that we still get mirrored characters in explicit RTL mode without
BiDi support. */
+ g_unichar_get_mirror_char (base_ch, &base_ch_mirrored);
+#endif
+ }
+
+ vteunistr unistr_mirrored = _vte_unistr_replace_base (unistr, base_ch_mirrored);
+
+ if (out)
+ *out = unistr_mirrored;
+ return unistr_mirrored == unistr;
+}
diff --git a/src/bidi.hh b/src/bidi.hh
new file mode 100644
index 00000000..38e8035e
--- /dev/null
+++ b/src/bidi.hh
@@ -0,0 +1,124 @@
+/*
+ * Copyright © 2018–2019 Egmont Koblinger
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma once
+
+#include <glib.h>
+
+#include "ring.hh"
+#include "vterowdata.hh"
+#include "vtetypes.hh"
+#include "vteunistr.h"
+
+namespace vte {
+
+namespace base { // FIXME ???
+
+/* BidiRow contains the BiDi transformation of a single row. */
+class BidiRow {
+ friend class RingView;
+
+public:
+ BidiRow();
+ ~BidiRow();
+
+ // prevent accidents
+ BidiRow(BidiRow& o) = delete;
+ BidiRow(BidiRow const& o) = delete;
+ BidiRow(BidiRow&& o) = delete;
+ BidiRow& operator= (BidiRow& o) = delete;
+ BidiRow& operator= (BidiRow const& o) = delete;
+ BidiRow& operator= (BidiRow&& o) = delete;
+
+ vte::grid::column_t log2vis(vte::grid::column_t col) const;
+ vte::grid::column_t vis2log(vte::grid::column_t col) const;
+ bool log_is_rtl(vte::grid::column_t col) const;
+ bool vis_is_rtl(vte::grid::column_t col) const;
+ vteunistr vis_get_shaped_char(vte::grid::column_t col, vteunistr s) const;
+ bool base_is_rtl() const;
+ bool has_foreign() const;
+
+private:
+ void set_width(vte::grid::column_t width);
+
+ vte::grid::column_t m_width;
+ vte::grid::column_t m_width_alloc;
+
+ vte::grid::column_t *m_log2vis;
+ vte::grid::column_t *m_vis2log;
+ guint8 *m_vis_rtl;
+ gunichar *m_vis_shaped_char;
+
+ guint8 m_base_rtl: 1;
+ guint8 m_has_foreign: 1;
+};
+
+
+/* RingView contains the BiDi transformations for all the rows of the viewport. */
+class RingView {
+public:
+ RingView();
+ ~RingView();
+
+ // prevent accidents
+ RingView(RingView& o) = delete;
+ RingView(RingView const& o) = delete;
+ RingView(RingView&& o) = delete;
+ RingView& operator= (RingView& o) = delete;
+ RingView& operator= (RingView const& o) = delete;
+ RingView& operator= (RingView&& o) = delete;
+
+ void set_ring(Ring *ring);
+ void set_rows(vte::grid::row_t start, vte::grid::row_t len);
+ void set_width(vte::grid::column_t width);
+
+ inline void invalidate() { m_invalid = true; }
+ void maybe_update();
+
+ BidiRow const* get_row_map(vte::grid::row_t row) const;
+
+private:
+ Ring *m_ring;
+
+ BidiRow **m_bidirows;
+
+ vte::grid::row_t m_start;
+ vte::grid::row_t m_len;
+ vte::grid::column_t m_width;
+
+ vte::grid::row_t m_height_alloc;
+
+ bool m_invalid;
+
+ BidiRow* get_row_map_writable(vte::grid::row_t row) const;
+
+ void explicit_line(vte::grid::row_t row, bool rtl);
+ vte::grid::row_t explicit_paragraph(vte::grid::row_t row, bool rtl);
+ vte::grid::row_t find_paragraph(vte::grid::row_t row);
+ vte::grid::row_t paragraph(vte::grid::row_t row);
+};
+
+}; /* namespace base */
+
+}; /* namespace vte */
+
+G_BEGIN_DECLS
+
+gboolean vte_bidi_get_mirror_char (vteunistr unistr, gboolean mirror_box_drawing, vteunistr
*unistr_mirrored);
+
+G_END_DECLS
diff --git a/src/meson.build b/src/meson.build
index 29046b77..fef7da1e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -56,6 +56,8 @@ utf8_sources = files(
libvte_common_sources = debug_sources + modes_sources + parser_sources + utf8_sources + files(
'attr.hh',
+ 'bidi.cc',
+ 'bidi.hh',
'buffer.h',
'caps.hh',
'cell.hh',
@@ -144,6 +146,7 @@ libvte_common_public_deps = [
]
libvte_common_deps = libvte_common_public_deps + [
+ fribidi_dep,
gnutls_dep,
pcre2_dep,
libm_dep,
diff --git a/src/ring.cc b/src/ring.cc
index a121e3e8..400f1269 100644
--- a/src/ring.cc
+++ b/src/ring.cc
@@ -609,6 +609,15 @@ Ring::index(row_t position)
return &m_cached_row;
}
+VteRowData const*
+Ring::index_safe(row_t position)
+{
+ if (G_UNLIKELY (position < m_start || position >= m_end))
+ return nullptr;
+
+ return index(position);
+}
+
/*
* Returns the hyperlink idx at the given position.
*
@@ -873,6 +882,7 @@ Ring::insert(row_t position, guint8 bidi_flags)
*get_writable_index(position) = tmp;
row = get_writable_index(position);
+ row->attr.bidi_flags = bidi_flags;
_vte_row_data_clear (row);
row->attr.bidi_flags = bidi_flags;
m_end++;
diff --git a/src/ring.hh b/src/ring.hh
index 06ed22fe..831a55da 100644
--- a/src/ring.hh
+++ b/src/ring.hh
@@ -73,6 +73,7 @@ public:
//FIXMEchpe rename this to at()
//FIXMEchpe use references not pointers
VteRowData const* index(row_t position); /* const? */
+ VteRowData const* index_safe(row_t position);
VteRowData* index_writable(row_t position);
void hyperlink_maybe_gc(row_t increment);
diff --git a/src/vte.cc b/src/vte.cc
index a76ffd1c..3ea62ec6 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -36,6 +36,7 @@
#include <vte/vte.h>
#include "vteinternal.hh"
+#include "bidi.hh"
#include "buffer.h"
#include "debug.h"
#include "vtedraw.hh"
@@ -263,6 +264,13 @@ Terminal::invalidate_rows(vte::grid::row_t row_start,
row_start, row_end);
_vte_debug_print (VTE_DEBUG_WORK, "?");
+ // HACK for BiDi: Always invalidate everything.
+ // In fact we'd need to invalidate the entire implicit paragraph.
+ if (TRUE) {
+ invalidate_all();
+ return;
+ }
+
/* Scrolled back, visible parts didn't change. */
if (row_start > last_displayed_row())
return;
@@ -388,7 +396,7 @@ Terminal::invalidate_all()
/* Find the row in the given position in the backscroll buffer.
* Note that calling this method may invalidate the return value of
* a previous find_row_data() call. */
-// FIXMEchpe replace this with a method on VteRing
+// FIXMEchpe replace this with a method on VteRing (index_safe())
VteRowData const*
Terminal::find_row_data(vte::grid::row_t row) const
{
@@ -1558,9 +1566,22 @@ Terminal::grid_coords_from_view_coords(vte::view::coords const& pos) const
vte::grid::row_t row = pixel_to_row(pos.y);
+ /* BiDi: convert to logical column. */
+ vte::base::BidiRow const* bidirow = m_ringview.get_row_map(confine_grid_row(row));
+ col = bidirow->vis2log(col);
+
return vte::grid::coords(row, col);
}
+vte::grid::row_t
+Terminal::confine_grid_row(vte::grid::row_t const& row) const
+{
+ auto first_row = first_displayed_row();
+ auto last_row = last_displayed_row();
+
+ return CLAMP(row, first_row, last_row);
+}
+
/*
* Terminal::confined_grid_coords_from_view_coords:
* @pos: the view coordinates
@@ -1622,34 +1643,46 @@ Terminal::confine_grid_coords(vte::grid::coords const& rowcol) const
/*
* Track mouse click and drag positions (the "origin" and "last" coordinates) with half cell accuracy,
- * that is, know whether the event occurred over the left or right half of the cell.
+ * that is, know whether the event occurred over the left/start or right/end half of the cell.
* This is required because some selection modes care about the cell over which the event occurred,
* while some care about the closest boundary between cells.
*
* Storing the actual view coordinates would become problematic when the font size changes (bug 756058),
* and would cause too much work when the mouse moves within the half cell.
*
- * Left margin or anything further to the left is denoted by column -1's right half,
- * right margin or anything further to the right is denoted by column m_column_count's left half.
+ * Left/start margin or anything further to the left/start is denoted by column -1's right half,
+ * right/end margin or anything further to the right/end is denoted by column m_column_count's left half.
+ *
+ * BiDi: returns logical (start/end) position for normal selection modes, visual (left/right) position for
block mode.
*/
vte::grid::halfcoords
Terminal::selection_grid_halfcoords_from_view_coords(vte::view::coords const& pos) const
{
vte::grid::row_t row = pixel_to_row(pos.y);
- vte::grid::halfcolumn_t halfcolumn;
+ vte::grid::column_t col;
+ vte::grid::half_t half;
if (pos.x < 0) {
- halfcolumn.set_column(-1);
- halfcolumn.set_half(1);
+ col = -1;
+ half = 1;
} else if (pos.x >= m_column_count * m_cell_width) {
- halfcolumn.set_column(m_column_count);
- halfcolumn.set_half(0);
+ col = m_column_count;
+ half = 0;
} else {
- halfcolumn.set_column(pos.x / m_cell_width);
- halfcolumn.set_half((pos.x * 2 / m_cell_width) % 2);
+ col = pos.x / m_cell_width;
+ half = (pos.x * 2 / m_cell_width) % 2;
+ }
+
+ if (!m_selection_block_mode) {
+ /* BiDi: convert from visual to logical half column. */
+ vte::base::BidiRow const* bidirow = m_ringview.get_row_map(confine_grid_row(row));
+
+ if (bidirow->vis_is_rtl(col))
+ half = 1 - half;
+ col = bidirow->vis2log(col);
}
- return { row, halfcolumn };
+ return { row, vte::grid::halfcolumn_t(col, half) };
}
/*
@@ -1666,6 +1699,8 @@ Terminal::selection_maybe_swap_endpoints(vte::view::coords const& pos)
if (m_selection_resolved.empty())
return;
+ ringview_maybe_update();
+
auto current = selection_grid_halfcoords_from_view_coords (pos);
if (m_selection_block_mode) {
@@ -3859,6 +3894,11 @@ Terminal::process_incoming()
queue_contents_changed();
}
+ /* BiDi properties might have changed, even when !modified.
+ * emit_pending_signals() requires the ringview to be updated. */
+ m_ringview.invalidate();
+ ringview_maybe_update();
+
emit_pending_signals();
if (invalidated_text) {
@@ -4974,6 +5014,39 @@ Terminal::widget_key_press(GdkEventKey *event)
/* If the above switch statement didn't do the job, try mapping
* it to a literal or capability name. */
if (handled == FALSE) {
+ /* In keyboard arrow swapping mode, the left and right arrows
+ * are swapped if the cursor stands inside an RTL paragraph. */
+ if (m_modes_private.VTE_BIDI_SWAP_ARROW_KEYS() &&
+ (keyval == GDK_KEY_Left ||
+ keyval == GDK_KEY_Right ||
+ keyval == GDK_KEY_KP_Left ||
+ keyval == GDK_KEY_KP_Right)) {
+ /* m_ringview is for the onscreen contents and the cursor may be
+ * offscreen, so use a temporary ringview for the cursor's row. */
+ vte::base::RingView *ringview = new vte::base::RingView();
+ ringview->set_ring(m_screen->row_data);
+ ringview->set_rows(m_screen->cursor.row, 1);
+ ringview->set_width(m_column_count);
+ ringview->maybe_update();
+ if (ringview->get_row_map(m_screen->cursor.row)->base_is_rtl()) {
+ switch (keyval) {
+ case GDK_KEY_Left:
+ keyval = GDK_KEY_Right;
+ break;
+ case GDK_KEY_Right:
+ keyval = GDK_KEY_Left;
+ break;
+ case GDK_KEY_KP_Left:
+ keyval = GDK_KEY_KP_Right;
+ break;
+ case GDK_KEY_KP_Right:
+ keyval = GDK_KEY_KP_Left;
+ break;
+ }
+ }
+ delete ringview;
+ }
+
_vte_keymap_map(keyval, m_modifiers,
m_modes_private.DEC_APPLICATION_CURSOR_KEYS(),
m_modes_private.DEC_APPLICATION_KEYPAD(),
@@ -5216,7 +5289,7 @@ Terminal::line_is_wrappable(vte::grid::row_t row) const
* In block mode, similarly to char mode, we care about vertical character boundary. (This is somewhat
* debatable, as results in asymmetrical behavior along the two axes: a rectangle can disappear by
* becoming zero wide, but not zero high.) We cannot take care of CJKs at the endpoints now because CJKs
- * can cross the boundary in any included row. Taking care of them needs to go to cell_is_selected().
+ * can cross the boundary in any included row. Taking care of them needs to go to cell_is_selected_vis().
* We don't care about used vs. unused cells either. The event coordinate is simply rounded to the
* nearest vertical cell boundary.
*/
@@ -5500,6 +5573,8 @@ Terminal::modify_selection (vte::view::coords const& pos)
{
g_assert (m_selecting);
+ ringview_maybe_update();
+
auto current = selection_grid_halfcoords_from_view_coords (pos);
if (current == m_selection_last)
@@ -5513,21 +5588,29 @@ Terminal::modify_selection (vte::view::coords const& pos)
resolve_selection();
}
-/* Check if a cell is selected or not. */
+/* Check if a cell is selected or not. BiDi: the coordinate is visual. */
bool
-Terminal::cell_is_selected(vte::grid::column_t col,
- vte::grid::row_t row) const
+Terminal::cell_is_selected_vis(vte::grid::column_t col,
+ vte::grid::row_t row) const
{
if (m_selection_block_mode) {
/* In block mode, make sure CJKs and TABs aren't cut in half. */
+ /* BiDi: convert to logical column... */
+ vte::base::BidiRow const* bidirow = m_ringview.get_row_map(row);
+ col = bidirow->vis2log(col);
while (col > 0) {
VteCell const* cell = find_charcell(col, row);
if (!cell || !cell->attr.fragment())
break;
col--;
}
+ /* ... and back to visual. */
+ col = bidirow->log2vis(col);
return m_selection_resolved.box_contains ({ row, col });
} else {
+ /* BiDi: convert to logical column. */
+ vte::base::BidiRow const* bidirow = m_ringview.get_row_map(row);
+ col = bidirow->vis2log(col);
/* In normal modes, resolve_selection() made sure to generate such boundaries for
m_selection_resolved. */
return m_selection_resolved.contains ({ row, col });
}
@@ -5988,6 +6071,10 @@ Terminal::match_hilite_update()
glong col = pos.x / m_cell_width;
glong row = pixel_to_row(pos.y);
+ /* BiDi: convert to logical column. */
+ vte::base::BidiRow const* bidirow = m_ringview.get_row_map(confine_grid_row(row));
+ col = bidirow->vis2log(col);
+
_vte_debug_print(VTE_DEBUG_EVENTS,
"Match hilite update (%ld, %ld) -> %ld, %ld\n",
pos.x, pos.y, col, row);
@@ -6192,6 +6279,9 @@ Terminal::get_text(vte::grid::row_t start_row,
GString *string;
struct _VteCharAttributes attr;
vte::color::rgb fore, back;
+ vte::base::RingView *ringview = nullptr;
+ vte::base::BidiRow const *bidirow = nullptr;
+ vte::grid::column_t col_vis;
if (attributes)
g_array_set_size (attributes, 0);
@@ -6202,26 +6292,50 @@ Terminal::get_text(vte::grid::row_t start_row,
if (start_col < 0)
start_col = 0;
- vte::grid::column_t next_first_column = block ? start_col : 0;
- vte::grid::column_t col = start_col;
+ if (block) {
+ /* Rectangular selection operates on the visual contents, not the logical.
+ * m_ringview corresponds to the currently onscreen bits, therefore does not
+ * necessarily include the entire selection.
+ * Modifying m_ringview and then reverting would be a bit cumbersome,
+ * creating a new one for the selection is simpler. */
+ ringview = new vte::base::RingView();
+ ringview->set_ring(m_screen->row_data);
+ ringview->set_rows(start_row, end_row - start_row + 1);
+ ringview->set_width(m_column_count);
+ ringview->maybe_update();
+ }
+
+ vte::grid::column_t col = block ? 0 : start_col;
vte::grid::row_t row;
- for (row = start_row; row < end_row + 1; row++, col = next_first_column) {
+ for (row = start_row; row < end_row + 1; row++, col = 0) {
VteRowData const* row_data = find_row_data(row);
gsize last_empty, last_nonempty;
vte::grid::column_t last_emptycol, last_nonemptycol;
- vte::grid::column_t line_last_column = (block || row == end_row) ? end_col : G_MAXLONG;
+ vte::grid::column_t line_last_column = (!block && row == end_row) ? end_col : m_column_count;
last_empty = last_nonempty = string->len;
last_emptycol = last_nonemptycol = -1;
attr.row = row;
- attr.column = col;
+ attr.column = col; // FIXME is attr.column supposed to contain logical or visual? What is it
used for?
pcell = NULL;
if (row_data != NULL) {
+ bidirow = ringview ? ringview->get_row_map(row) : nullptr;
while (col < line_last_column &&
(pcell = _vte_row_data_get (row_data, col))) {
- attr.column = col;
+ /* In block mode, we scan each row from its very beginning to its very end
in logical order,
+ * and here filter out the characters that are visually outside of the
block. */
+ if (bidirow) {
+ col_vis = bidirow->log2vis(col);
+ // FIXME handle CJK and friends consistently with cell_is_selected().
+ if (col_vis < start_col || col_vis >= end_col) {
+ col++;
+ continue;
+ }
+ }
+
+ attr.column = col; // FIXME ditto
/* If it's not part of a multi-column character,
* and passes the selection criterion, add it to
@@ -6320,6 +6434,8 @@ Terminal::get_text(vte::grid::row_t start_row,
}
}
+ delete ringview;
+
/* Sanity check. */
if (attributes != nullptr)
g_assert_cmpuint(string->len, ==, attributes->len);
@@ -6893,6 +7009,8 @@ Terminal::widget_motion_notify(GdkEventMotion *event)
{
bool handled = false;
+ ringview_maybe_update();
+
GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
auto pos = view_coords_from_event(base_event);
auto rowcol = grid_coords_from_view_coords(pos);
@@ -6901,6 +7019,8 @@ Terminal::widget_motion_notify(GdkEventMotion *event)
"Motion notify %s %s\n",
pos.to_string(), rowcol.to_string());
+ ringview_maybe_update();
+
read_modifiers(base_event);
switch (event->type) {
@@ -6958,6 +7078,8 @@ Terminal::widget_button_press(GdkEventButton *event)
bool handled = false;
gboolean start_selecting = FALSE, extend_selecting = FALSE;
+ ringview_maybe_update();
+
GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
auto pos = view_coords_from_event(base_event);
auto rowcol = grid_coords_from_view_coords(pos);
@@ -7107,6 +7229,8 @@ Terminal::widget_button_release(GdkEventButton *event)
{
bool handled = false;
+ ringview_maybe_update();
+
GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
auto pos = view_coords_from_event(base_event);
auto rowcol = grid_coords_from_view_coords(pos);
@@ -7224,6 +7348,8 @@ Terminal::widget_focus_out(GdkEventFocus *event)
void
Terminal::widget_enter(GdkEventCrossing *event)
{
+ ringview_maybe_update();
+
GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
auto pos = view_coords_from_event(base_event);
@@ -7241,6 +7367,8 @@ Terminal::widget_enter(GdkEventCrossing *event)
void
Terminal::widget_leave(GdkEventCrossing *event)
{
+ ringview_maybe_update();
+
GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
auto pos = view_coords_from_event(base_event);
@@ -8913,6 +9041,27 @@ Terminal::draw_cells_with_attributes(struct _vte_draw_text_request *items,
}
+void
+Terminal::ringview_maybe_update()
+{
+ m_ringview.set_ring (m_screen->row_data);
+ /* Due to possibly unaligned height and per-pixel scrolling, up to 2 more lines than the
+ * logical height can be partially visible.
+ * If a row is just underneath the current viewport, we still need to run BiDi on it
+ * because the top an outlined rectangle cursor shape peeks in into the viewport, and we need
+ * to know where the BiDi algorithm maps that cursor. However, +2 is still enough for this
+ * as long as the outline cursor is 1px thin. If we ever make it wider, we'll need +3.
+ */
+ m_ringview.set_rows ((long) m_screen->scroll_delta, m_row_count + 2);
+ m_ringview.set_width (m_column_count);
+ m_ringview.maybe_update ();
+}
+
+/* XXX tmp hack */
+#define _vte_row_data_get_visual(row_data_p, bidimap, col) \
+ row_data_p == nullptr ? nullptr : _vte_row_data_get(row_data_p, bidimap->vis2log(col))
+
+
/* Paint the contents of a given row at the given location. Take advantage
* of multiple-draw APIs by finding runs of characters with identical
* attributes and bundling them together. */
@@ -8932,10 +9081,12 @@ Terminal::draw_rows(VteScreen *screen_,
guint fore = VTE_DEFAULT_FG, nfore, back = VTE_DEFAULT_BG, nback, deco = VTE_DEFAULT_FG, ndeco;
gboolean hyperlink = FALSE, nhyperlink, hilite = FALSE, nhilite;
gboolean selected;
+ gboolean nrtl = FALSE, rtl; /* for debugging */
uint32_t attr = 0, nattr;
guint item_count;
const VteCell *cell;
VteRowData const* row_data;
+ vte::base::BidiRow const* bidirow;
auto const column_count = m_column_count;
uint32_t const attr_mask = m_allow_bold ? ~0 : ~VTE_ATTR_BOLD_MASK;
@@ -8963,25 +9114,28 @@ Terminal::draw_rows(VteScreen *screen_,
if (row_data == nullptr)
continue; /* Skip row. FIXME: just paint this row empty? */
+ bidirow = m_ringview.get_row_map(row);
i = j = 0;
/* Walk the line.
* Locate runs of identical bg colors within a row, and paint each run as a single
rectangle. */
do {
/* Get the first cell's contents. */
- cell = row_data ? _vte_row_data_get (row_data, i) : nullptr;
+ cell = row_data ? _vte_row_data_get_visual (row_data, bidirow, i) : nullptr;
/* Find the colors for this cell. */
- selected = cell_is_selected(i, row);
+ selected = cell_is_selected_vis(i, row);
determine_colors(cell, selected, &fore, &back, &deco);
+ rtl = bidirow->vis_is_rtl(i);
while (++j < column_count) {
/* Retrieve the next cell. */
- cell = row_data ? _vte_row_data_get (row_data, j) : nullptr;
+ cell = row_data ? _vte_row_data_get_visual (row_data, bidirow, j) : nullptr;
/* Resolve attributes to colors where possible and
* compare visual attributes to the first character
* in this chunk. */
- selected = cell_is_selected(j, row);
+ selected = cell_is_selected_vis(j, row);
determine_colors(cell, selected, &nfore, &nback, &ndeco);
- if (nback != back) {
+ nrtl = bidirow->vis_is_rtl(j);
+ if (nback != back || (_vte_debug_on (VTE_DEBUG_BIDI) && nrtl != rtl)) {
break;
}
}
@@ -8995,6 +9149,21 @@ Terminal::draw_rows(VteScreen *screen_,
row_height,
&bg, VTE_DRAW_OPAQUE);
}
+ if (G_UNLIKELY (_vte_debug_on (VTE_DEBUG_BIDI) && rtl)) {
+ /* Debug: Highlight RTL letters with a slightly different background. */
+ vte::color::rgb bg;
+ rgb_from_index<8, 8, 8>(back, bg);
+ bg.red = 0xC000 + (bg.red - 0xC000) / 2;
+ bg.green = 0xC000 + (bg.green - 0xC000) / 2;
+ bg.blue = 0xC000 + (bg.blue - 0xC000) / 2;
+ _vte_draw_fill_rectangle (
+ m_draw,
+ i * column_width,
+ y + row_height / 8,
+ (j - i) * column_width,
+ row_height * 3 / 4,
+ &bg, VTE_DRAW_OPAQUE);
+ }
/* We'll need to continue at the first cell which didn't
* match the first one in this set. */
i = j;
@@ -9025,15 +9194,18 @@ Terminal::draw_rows(VteScreen *screen_,
/* Ensure that drawing is restricted to the cell (plus the overdraw area) */
_vte_draw_autoclip_t clipper{m_draw, &rect};
+ bidirow = m_ringview.get_row_map(row);
+
/* Walk the line.
* Locate runs of identical attributes within a row, and draw each run using a single
draw_cells() call. */
item_count = 0;
for (col = 0; col < column_count; ) {
/* Get the character cell's contents. */
- cell = _vte_row_data_get (row_data, col);
+ cell = _vte_row_data_get_visual (row_data, bidirow, col);
if (cell == NULL) {
- /* There'll be no more real cells in this row. */
- break;
+ /* We're rendering BiDi text in visual order, so an unused cell can be
followed by a used one. */
+ col++;
+ continue;
}
nhyperlink = (m_allow_hyperlink && cell->attr.hyperlink_idx != 0);
@@ -9052,11 +9224,11 @@ Terminal::draw_rows(VteScreen *screen_,
/* Find the colors for this cell. */
nattr = cell->attr.attr;
- selected = cell_is_selected(col, row);
+ selected = cell_is_selected_vis(col, row);
determine_colors(cell, selected, &nfore, &nback, &ndeco);
nhilite = (nhyperlink && cell->attr.hyperlink_idx == m_hyperlink_hover_idx) ||
- (!nhyperlink && m_match != nullptr && m_match_span.contains(row, col));
+ (!nhyperlink && m_match != nullptr && m_match_span.contains(row,
bidirow->vis2log(col)));
/* See if it no longer fits the run. */
if (item_count > 0 &&
@@ -9111,10 +9283,12 @@ Terminal::draw_rows(VteScreen *screen_,
hilite = nhilite;
g_assert_cmpint (item_count, <, column_count);
- items[item_count].c = c;
+ items[item_count].c = bidirow->vis_get_shaped_char(col, c);
items[item_count].columns = j - col;
- items[item_count].x = col * column_width;
+ items[item_count].x = (col - (bidirow->vis_is_rtl(col) ? j - col - 1 : 0)) *
column_width;
items[item_count].y = y;
+ items[item_count].mirror = bidirow->vis_is_rtl(col);
+ items[item_count].box_mirror = !!(row_data->attr.bidi_flags & VTE_BIDI_BOX_MIRROR);
item_count++;
g_assert_cmpint (j, >, col);
@@ -9137,7 +9311,7 @@ Terminal::paint_cursor()
{
struct _vte_draw_text_request item;
vte::grid::row_t drow;
- vte::grid::column_t col;
+ vte::grid::column_t col, viscol;
int width, height, cursor_width;
guint style = 0;
guint fore, back, deco;
@@ -9170,6 +9344,9 @@ Terminal::paint_cursor()
/* Find the first cell of the character "under" the cursor.
* This is for CJK. For TAB, paint the cursor where it really is. */
+ VteRowData const *row_data = find_row_data(drow);
+ vte::base::BidiRow const *bidirow = m_ringview.get_row_map(drow);
+
auto cell = find_charcell(col, drow);
while (cell != NULL && cell->attr.fragment() && cell->c != '\t' && col > 0) {
col--;
@@ -9177,15 +9354,18 @@ Terminal::paint_cursor()
}
/* Draw the cursor. */
- item.c = (cell && cell->c) ? cell->c : ' ';
+ viscol = bidirow->log2vis(col);
+ item.c = (cell && cell->c) ? bidirow->vis_get_shaped_char(viscol, cell->c) : ' ';
item.columns = item.c == '\t' ? 1 : cell ? cell->attr.columns() : 1;
- item.x = col * width;
+ item.x = (viscol - ((cell && bidirow->vis_is_rtl(viscol)) ? cell->attr.columns() - 1 : 0)) * width;
item.y = row_to_pixel(drow);
+ item.mirror = bidirow->vis_is_rtl(viscol);
+ item.box_mirror = (row_data && (row_data->attr.bidi_flags & VTE_BIDI_BOX_MIRROR));
if (cell && cell->c != 0) {
style = _vte_draw_get_style(cell->attr.bold(), cell->attr.italic());
}
- selected = cell_is_selected(col, drow);
+ selected = cell_is_selected_vis(viscol, drow);
determine_cursor_colors(cell, selected, &fore, &back, &deco);
rgb_from_index<8, 8, 8>(back, bg);
@@ -9207,9 +9387,23 @@ Terminal::paint_cursor()
stem_width = (int) (((float) (m_char_ascent + m_char_descent)) *
m_cursor_aspect_ratio + 0.5);
stem_width = CLAMP (stem_width, VTE_LINE_WIDTH, m_cell_width);
+ // FIXME make an exception when the cursor is just after the last character:
+ // show it next to the last character (wherever it appears due to BiDi) rather than
+ // beyond the end of the string, as KDE/Qt does. Maybe not just for I-beam.
+
+ if (row_data && bidirow->vis_is_rtl(viscol))
+ x += item.columns * m_cell_width - stem_width;
+
_vte_draw_fill_rectangle(m_draw,
x, y + m_char_padding.top, stem_width, m_char_ascent +
m_char_descent,
&bg, VTE_DRAW_OPAQUE);
+
+ if (focus && row_data && bidirow->has_foreign())
+ _vte_draw_fill_rectangle(m_draw,
+ bidirow->vis_is_rtl(viscol) ? x - stem_width : x +
stem_width, y + m_char_padding.top,
+ stem_width, stem_width,
+ &bg, VTE_DRAW_OPAQUE);
+
break;
}
@@ -9293,12 +9487,19 @@ void
Terminal::paint_im_preedit_string()
{
int col, columns;
+ long row;
long width, height;
int i, len;
if (m_im_preedit.empty())
return;
+ /* Get the row's BiDi information. */
+ row = m_screen->cursor.row;
+ if (row < first_displayed_row() || row > last_displayed_row())
+ return;
+ vte::base::BidiRow const *bidirow = m_ringview.get_row_map(row);
+
/* Keep local copies of rendering information. */
width = m_cell_width;
height = m_cell_height;
@@ -9309,7 +9510,7 @@ Terminal::paint_im_preedit_string()
/* If the pre-edit string won't fit on the screen if we start
* drawing it at the cursor's position, move it left. */
- col = m_screen->cursor.col;
+ col = bidirow->log2vis(m_screen->cursor.col);
if (col + columns > m_column_count) {
col = MAX(0, m_column_count - columns);
}
@@ -9320,7 +9521,7 @@ Terminal::paint_im_preedit_string()
const char *preedit = m_im_preedit.c_str();
int preedit_cursor;
- items = g_new(struct _vte_draw_text_request, len);
+ items = g_new0(struct _vte_draw_text_request, len);
for (i = columns = 0; i < len; i++) {
items[i].c = g_utf8_get_char(preedit);
items[i].columns = _vte_unichar_width(items[i].c,
@@ -9387,6 +9588,8 @@ Terminal::widget_draw(cairo_t *cr)
if (region == NULL)
return;
+ ringview_maybe_update();
+
allocated_width = get_allocated_width();
allocated_height = get_allocated_height();
@@ -9519,7 +9722,8 @@ Terminal::widget_scroll(GdkEventScroll *event)
int button;
GdkEvent *base_event = reinterpret_cast<GdkEvent*>(event);
- auto rowcol = confined_grid_coords_from_event(base_event);
+
+ ringview_maybe_update();
read_modifiers(base_event);
@@ -9554,6 +9758,8 @@ Terminal::widget_scroll(GdkEventScroll *event)
"Scroll application by %d lines, smooth scroll delta set back to %f\n",
cnt, m_mouse_smooth_scroll_delta);
+ auto rowcol = confined_grid_coords_from_event(base_event);
+
button = cnt > 0 ? 5 : 4;
if (cnt < 0)
cnt = -cnt;
@@ -10398,8 +10604,13 @@ Terminal::process(bool emit_adj_changed)
process_incoming();
}
m_input_bytes = 0;
- } else
+ } else {
+ // FIXMEegmont why is it needed here, and is this the best place?
+ // Without this, sudden two-finger kinetic scrolls result in crash.
+ ringview_maybe_update();
+
emit_pending_signals();
+ }
return is_active;
}
diff --git a/src/vte/vteterminal.h b/src/vte/vteterminal.h
index 26ac236e..4ca6efb4 100644
--- a/src/vte/vteterminal.h
+++ b/src/vte/vteterminal.h
@@ -113,7 +113,7 @@ struct _VteTerminalClass {
/* The structure we return as the supplemental attributes for strings. */
struct _VteCharAttributes {
/*< private >*/
- long row, column;
+ long row, column; // FIXME clarify if column is logical or visual
PangoColor fore, back;
guint underline:1, strikethrough:1, columns:4;
};
diff --git a/src/vtedefines.hh b/src/vtedefines.hh
index 71896a76..3502fec1 100644
--- a/src/vtedefines.hh
+++ b/src/vtedefines.hh
@@ -136,3 +136,6 @@
/* Max depth of title stack */
#define VTE_WINDOW_TITLE_STACK_MAX_DEPTH (8)
+
+/* Maximum length of a paragraph, in lines, that might get proper BiDi treatment. */
+#define VTE_BIDI_PARAGRAPH_LENGTH_MAX 500
diff --git a/src/vtedraw.cc b/src/vtedraw.cc
index 215e27ee..aafb012b 100644
--- a/src/vtedraw.cc
+++ b/src/vtedraw.cc
@@ -26,6 +26,7 @@
#include <glib.h>
#include <gtk/gtk.h>
+#include "bidi.hh"
#include "vtedraw.hh"
#include "vtedefines.hh"
#include "debug.h"
@@ -1487,6 +1488,12 @@ _vte_draw_text_internal (struct _vte_draw *draw,
for (i = 0; i < n_requests; i++) {
vteunistr c = requests[i].c;
+
+ if (G_UNLIKELY (requests[i].mirror)) {
+ // FIXME what if 'c' is actually a real vteunistr?
+ vte_bidi_get_mirror_char (c, requests[i].box_mirror, &c);
+ }
+
struct unistr_info *uinfo = font_info_get_unistr_info (font, c);
union unistr_font_info *ufi = &uinfo->ufi;
int x, y;
diff --git a/src/vtedraw.hh b/src/vtedraw.hh
index 7352d376..7006b77d 100644
--- a/src/vtedraw.hh
+++ b/src/vtedraw.hh
@@ -44,6 +44,8 @@ struct _vte_draw;
struct _vte_draw_text_request {
vteunistr c;
gshort x, y, columns;
+ guint8 mirror : 1;
+ guint8 box_mirror : 1;
};
guint _vte_draw_get_style(gboolean bold, gboolean italic);
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index f8f296b9..262691bd 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -1791,6 +1791,12 @@ vte_get_features (void)
"+GNUTLS"
#else
"-GNUTLS"
+#endif
+ " "
+#ifdef WITH_FRIBIDI
+ "+BIDI"
+#else
+ "-BIDI"
#endif
;
}
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index d1878f9a..dee8db77 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -24,6 +24,7 @@
#include "vtetypes.hh"
#include "vtedraw.hh"
#include "reaper.hh"
+#include "bidi.hh"
#include "ring.hh"
#include "buffer.h"
#include "parser.hh"
@@ -400,7 +401,7 @@ public:
gboolean m_selecting_had_delta;
gboolean m_selection_block_mode; // FIXMEegmont move it into a 4th value in vte_selection_type?
enum vte_selection_type m_selection_type;
- vte::grid::halfcoords m_selection_origin, m_selection_last;
+ vte::grid::halfcoords m_selection_origin, m_selection_last; /* BiDi: logical in normal modes,
visual in m_selection_block_mode */
vte::grid::span m_selection_resolved;
/* Clipboard data information. */
@@ -618,6 +619,7 @@ public:
/* BiDi parameters outside of ECMA and DEC private modes */
guint m_bidi_rtl : 1;
+ vte::base::RingView m_ringview;
public:
@@ -728,6 +730,7 @@ public:
inline bool grid_coords_in_scrollback(vte::grid::coords const& rowcol) const { return rowcol.row() <
m_screen->insert_delta; }
+ vte::grid::row_t confine_grid_row(vte::grid::row_t const& row) const;
vte::grid::coords confine_grid_coords(vte::grid::coords const& rowcol) const;
vte::grid::coords confined_grid_coords_from_event(GdkEvent const* event) const;
vte::grid::coords confined_grid_coords_from_view_coords(vte::view::coords const& pos) const;
@@ -951,8 +954,8 @@ public:
void resolve_selection();
void selection_maybe_swap_endpoints(vte::view::coords const& pos);
void modify_selection(vte::view::coords const& pos);
- bool cell_is_selected(vte::grid::column_t col,
- vte::grid::row_t) const;
+ bool cell_is_selected_vis(vte::grid::column_t col,
+ vte::grid::row_t) const;
void reset_default_attributes(bool reset_hyperlink);
@@ -1328,6 +1331,8 @@ public:
vte::parser::StringTokeniser::const_iterator& token,
vte::parser::StringTokeniser::const_iterator const& endtoken) noexcept;
+ void ringview_maybe_update();
+
/* Sequence handlers */
bool m_line_wrapped; // signals line wrapped from character insertion
// Note: inlining the handlers seems to worsen the performance, so we don't do that
diff --git a/src/vteunistr.cc b/src/vteunistr.cc
index 69c47fe0..319bd6bf 100644
--- a/src/vteunistr.cc
+++ b/src/vteunistr.cc
@@ -162,6 +162,39 @@ _vte_unistr_get_base (vteunistr s)
return (gunichar) s;
}
+void
+_vte_unistr_append_to_gunichars (vteunistr s, GArray *a)
+{
+ if (G_UNLIKELY (s >= VTE_UNISTR_START)) {
+ struct VteUnistrDecomp *decomp;
+ decomp = &DECOMP_FROM_UNISTR (s);
+ _vte_unistr_append_to_gunichars (decomp->prefix, a);
+ s = decomp->suffix;
+ }
+ gunichar val = (gunichar) s;
+ g_array_append_val (a, val);
+}
+
+vteunistr
+_vte_unistr_replace_base (vteunistr s, gunichar c)
+{
+ g_return_val_if_fail (s < unistr_next, s);
+
+ if (G_LIKELY (_vte_unistr_get_base(s) == c))
+ return s;
+
+ GArray *a = g_array_new (FALSE, FALSE, sizeof (gunichar));
+ _vte_unistr_append_to_gunichars (s, a);
+ g_assert_cmpint(a->len, >=, 1);
+
+ s = c;
+ for (glong i = 1; i < a->len; i++)
+ s = _vte_unistr_append_unichar (s, g_array_index (a, gunichar, i));
+
+ g_array_free (a, TRUE);
+ return s;
+}
+
void
_vte_unistr_append_to_string (vteunistr s, GString *gs)
{
diff --git a/src/vteunistr.h b/src/vteunistr.h
index 9bde6b2c..84cb0562 100644
--- a/src/vteunistr.h
+++ b/src/vteunistr.h
@@ -73,6 +73,19 @@ _vte_unistr_append_unistr (vteunistr s, vteunistr t);
gunichar
_vte_unistr_get_base (vteunistr s);
+/**
+ * _vte_unistr_append_to_string:
+ * @s: a #vteunistr
+ * @c: Unicode character to replace the base character of @s.
+ *
+ * Creates a vteunistr value where the base character from @s is
+ * replaced by @c, while the combining characters from @s are carried over.
+ *
+ * Returns: the new #vteunistr value
+ */
+vteunistr
+_vte_unistr_replace_base (vteunistr s, gunichar c);
+
/**
* _vte_unistr_append_to_string:
* @s: a #vteunistr
@@ -84,6 +97,16 @@ _vte_unistr_get_base (vteunistr s);
void
_vte_unistr_append_to_string (vteunistr s, GString *gs);
+/**
+ * _vte_unistr_append_to_gunichars:
+ * @s: a #vteunistr
+ * @a: a #GArray of #gunichar items to append @s to
+ *
+ * Appends @s to @a.
+ **/
+void
+_vte_unistr_append_to_gunichars (vteunistr s, GArray *a);
+
/**
* _vte_unistr_strlen:
* @s: a #vteunistr
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]