Skip to content

Commit

Permalink
Merge pull request #80 from kas-gui/work2
Browse files Browse the repository at this point in the history
BiDi: AutoRtl, text_is_rtl; default font size 16px
  • Loading branch information
dhardy authored Mar 1, 2024
2 parents 77c6cfc + 08f791a commit ba6b249
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 71 deletions.
38 changes: 37 additions & 1 deletion src/display/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use smallvec::SmallVec;

use crate::conv::to_usize;
use crate::{shaper, Action, Vec2};
use crate::{shaper, Action, Direction, Vec2};

mod glyph_pos;
mod text_runs;
Expand Down Expand Up @@ -219,6 +219,42 @@ impl TextDisplay {
Ok(self.lines.get(line).map(|line| line.text_range.to_std()))
}

/// Get the base directionality of the text
///
/// This does not require that the text is prepared.
pub fn text_is_rtl(&self, text: &str, direction: Direction) -> bool {
let cached_is_rtl = match self.line_is_rtl(0) {
Ok(None) => Some(direction == Direction::Rtl),
Ok(Some(is_rtl)) => Some(is_rtl),
Err(NotReady) => None,
};

#[cfg(not(debug_assertions))]
if let Some(cached) = cached_is_rtl {
return cached;
}

let (is_auto, mut is_rtl) = match direction {
Direction::Ltr => (false, false),
Direction::Rtl => (false, true),
Direction::Auto => (true, false),
Direction::AutoRtl => (true, true),
};

if is_auto {
match unicode_bidi::get_base_direction(text) {
unicode_bidi::Direction::Ltr => is_rtl = false,
unicode_bidi::Direction::Rtl => is_rtl = true,
unicode_bidi::Direction::Mixed => (),
}
}

if let Some(cached) = cached_is_rtl {
debug_assert_eq!(cached, is_rtl);
}
is_rtl
}

/// Get the directionality of the current line
///
/// Returns:
Expand Down
47 changes: 20 additions & 27 deletions src/display/text_runs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::conv::{to_u32, to_usize};
use crate::fonts::{fonts, FontId, InvalidFontId};
use crate::format::FormattableText;
use crate::{shaper, Action, Direction, Range};
use unicode_bidi::{BidiClass, BidiInfo, Level, LTR_LEVEL, RTL_LEVEL};
use unicode_bidi::{BidiClass, BidiInfo, LTR_LEVEL, RTL_LEVEL};
use xi_unicode::LineBreakIterator;

#[derive(Clone, Copy, Debug, PartialEq)]
Expand Down Expand Up @@ -113,33 +113,28 @@ impl TextDisplay {
let fonts = fonts();
let text = text.as_str();

let (bidi, default_para_level) = match direction {
Direction::Bidi => (true, None),
Direction::BidiRtl => (true, Some(RTL_LEVEL)),
Direction::Single => (false, None),
Direction::Ltr => (false, Some(LTR_LEVEL)),
Direction::Rtl => (false, Some(RTL_LEVEL)),
let default_para_level = match direction {
Direction::Auto => None,
Direction::AutoRtl => {
use unicode_bidi::Direction::*;
match unicode_bidi::get_base_direction(text) {
Ltr | Rtl => None,
Mixed => Some(RTL_LEVEL),
}
}
Direction::Ltr => Some(LTR_LEVEL),
Direction::Rtl => Some(RTL_LEVEL),
};
let level: Level;
let levels;
let classes;
if bidi || default_para_level.is_none() {
let info = BidiInfo::new(text, default_para_level);
levels = info.levels;
assert_eq!(text.len(), levels.len());
level = levels.first().cloned().unwrap_or(LTR_LEVEL);
classes = info.original_classes;
} else {
level = default_para_level.unwrap();
levels = vec![];
classes = vec![];
}
let info = BidiInfo::new(text, default_para_level);
let levels = info.levels;
assert_eq!(text.len(), levels.len());
let classes = info.original_classes;

let mut input = shaper::Input {
text,
dpem,
face_id: fonts.first_face_for(font_id)?,
level,
level: levels.first().cloned().unwrap_or(LTR_LEVEL),
};

let mut start = 0;
Expand Down Expand Up @@ -171,7 +166,7 @@ impl TextDisplay {
}

// Force end of current run?
let bidi_break = bidi && levels[pos] != input.level;
let bidi_break = levels[pos] != input.level;

let mut fmt_break = false;
if let Some(fmt) = next_fmt.as_ref() {
Expand Down Expand Up @@ -211,9 +206,7 @@ impl TextDisplay {

start = pos;
non_control_end = pos;
if bidi {
input.level = levels[pos];
}
input.level = levels[pos];
breaks = Default::default();
} else if is_break && !is_control {
// We do break runs when hitting control chars, but only when
Expand Down Expand Up @@ -263,7 +256,7 @@ impl TextDisplay {
);
match run.special {
RunSpecial::None => (),
RunSpecial::HardBreak => println!("HardBreak, "),
RunSpecial::HardBreak => print!("HardBreak, "),
RunSpecial::NoBreak => print!("NoBreak, "),
RunSpecial::HTab => print!("HTab, "),
}
Expand Down
25 changes: 7 additions & 18 deletions src/display/wrap_lines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::fonts::{fonts, FontLibrary};
use crate::shaper::GlyphRun;
use crate::{Action, Align, Range, Vec2};
use smallvec::SmallVec;
use unicode_bidi::Level;
use unicode_bidi::{Level, LTR_LEVEL};

#[derive(Clone, Debug)]
pub struct RunPart {
Expand Down Expand Up @@ -103,11 +103,6 @@ impl TextDisplay {
let justify = align.0 == Align::Stretch;
let mut parts = Vec::with_capacity(16);

// Almost everything in "this" method depends on the line direction, so
// we determine that then call the appropriate implementation.
let mut level = unicode_bidi::LTR_LEVEL;
let mut last_hard_break = true;

// Tuples: (index, part, num_parts)
let mut start = (0, 0, 0);
let mut end = start;
Expand All @@ -124,11 +119,6 @@ impl TextDisplay {
let allow_break = run.special != RunSpecial::NoBreak;
let tab = run.special == RunSpecial::HTab;

if last_hard_break {
level = run.level;
last_hard_break = false;
}

let mut last_part = start.1;
let mut part = last_part + 1;
while part <= num_parts {
Expand All @@ -153,7 +143,7 @@ impl TextDisplay {
if wrap && line_len > width_bound && end.2 > 0 {
// Add up to last valid break point then wrap and reset
let slice = &mut parts[0..end.2];
adder.add_line(fonts, level, &self.runs, slice, true);
adder.add_line(fonts, &self.runs, slice, true);

end.2 = 0;
start = end;
Expand Down Expand Up @@ -210,7 +200,7 @@ impl TextDisplay {
if parts.len() > 0 {
// It should not be possible for a line to end with a no-break, so:
debug_assert_eq!(parts.len(), end.2);
adder.add_line(fonts, level, &self.runs, &mut parts, false);
adder.add_line(fonts, &self.runs, &mut parts, false);
}

start = (index, 0, 0);
Expand All @@ -219,7 +209,6 @@ impl TextDisplay {

caret = 0.0;
index = start.0;
last_hard_break = true;
}
}

Expand Down Expand Up @@ -298,19 +287,18 @@ impl LineAdder {
fn add_line(
&mut self,
fonts: &FontLibrary,
line_level: Level,
runs: &[GlyphRun],
parts: &mut [PartInfo],
is_wrap: bool,
) {
assert!(parts.len() > 0);
let line_start = self.runs.len();
let line_is_rtl = line_level.is_rtl();

// Iterate runs to determine max ascent, level, etc.
let mut last_run = u32::MAX;
let (mut ascent, mut descent, mut line_gap) = (0f32, 0f32, 0f32);
let mut max_level = line_level;
let mut line_level = Level::new(Level::max_implicit_depth()).unwrap();
let mut max_level = LTR_LEVEL;
for part in parts.iter() {
if last_run == part.run {
continue;
Expand All @@ -323,9 +311,10 @@ impl LineAdder {
descent = descent.min(scale_font.descent());
line_gap = line_gap.max(scale_font.line_gap());

debug_assert!(run.level >= line_level);
line_level = line_level.min(run.level);
max_level = max_level.max(run.level);
}
let line_is_rtl = line_level.is_rtl();

if !self.lines.is_empty() {
self.vcaret += line_gap.max(self.line_gap);
Expand Down
56 changes: 39 additions & 17 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ use crate::Vec2;
/// An `Environment` can be default-constructed (without line-wrapping).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Environment {
/// Text direction
/// Base text direction
///
/// By default, text direction (LTR or RTL) is automatically detected with
/// full bi-directional text support (Unicode Technical Report #9).
/// Texts may be bi-directional as specified by Unicode Technical Report #9.
/// This value controls the base paragraph direction (TR9 HL1).
pub direction: Direction,

/// Line wrapping
///
/// By default, this is true and long text lines are wrapped based on the
Expand All @@ -28,8 +29,8 @@ pub struct Environment {
/// Alignment (`horiz`, `vert`)
///
/// By default, horizontal alignment is left or right depending on the
/// text direction (see [`Self::direction`]), and vertical alignment is
/// to the top.
/// text direction (see [`Self::direction`]), and vertical alignment
/// is to the top.
pub align: (Align, Align),
/// Default font
///
Expand Down Expand Up @@ -61,7 +62,7 @@ impl Default for Environment {
direction: Direction::default(),
wrap: true,
font_id: Default::default(),
dpem: 11.0 * 96.0 / 72.0,
dpem: 16.0,
bounds: Vec2::INFINITY,
align: Default::default(),
}
Expand Down Expand Up @@ -127,21 +128,42 @@ pub enum Align {

/// Directionality of text
///
/// This can be used to force the text direction.
/// Texts may be bi-directional as specified by Unicode Technical Report #9.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum Direction {
/// Auto-detect with bi-directional support
/// Auto-detect direction
///
/// The text direction is inferred from the first strongly-directional
/// character. In case no such character is found, the text will be
/// left-to-right.
#[default]
Bidi,
/// Auto-detect with bi-directional support, defaulting to right-to-left
BidiRtl,
/// Auto-detect, single line direction only
Single,
/// Force left-to-right text direction
Ltr,
/// Force right-to-left text direction
Rtl,
Auto = 2,
/// Auto-detect, default right-to-left
///
/// The text direction is inferred from the first strongly-directional
/// character. In case no such character is found, the text will be
/// right-to-left.
AutoRtl = 3,
/// The base text direction is left-to-right
///
/// If the text contains right-to-left content, this will be considered an
/// embedded right-to-left run. Non-directional leading and trailing
/// characters (e.g. a full stop) will normally not be included within this
/// right-to-left section.
///
/// This uses Unicode TR9 HL1 to set an explicit paragraph embedding level of 0.
Ltr = 0,
/// The base text direction is right-to-left
///
/// If the text contains left-to-right content, this will be considered an
/// embedded left-to-right run. Non-directional leading and trailing
/// characters (e.g. a full stop) will normally not be included within this
/// left-to-right section.
///
/// This uses Unicode TR9 HL1 to set an explicit paragraph embedding level of 1.
Rtl = 1,
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src/fonts/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl FaceId {
/// access a font with a (default-constructed) `FontId` will cause a panic in
/// the [`FontLibrary`] method used.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct FontId(pub u32);
pub struct FontId(u32);
impl FontId {
/// Get as `usize`
pub fn get(self) -> usize {
Expand Down
15 changes: 8 additions & 7 deletions src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::display::{Effect, MarkerPosIter, NotReady, TextDisplay};
use crate::fonts::{self, FaceId, InvalidFontId};
use crate::format::{EditableText, FormattableText};
use crate::{Action, Glyph, Vec2};
use crate::{Align, Direction, Environment};
use crate::{Align, Environment};

/// Text, prepared for display in a given environment
///
Expand Down Expand Up @@ -375,12 +375,13 @@ pub trait TextApiExt: TextApi {
self.display().line_range(line)
}

/// Get the directionality of the first line
fn text_is_rtl(&self) -> Result<bool, NotReady> {
Ok(match self.display().line_is_rtl(0)? {
None => matches!(self.env().direction, Direction::BidiRtl | Direction::Rtl),
Some(is_rtl) => is_rtl,
})
/// Get the base directionality of the text
///
/// This does not require that the text is prepared.
#[inline]
fn text_is_rtl(&self) -> bool {
self.display()
.text_is_rtl(self.as_str(), self.env().direction)
}

/// Get the directionality of the current line
Expand Down

0 comments on commit ba6b249

Please sign in to comment.