1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-22 04:56:12 +03:00

wezterm: add experimental_bidi config option and very basic bidi

This commit is larger than it appears to due fanout from threading
through bidi parameters.  The main changes are:

* When clustering cells, add an additional phase to resolve embedding
  levels and further sub-divide a cluster based on the resolved bidi
  runs; this is where we get the direction for a run and this needs
  to be passed through to the shaper.
* When doing bidi, the forced cluster boundary hack that we use to
  de-ligature when cursoring through text needs to be disabled,
  otherwise the cursor appears to push/rotate the text in that
  cluster when moving through it! We'll need to find a different
  way to handle shading the cursor that eliminates the original
  cursor/ligature/black issue.
* In the shaper, the logic for coalescing unresolved runs for font
  fallback assumed LTR and needed to be adjusted to cluster RTL.
  That meant also computing a little index of codepoint lengths.
* Added `experimental_bidi` boolean option that defaults to false.
  When enabled, it activates the bidi processing phase in clustering
  with a strong hint that the paragraph is LTR.

This implementation is incomplete and/or wrong for a number of cases:

* The config option should probably allow specifying the paragraph
  direction hint to use by default.
* https://terminal-wg.pages.freedesktop.org/bidi/recommendation/paragraphs.html
  recommends that bidi be applied to logical lines, not physical
  lines (or really: ranges within physical lines) that we're doing
  at the moment
* The paragraph direction hint should be overridden by cell attributes
  and other escapes; see 85a6b178cf

and probably others.

However, as of this commit, if you `experimental_bidi=true` then

```
echo This is RTL -> عربي فارسی bidi
```

(that text was sourced from:
https://github.com/microsoft/terminal/issues/538#issuecomment-677017322)

then wezterm will display the text in the same order as the text
renders in Chrome for that github comment.

```
; ./target/debug/wezterm --config experimental_bidi=false ls-fonts --text "عربي فارسی ->"
LeftToRight
 0 ع    \u{639}      x_adv=8  glyph=300  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 2 ر    \u{631}      x_adv=3.78125 glyph=273  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 4 ب    \u{628}      x_adv=4  glyph=244  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 6 ي    \u{64a}      x_adv=4  glyph=363  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 8      \u{20}       x_adv=8  glyph=2    wezterm.font("Operator Mono SSm Lig", {weight="DemiLight", stretch="Normal", italic=false})
                                      /Users/wez/.fonts/OperatorMonoSSmLig-Medium.otf, FontDirs
 9 ف    \u{641}      x_adv=11 glyph=328  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
11 ا    \u{627}      x_adv=4  glyph=240  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
13 ر    \u{631}      x_adv=3.78125 glyph=273  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
15 س    \u{633}      x_adv=10 glyph=278  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
17 ی    \u{6cc}      x_adv=4  glyph=664  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
19      \u{20}       x_adv=8  glyph=2    wezterm.font("Operator Mono SSm Lig", {weight="DemiLight", stretch="Normal", italic=false})
                                      /Users/wez/.fonts/OperatorMonoSSmLig-Medium.otf, FontDirs
20 -    \u{2d}       x_adv=8  glyph=276  wezterm.font("Operator Mono SSm Lig", {weight="DemiLight", stretch="Normal", italic=false})
                                      /Users/wez/.fonts/OperatorMonoSSmLig-Medium.otf, FontDirs
21 >    \u{3e}       x_adv=8  glyph=338  wezterm.font("Operator Mono SSm Lig", {weight="DemiLight", stretch="Normal", italic=false})
                                      /Users/wez/.fonts/OperatorMonoSSmLig-Medium.otf, FontDirs
```

```
; ./target/debug/wezterm --config experimental_bidi=true ls-fonts --text "عربي فارسی ->"
RightToLeft
17 ی    \u{6cc}      x_adv=9  glyph=906  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
15 س    \u{633}      x_adv=10 glyph=277  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
13 ر    \u{631}      x_adv=4.78125 glyph=272  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
11 ا    \u{627}      x_adv=4  glyph=241  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 9 ف    \u{641}      x_adv=5  glyph=329  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 8      \u{20}       x_adv=8  glyph=2    wezterm.font("Operator Mono SSm Lig", {weight="DemiLight", stretch="Normal", italic=false})
                                      /Users/wez/.fonts/OperatorMonoSSmLig-Medium.otf, FontDirs
 6 ي    \u{64a}      x_adv=9  glyph=904  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 4 ب    \u{628}      x_adv=4  glyph=243  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 2 ر    \u{631}      x_adv=5  glyph=273  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
 0 ع    \u{639}      x_adv=6  glyph=301  wezterm.font(".Geeza Pro Interface", {weight="Regular", stretch="Normal", italic=false})
                                      /System/Library/Fonts/GeezaPro.ttc index=2 variation=0, CoreText
LeftToRight
 0      \u{20}       x_adv=8  glyph=2    wezterm.font("Operator Mono SSm Lig", {weight="DemiLight", stretch="Normal", italic=false})
                                      /Users/wez/.fonts/OperatorMonoSSmLig-Medium.otf, FontDirs
 1 -    \u{2d}       x_adv=8  glyph=480  wezterm.font("Operator Mono SSm Lig", {weight="DemiLight", stretch="Normal", italic=false})
                                      /Users/wez/.fonts/OperatorMonoSSmLig-Medium.otf, FontDirs
 2 >    \u{3e}       x_adv=8  glyph=470  wezterm.font("Operator Mono SSm Lig", {weight="DemiLight", stretch="Normal", italic=false})
                                      /Users/wez/.fonts/OperatorMonoSSmLig-Medium.otf, FontDirs
;
```

refs: https://github.com/wez/wezterm/issues/784
This commit is contained in:
Wez Furlong 2022-01-25 08:16:51 -07:00
parent 601a85e12b
commit 0324ff66f0
17 changed files with 272 additions and 58 deletions

3
Cargo.lock generated
View File

@ -4125,6 +4125,7 @@ dependencies = [
"unicode-segmentation",
"varbincode",
"vtparse",
"wezterm-bidi",
"winapi 0.3.9",
]
@ -4728,6 +4729,7 @@ dependencies = [
"unicode-general-category",
"unicode-segmentation",
"walkdir",
"wezterm-bidi",
"wezterm-input-types",
"wezterm-term",
"wezterm-toast-notification",
@ -4795,6 +4797,7 @@ dependencies = [
"unicode-width",
"url",
"walkdir",
"wezterm-bidi",
"wezterm-client",
"wezterm-font",
"wezterm-gui-subcommands",

View File

@ -1,4 +1,4 @@
use wezterm_bidi::{BidiContext, Direction};
use wezterm_bidi::{BidiContext, Direction, ParagraphDirectionHint};
fn main() {
// The UBA is strongly coupled with codepoints and indices into the
@ -12,10 +12,10 @@ fn main() {
// Leave it to the algorithm to determine the paragraph direction.
// If you have some higher level understanding or override for the
// direction, you can set `direction` accordingly.
let direction: Option<Direction> = None;
let hint = ParagraphDirectionHint::AutoLeftToRight;
// Resolve the embedding levels for our paragraph.
context.resolve_paragraph(&paragraph, direction);
context.resolve_paragraph(&paragraph, hint);
/// In order to layout the text, we need to feed information to a shaper.
/// For the purposes of example, we're sketching out a stub shaper interface

View File

@ -15,6 +15,13 @@ pub use bidi_class::BidiClass;
pub use direction::Direction;
pub use level::Level;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParagraphDirectionHint {
LeftToRight,
RightToLeft,
AutoLeftToRight,
}
#[derive(Debug, Default)]
pub struct BidiContext {
orig_char_types: Vec<BidiClass>,
@ -275,9 +282,9 @@ impl BidiContext {
}
/// <http://unicode.org/reports/tr9/>
pub fn resolve_paragraph(&mut self, paragraph: &[char], dir: Option<Direction>) {
pub fn resolve_paragraph(&mut self, paragraph: &[char], hint: ParagraphDirectionHint) {
self.populate_char_types(paragraph);
self.resolve(dir, paragraph);
self.resolve(hint, paragraph);
}
/// BD1: The bidirectional character types are values assigned to each
@ -289,21 +296,21 @@ impl BidiContext {
.extend(paragraph.iter().map(|&c| bidi_class_for_char(c)));
}
pub fn set_char_types(&mut self, char_types: &[BidiClass], dir: Option<Direction>) {
pub fn set_char_types(&mut self, char_types: &[BidiClass], hint: ParagraphDirectionHint) {
self.orig_char_types.clear();
self.orig_char_types.extend(char_types);
self.resolve(dir, &[]);
self.resolve(hint, &[]);
}
fn resolve(&mut self, dir: Option<Direction>, paragraph: &[char]) {
fn resolve(&mut self, hint: ParagraphDirectionHint, paragraph: &[char]) {
trace!("\n**** resolve \n");
self.char_types.clear();
self.char_types.extend(self.orig_char_types.iter());
self.base_level = match dir {
Some(Direction::LeftToRight) => Level(0),
Some(Direction::RightToLeft) => Level(1),
None => paragraph_level(&self.char_types, false),
self.base_level = match hint {
ParagraphDirectionHint::LeftToRight => Level(0),
ParagraphDirectionHint::RightToLeft => Level(1),
ParagraphDirectionHint::AutoLeftToRight => paragraph_level(&self.char_types, false),
};
self.dump_state("before X1-X8");
@ -1857,7 +1864,7 @@ mod tests {
let text = vec!['א', 'ב', 'ג', 'a', 'b', 'c'];
let mut context = BidiContext::new();
context.resolve_paragraph(&text, None);
context.resolve_paragraph(&text, ParagraphDirectionHint::AutoLeftToRight);
k9::snapshot!(
context.runs().collect::<Vec<_>>(),
"
@ -1971,9 +1978,9 @@ mod tests {
context.resolve_paragraph(
&codepoints,
match direction {
0 => Some(Direction::LeftToRight),
1 => Some(Direction::RightToLeft),
2 => None,
0 => ParagraphDirectionHint::LeftToRight,
1 => ParagraphDirectionHint::RightToLeft,
2 => ParagraphDirectionHint::AutoLeftToRight,
_ => panic!("invalid direction code {}", direction),
},
);
@ -2086,15 +2093,15 @@ mod tests {
let inputs: Vec<BidiClass> = fields[0].split_whitespace().map(class_by_name).collect();
let bitset: u32 = fields[1].trim().parse().unwrap();
let mut directions: Vec<Option<Direction>> = vec![];
let mut directions: Vec<ParagraphDirectionHint> = vec![];
if bitset & 1 == 1 {
directions.push(None);
directions.push(ParagraphDirectionHint::AutoLeftToRight);
}
if bitset & 2 == 2 {
directions.push(Some(Direction::LeftToRight));
directions.push(ParagraphDirectionHint::LeftToRight);
}
if bitset & 4 == 4 {
directions.push(Some(Direction::RightToLeft));
directions.push(ParagraphDirectionHint::RightToLeft);
}
let mut printed_summary = false;

View File

@ -534,6 +534,9 @@ pub struct Config {
#[serde(default)]
pub experimental_pixel_positioning: bool,
#[serde(default)]
pub experimental_bidi: bool,
#[serde(default = "default_stateless_process_list")]
pub skip_close_confirmation_for_processes_named: Vec<String>,

View File

@ -37,6 +37,7 @@ thiserror = "1.0"
unicode-segmentation = "1.8"
ucd-trie = "0.1"
vtparse = { version="0.6", path="../vtparse" }
wezterm-bidi = { path = "../bidi" }
[features]
widgets = ["cassowary", "fnv"]

View File

@ -1,6 +1,7 @@
use crate::cell::{Cell, CellAttributes};
use crate::emoji::Presentation;
use std::borrow::Cow;
use wezterm_bidi::{BidiContext, Direction, ParagraphDirectionHint};
/// A `CellCluster` is another representation of a Line.
/// A `Vec<CellCluster>` is produced by walking through the Cells in
@ -13,6 +14,7 @@ pub struct CellCluster {
pub text: String,
pub width: usize,
pub presentation: Presentation,
pub direction: Direction,
byte_to_cell_idx: Vec<usize>,
byte_to_cell_width: Vec<u8>,
pub first_cell_idx: usize,
@ -46,6 +48,7 @@ impl CellCluster {
hint: usize,
iter: impl Iterator<Item = (usize, &'a Cell)>,
cursor_idx: Option<usize>,
bidi_hint: Option<ParagraphDirectionHint>,
) -> Vec<CellCluster> {
let mut last_cluster = None;
let mut clusters = Vec::new();
@ -64,7 +67,7 @@ impl CellCluster {
Cow::Borrowed(c.attrs())
};
let is_cursor_boundary = Some(cell_idx) == cursor_idx;
let is_cursor_boundary = bidi_hint.is_none() && Some(cell_idx) == cursor_idx;
let was_cursor = last_was_cursor;
last_was_cursor = is_cursor_boundary;
@ -143,7 +146,78 @@ impl CellCluster {
clusters.push(cluster);
}
clusters
if let Some(hint) = bidi_hint {
let mut resolved_clusters = vec![];
let mut context = BidiContext::new();
for cluster in clusters {
Self::resolve_bidi(&mut context, hint, cluster, &mut resolved_clusters);
}
resolved_clusters
} else {
clusters
}
}
fn resolve_bidi(
context: &mut BidiContext,
hint: ParagraphDirectionHint,
cluster: CellCluster,
resolved: &mut Vec<Self>,
) {
let mut paragraph = Vec::with_capacity(cluster.text.len());
let mut codepoint_index_to_byte_idx = Vec::with_capacity(cluster.text.len());
for (byte_idx, c) in cluster.text.char_indices() {
codepoint_index_to_byte_idx.push(byte_idx);
paragraph.push(c);
}
context.resolve_paragraph(&paragraph, hint);
for run in context.runs() {
let mut text = String::with_capacity(run.range.end - run.range.start);
let mut byte_to_cell_idx = vec![];
let mut byte_to_cell_width = vec![];
let mut width = 0usize;
let mut first_cell_idx = None;
for cp_idx in run.indices() {
let cp = paragraph[cp_idx];
text.push(cp);
let original_byte = codepoint_index_to_byte_idx[cp_idx];
let cell_width = cluster.byte_to_cell_width(original_byte);
width += cell_width as usize;
let cell_idx = cluster.byte_to_cell_idx(original_byte);
if first_cell_idx.is_none() {
first_cell_idx.replace(cell_idx);
}
if !cluster.byte_to_cell_width.is_empty() {
for _ in 0..cp.len_utf8() {
byte_to_cell_width.push(cell_width);
}
}
if !cluster.byte_to_cell_idx.is_empty() {
for _ in 0..cp.len_utf8() {
byte_to_cell_idx.push(cell_idx);
}
}
}
resolved.push(CellCluster {
attrs: cluster.attrs.clone(),
text,
width,
direction: run.direction,
presentation: cluster.presentation,
byte_to_cell_width,
byte_to_cell_idx,
first_cell_idx: first_cell_idx.unwrap(),
});
}
}
/// Start off a new cluster with some initial data
@ -182,6 +256,7 @@ impl CellCluster {
byte_to_cell_idx: idx,
byte_to_cell_width,
first_cell_idx: cell_idx,
direction: Direction::LeftToRight,
}
}

View File

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use std::ops::Range;
use std::sync::Arc;
use unicode_segmentation::UnicodeSegmentation;
use wezterm_bidi::ParagraphDirectionHint;
bitflags! {
#[cfg_attr(feature="use_serde", derive(Serialize, Deserialize))]
@ -726,8 +727,17 @@ impl Line {
})
}
pub fn cluster(&self, cursor_idx: Option<usize>) -> Vec<CellCluster> {
CellCluster::make_cluster(self.cells.len(), self.visible_cells(), cursor_idx)
pub fn cluster(
&self,
cursor_idx: Option<usize>,
bidi_hint: Option<ParagraphDirectionHint>,
) -> Vec<CellCluster> {
CellCluster::make_cluster(
self.cells.len(),
self.visible_cells(),
cursor_idx,
bidi_hint,
)
}
pub fn cells(&self) -> &[Cell] {

View File

@ -29,6 +29,7 @@ walkdir = "2"
wezterm-input-types = { path = "../wezterm-input-types" }
wezterm-term = { path = "../term", features=["use_serde"] }
wezterm-toast-notification = { path = "../wezterm-toast-notification" }
wezterm-bidi = { path = "../bidi" }
[target.'cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))'.dependencies]
fontconfig = { path = "../deps/fontconfig" }

View File

@ -98,6 +98,12 @@ impl Buffer {
unsafe { hb_buffer_allocation_successful(buf) } != 0,
"hb_buffer_create failed"
);
unsafe {
hb_buffer_set_content_type(
buf,
harfbuzz::hb_buffer_content_type_t::HB_BUFFER_CONTENT_TYPE_UNICODE,
)
};
Ok(Buffer { buf })
}

View File

@ -17,6 +17,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
use termwiz::cell::Presentation;
use thiserror::Error;
use wezterm_bidi::Direction;
use wezterm_term::CellAttributes;
use wezterm_toast_notification::ToastNotification;
@ -114,6 +115,7 @@ impl LoadedFont {
&self,
text: &str,
presentation: Option<Presentation>,
direction: Direction,
) -> anyhow::Result<Vec<GlyphInfo>> {
loop {
let (tx, rx) = channel();
@ -125,6 +127,7 @@ impl LoadedFont {
},
|_| {},
presentation,
direction,
) {
Ok(tuple) => tuple,
Err(err) if err.downcast_ref::<ClearShapeCache>().is_some() => {
@ -148,9 +151,15 @@ impl LoadedFont {
completion: F,
filter_out_synthetic: FS,
presentation: Option<Presentation>,
direction: Direction,
) -> anyhow::Result<Vec<GlyphInfo>> {
let (_async_resolve, res) =
self.shape_impl(text, completion, filter_out_synthetic, presentation)?;
let (_async_resolve, res) = self.shape_impl(
text,
completion,
filter_out_synthetic,
presentation,
direction,
)?;
Ok(res)
}
@ -160,6 +169,7 @@ impl LoadedFont {
completion: F,
filter_out_synthetic: FS,
presentation: Option<Presentation>,
direction: Direction,
) -> anyhow::Result<(bool, Vec<GlyphInfo>)> {
let mut no_glyphs = vec![];
@ -182,6 +192,7 @@ impl LoadedFont {
self.dpi,
&mut no_glyphs,
presentation,
direction,
);
no_glyphs.retain(|&c| c != '\u{FE0F}' && c != '\u{FE0E}');

View File

@ -11,6 +11,7 @@ use std::collections::HashMap;
use termwiz::cell::{unicode_column_width, Presentation};
use thiserror::Error;
use unicode_segmentation::UnicodeSegmentation;
use wezterm_bidi::Direction;
#[derive(Clone, Debug)]
struct Info {
@ -185,6 +186,7 @@ impl HarfbuzzShaper {
dpi: u32,
no_glyphs: &mut Vec<char>,
presentation: Option<Presentation>,
direction: Direction,
) -> anyhow::Result<Vec<GlyphInfo>> {
let mut buf = harfbuzz::Buffer::new()?;
// We deliberately omit setting the script and leave it to harfbuzz
@ -193,9 +195,20 @@ impl HarfbuzzShaper {
// <https://github.com/wez/wezterm/issues/1474> and
// <https://github.com/wez/wezterm/issues/1573>
// buf.set_script(harfbuzz::hb_script_t::HB_SCRIPT_LATIN);
buf.set_direction(harfbuzz::hb_direction_t::HB_DIRECTION_LTR);
buf.set_direction(match direction {
Direction::LeftToRight => harfbuzz::hb_direction_t::HB_DIRECTION_LTR,
Direction::RightToLeft => harfbuzz::hb_direction_t::HB_DIRECTION_RTL,
});
buf.set_language(self.lang);
buf.add_str(s);
let mut cluster_to_len = vec![];
for c in s.chars() {
let len = c.len_utf8();
for _ in 0..len {
cluster_to_len.push(len as u8);
}
}
buf.guess_segment_properties();
buf.set_cluster_level(
harfbuzz::hb_buffer_cluster_level_t::HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES,
@ -267,7 +280,15 @@ impl HarfbuzzShaper {
// but might potentially discover the text presentation for
// that glyph in a fallback font and swap it out a little
// later after a flash of showing the emoji one.
return self.do_shape(initial_font_idx, s, font_size, dpi, no_glyphs, None);
return self.do_shape(
initial_font_idx,
s,
font_size,
dpi,
no_glyphs,
None,
direction,
);
}
}
@ -291,14 +312,11 @@ impl HarfbuzzShaper {
let mut info_clusters: Vec<Vec<Info>> = Vec::with_capacity(s.len());
let mut info_iter = hb_infos.iter().zip(positions.iter()).peekable();
while let Some((info, pos)) = info_iter.next() {
let next_pos = info_iter
.peek()
.map(|(info, _)| info.cluster as usize)
.unwrap_or(s.len());
let len = cluster_to_len[info.cluster as usize] as usize;
let info = Info {
let mut info = Info {
cluster: info.cluster as usize,
len: next_pos - info.cluster as usize,
len,
codepoint: info.codepoint,
x_advance: pos.x_advance,
y_advance: pos.y_advance,
@ -311,13 +329,27 @@ impl HarfbuzzShaper {
cluster.push(info);
continue;
}
// Don't fragment runs of unresolve codepoints; they could be a sequence
// Don't fragment runs of unresolved codepoints; they could be a sequence
// that shapes together in a fallback font.
if info.codepoint == 0 {
let prior = cluster.last_mut().unwrap();
// This logic essentially merges `info` into `prior` by
// extending the length of prior by `info`.
// We can only do that if they are contiguous.
// Take care, as the shaper may have re-ordered things!
if prior.codepoint == 0 {
prior.len = next_pos - prior.cluster;
continue;
if prior.cluster + prior.len == info.cluster {
// Coalesce with prior
prior.len += info.len;
continue;
} else if info.cluster + info.len == prior.cluster {
// We actually precede prior; we must have been
// re-ordered by the shaper. Re-arrange and
// coalesce
std::mem::swap(&mut info, prior);
prior.len += info.len;
continue;
}
}
}
}
@ -333,7 +365,7 @@ impl HarfbuzzShaper {
for infos in &info_clusters {
let cluster_len: usize = infos.iter().map(|info| info.len).sum();
let cluster_start = infos.first().unwrap().cluster;
let cluster_start = infos.iter().map(|info| info.cluster).min().unwrap_or(0);
let substr = &s[cluster_start..cluster_start + cluster_len];
let incomplete = infos.iter().find(|info| info.codepoint == 0).is_some();
@ -355,6 +387,7 @@ impl HarfbuzzShaper {
dpi,
no_glyphs,
presentation,
direction,
) {
Ok(shape) => Ok(shape),
Err(e) => {
@ -366,6 +399,7 @@ impl HarfbuzzShaper {
dpi,
no_glyphs,
presentation,
direction,
)
}
}?;
@ -446,10 +480,11 @@ impl FontShaper for HarfbuzzShaper {
dpi: u32,
no_glyphs: &mut Vec<char>,
presentation: Option<Presentation>,
direction: Direction,
) -> anyhow::Result<Vec<GlyphInfo>> {
log::trace!("shape byte_len={} `{}`", text.len(), text.escape_debug());
let start = std::time::Instant::now();
let result = self.do_shape(0, text, size, dpi, no_glyphs, presentation);
let result = self.do_shape(0, text, size, dpi, no_glyphs, presentation, direction);
metrics::histogram!("shape.harfbuzz", start.elapsed());
/*
if let Ok(glyphs) = &result {
@ -602,7 +637,9 @@ mod test {
let shaper = HarfbuzzShaper::new(&config, &[handle]).unwrap();
{
let mut no_glyphs = vec![];
let info = shaper.shape("abc", 10., 72, &mut no_glyphs, None).unwrap();
let info = shaper
.shape("abc", 10., 72, &mut no_glyphs, None, Direction::LeftToRight)
.unwrap();
assert!(no_glyphs.is_empty(), "{:?}", no_glyphs);
k9::snapshot!(
info,
@ -650,7 +687,9 @@ mod test {
}
{
let mut no_glyphs = vec![];
let info = shaper.shape("<", 10., 72, &mut no_glyphs, None).unwrap();
let info = shaper
.shape("<", 10., 72, &mut no_glyphs, None, Direction::LeftToRight)
.unwrap();
assert!(no_glyphs.is_empty(), "{:?}", no_glyphs);
k9::snapshot!(
info,
@ -676,7 +715,9 @@ mod test {
// This is a ligatured sequence, but you wouldn't know
// from this info :-/
let mut no_glyphs = vec![];
let info = shaper.shape("<-", 10., 72, &mut no_glyphs, None).unwrap();
let info = shaper
.shape("<-", 10., 72, &mut no_glyphs, None, Direction::LeftToRight)
.unwrap();
assert!(no_glyphs.is_empty(), "{:?}", no_glyphs);
k9::snapshot!(
info,
@ -712,7 +753,9 @@ mod test {
}
{
let mut no_glyphs = vec![];
let info = shaper.shape("<--", 10., 72, &mut no_glyphs, None).unwrap();
let info = shaper
.shape("<--", 10., 72, &mut no_glyphs, None, Direction::LeftToRight)
.unwrap();
assert!(no_glyphs.is_empty(), "{:?}", no_glyphs);
k9::snapshot!(
info,
@ -761,7 +804,9 @@ mod test {
{
let mut no_glyphs = vec![];
let info = shaper.shape("x x", 10., 72, &mut no_glyphs, None).unwrap();
let info = shaper
.shape("x x", 10., 72, &mut no_glyphs, None, Direction::LeftToRight)
.unwrap();
assert!(no_glyphs.is_empty(), "{:?}", no_glyphs);
k9::snapshot!(
info,
@ -811,7 +856,14 @@ mod test {
{
let mut no_glyphs = vec![];
let info = shaper
.shape("x\u{3000}x", 10., 72, &mut no_glyphs, None)
.shape(
"x\u{3000}x",
10.,
72,
&mut no_glyphs,
None,
Direction::LeftToRight,
)
.unwrap();
assert!(no_glyphs.is_empty(), "{:?}", no_glyphs);
k9::snapshot!(

View File

@ -3,6 +3,7 @@ use crate::units::PixelLength;
use termwiz::cell::Presentation;
pub mod harfbuzz;
pub use wezterm_bidi::Direction;
/// Holds information about a shaped glyph
#[derive(Clone, Debug, PartialEq)]
@ -79,6 +80,7 @@ pub trait FontShaper {
dpi: u32,
no_glyphs: &mut Vec<char>,
presentation: Option<termwiz::cell::Presentation>,
direction: Direction,
) -> anyhow::Result<Vec<GlyphInfo>>;
/// Compute the font metrics for the preferred font

View File

@ -71,6 +71,7 @@ unicode-segmentation = "1.8"
unicode-width = "0.1"
url = "2"
walkdir = "2"
wezterm-bidi = { path = "../bidi" }
wezterm-client = { path = "../wezterm-client" }
wezterm-font = { path = "../wezterm-font" }
wezterm-gui-subcommands = { path = "../wezterm-gui-subcommands" }

View File

@ -18,6 +18,7 @@ use std::sync::Arc;
use structopt::StructOpt;
use termwiz::cell::CellAttributes;
use termwiz::surface::{Line, SEQ_ZERO};
use wezterm_bidi::{Direction, ParagraphDirectionHint};
use wezterm_client::domain::{ClientDomain, ClientDomainConfig};
use wezterm_gui_subcommands::*;
use wezterm_toast_notification::*;
@ -674,14 +675,20 @@ pub fn run_ls_fonts(config: config::ConfigHandle, cmd: &LsFontsCommand) -> anyho
config.dpi.unwrap_or_else(|| ::window::default_dpi()) as usize,
)?;
let bidi_hint = if config.experimental_bidi {
Some(ParagraphDirectionHint::LeftToRight)
} else {
None
};
if let Some(text) = &cmd.text {
let line = Line::from_text(text, &CellAttributes::default(), SEQ_ZERO);
let cell_clusters = line.cluster(None);
let cell_clusters = line.cluster(None, bidi_hint);
for cluster in cell_clusters {
let style = font_config.match_style(&config, &cluster.attrs);
let font = font_config.resolve_font(style)?;
let infos = font
.blocking_shape(&cluster.text, Some(cluster.presentation))
.blocking_shape(&cluster.text, Some(cluster.presentation), cluster.direction)
.unwrap();
// We must grab the handles after shaping, so that we get the
@ -689,12 +696,31 @@ pub fn run_ls_fonts(config: config::ConfigHandle, cmd: &LsFontsCommand) -> anyho
let handles = font.clone_handles();
let mut iter = infos.iter().peekable();
let mut byte_lens = vec![];
for c in cluster.text.chars() {
let len = c.len_utf8();
for _ in 0..len {
byte_lens.push(len);
}
}
println!("{:?}", cluster.direction);
while let Some(info) = iter.next() {
let idx = cluster.byte_to_cell_idx(info.cluster as usize);
let text = if let Some(ahead) = iter.peek() {
line.columns_as_str(idx..cluster.byte_to_cell_idx(ahead.cluster as usize))
let text = if cluster.direction == Direction::LeftToRight {
if let Some(next) = iter.peek() {
line.columns_as_str(idx..cluster.byte_to_cell_idx(next.cluster as usize))
} else {
let last_idx = cluster.byte_to_cell_idx(cluster.text.len() - 1);
line.columns_as_str(idx..last_idx + 1)
}
} else {
line.columns_as_str(idx..line.cells().len())
let info_len = byte_lens[info.cluster as usize];
let last_idx = cluster.byte_to_cell_idx(info.cluster as usize + info_len - 1);
line.columns_as_str(idx..last_idx + 1)
};
let parsed = &handles[info.font_idx];
@ -710,7 +736,8 @@ pub fn run_ls_fonts(config: config::ConfigHandle, cmd: &LsFontsCommand) -> anyho
}
println!(
"{:4} {:12} x_adv={:<2} glyph={:<4} {}\n{:38}{}",
"{:2} {:4} {:12} x_adv={:<2} glyph={:<4} {}\n{:38}{}",
info.cluster,
text,
escaped,
info.x_advance.get(),

View File

@ -261,6 +261,7 @@ mod test {
use std::rc::Rc;
use termwiz::cell::CellAttributes;
use termwiz::surface::{Line, SEQ_ZERO};
use wezterm_bidi::Direction;
use wezterm_font::{FontConfiguration, LoadedFont};
fn cluster_and_shape<T>(
@ -276,10 +277,12 @@ mod test {
{
let line = Line::from_text(text, &CellAttributes::default(), SEQ_ZERO);
eprintln!("{:?}", line);
let cell_clusters = line.cluster(None);
let cell_clusters = line.cluster(None, None);
assert_eq!(cell_clusters.len(), 1);
let cluster = &cell_clusters[0];
let infos = font.shape(&cluster.text, || {}, |_| {}, None).unwrap();
let infos = font
.shape(&cluster.text, || {}, |_| {}, None, Direction::LeftToRight)
.unwrap();
let glyphs = infos
.iter()
.map(|info| {
@ -394,11 +397,13 @@ mod test {
let style = TextStyle::default();
let font = fonts.resolve_font(&style).unwrap();
let line = Line::from_text(&text, &CellAttributes::default(), SEQ_ZERO);
let cell_clusters = line.cluster(None);
let cell_clusters = line.cluster(None, None);
let cluster = &cell_clusters[0];
measurer.measure(|| {
let _x = font.shape(&cluster.text, || {}, |_| {}, None).unwrap();
let _x = font
.shape(&cluster.text, || {}, |_| {}, None, Direction::LeftToRight)
.unwrap();
// println!("{:?}", &x[0..2]);
});
})

View File

@ -258,7 +258,7 @@ impl Element {
pub fn with_line(font: &Rc<LoadedFont>, line: &Line, palette: &ColorPalette) -> Self {
let mut content = vec![];
for cluster in line.cluster(None) {
for cluster in line.cluster(None, None) {
let child =
Element::new(font, ElementContent::Text(cluster.text)).colors(ElementColors {
border: BorderColor::default(),
@ -540,11 +540,13 @@ impl super::TermWindow {
match &element.content {
ElementContent::Text(s) => {
let window = self.window.as_ref().unwrap().clone();
let direction = wezterm_bidi::Direction::LeftToRight;
let infos = element.font.shape(
&s,
move || window.notify(TermWindowNotif::InvalidateShapeCache),
BlockKey::filter_out_synthetic,
element.presentation,
direction,
)?;
let mut computed_cells = vec![];
let mut glyph_cache = context.gl_state.glyph_cache.borrow_mut();

View File

@ -33,6 +33,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use termwiz::cell::{unicode_column_width, Blink};
use termwiz::cellcluster::CellCluster;
use termwiz::surface::{CursorShape, CursorVisibility};
use wezterm_bidi::ParagraphDirectionHint;
use wezterm_font::units::{IntPixelLength, PixelLength};
use wezterm_font::{ClearShapeCache, GlyphInfo, LoadedFont};
use wezterm_term::color::{ColorAttribute, ColorPalette, RgbColor};
@ -1759,6 +1760,12 @@ impl super::TermWindow {
let mut composition_width = 0;
let bidi_hint = if self.config.experimental_bidi {
Some(ParagraphDirectionHint::LeftToRight)
} else {
None
};
// Do we need to shape immediately, or can we use the pre-shaped data?
let to_shape = if let Some(composing) = composing {
// Create an updated line with the composition overlaid
@ -1769,11 +1776,11 @@ impl super::TermWindow {
CellAttributes::blank(),
termwiz::surface::SEQ_ZERO,
);
cell_clusters = line.cluster(cursor_idx);
cell_clusters = line.cluster(cursor_idx, bidi_hint);
composition_width = unicode_column_width(composing, None);
Some(&cell_clusters)
} else if params.pre_shaped.is_none() {
cell_clusters = params.line.cluster(cursor_idx);
cell_clusters = params.line.cluster(cursor_idx, bidi_hint);
Some(&cell_clusters)
} else {
None
@ -2678,6 +2685,7 @@ impl super::TermWindow {
move || window.notify(TermWindowNotif::InvalidateShapeCache),
BlockKey::filter_out_synthetic,
Some(cluster.presentation),
cluster.direction,
) {
Ok(info) => {
let glyphs = self.glyph_infos_to_glyphs(