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:
parent
601a85e12b
commit
0324ff66f0
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
|
@ -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(¶graph, direction);
|
||||
context.resolve_paragraph(¶graph, 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
|
||||
|
@ -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;
|
||||
|
@ -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>,
|
||||
|
||||
|
@ -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"]
|
||||
|
@ -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(¶graph, 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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] {
|
||||
|
@ -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" }
|
||||
|
@ -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 })
|
||||
}
|
||||
|
||||
|
@ -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}');
|
||||
|
@ -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!(
|
||||
|
@ -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
|
||||
|
@ -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" }
|
||||
|
@ -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(),
|
||||
|
@ -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]);
|
||||
});
|
||||
})
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user