port in more helper routines

This commit is contained in:
bunnie 2025-02-11 16:24:30 +08:00
parent 3890008af1
commit f7193ae29c
3 changed files with 866 additions and 0 deletions

View file

@ -0,0 +1,76 @@
#![cfg_attr(rustfmt, rustfmt_skip)]
// DO NOT MAKE EDITS HERE because this file is automatically generated.
// The order of these modules affects the link order in the loader, which is referred to in the graphics engine.
// To make changes, see <xous_root>/services/graphics-server/src/blitstr2/codegen/main.go
#![allow(dead_code)]
#[cfg(not(feature = "cramium-soc"))]
pub const FONT_BASE: usize = 0x2053_0000;
#[cfg(feature = "cramium-soc")]
pub const FONT_BASE: usize = 0x6002_0000;
#[cfg(not(feature = "cramium-soc"))]
pub const EMOJI_OFFSET: usize = 0x0;
#[cfg(not(feature = "cramium-soc"))]
pub const EMOJI_LEN: usize = 0xab00;
#[cfg(not(feature = "cramium-soc"))]
pub const ZH_OFFSET: usize = 0xab00;
#[cfg(not(feature = "cramium-soc"))]
pub const ZH_LEN: usize = 0x3fea0;
#[cfg(not(feature = "cramium-soc"))]
pub const JA_OFFSET: usize = 0x4a9a0;
#[cfg(not(feature = "cramium-soc"))]
pub const JA_LEN: usize = 0x56920;
#[cfg(not(feature = "cramium-soc"))]
pub const KR_OFFSET: usize = 0xa12c0;
#[cfg(not(feature = "cramium-soc"))]
pub const KR_LEN: usize = 0x61600;
#[cfg(not(feature = "cramium-soc"))]
pub const BOLD_OFFSET: usize = 0x1028c0;
#[cfg(not(feature = "cramium-soc"))]
pub const BOLD_LEN: usize = 0x19e0;
#[cfg(not(feature = "cramium-soc"))]
pub const MONO_OFFSET: usize = 0x1042a0;
#[cfg(not(feature = "cramium-soc"))]
pub const MONO_LEN: usize = 0x19e0;
#[cfg(not(feature = "cramium-soc"))]
pub const REGULAR_OFFSET: usize = 0x105c80;
#[cfg(not(feature = "cramium-soc"))]
pub const REGULAR_LEN: usize = 0x19e0;
#[cfg(not(feature = "cramium-soc"))]
pub const TALL_OFFSET: usize = 0x107660;
#[cfg(not(feature = "cramium-soc"))]
pub const TALL_LEN: usize = 0x6780;
#[cfg(not(feature = "cramium-soc"))]
pub const SMALL_OFFSET: usize = 0x10dde0;
#[cfg(not(feature = "cramium-soc"))]
pub const SMALL_LEN: usize = 0x19e0;
#[cfg(not(feature = "cramium-soc"))]
pub const FONT_TOTAL_LEN: usize = 0x10f7c0;
#[cfg(feature = "cramium-soc")]
pub const EMOJI_OFFSET: usize = 0x0;
#[cfg(feature = "cramium-soc")]
pub const EMOJI_LEN: usize = 0xab00;
#[cfg(feature = "cramium-soc")]
pub const BOLD_OFFSET: usize = 0xab00;
#[cfg(feature = "cramium-soc")]
pub const BOLD_LEN: usize = 0x19e0;
#[cfg(feature = "cramium-soc")]
pub const MONO_OFFSET: usize = 0xc4e0;
#[cfg(feature = "cramium-soc")]
pub const MONO_LEN: usize = 0x19e0;
#[cfg(feature = "cramium-soc")]
pub const REGULAR_OFFSET: usize = 0xdec0;
#[cfg(feature = "cramium-soc")]
pub const REGULAR_LEN: usize = 0x19e0;
#[cfg(feature = "cramium-soc")]
pub const TALL_OFFSET: usize = 0xf8a0;
#[cfg(feature = "cramium-soc")]
pub const TALL_LEN: usize = 0x6780;
#[cfg(feature = "cramium-soc")]
pub const SMALL_OFFSET: usize = 0x16020;
#[cfg(feature = "cramium-soc")]
pub const SMALL_LEN: usize = 0x19e0;
#[cfg(feature = "cramium-soc")]
pub const FONT_TOTAL_LEN: usize = 0x17a00;

View file

@ -0,0 +1,148 @@
// A set of macros that define the priority order for resolving fonts across language definitions.
macro_rules! style_wrapper {
($rule:ident, $base_style:ident, $ch:ident) => {
match &$base_style {
GlyphStyle::Small => {
$rule!(small_glyph, emoji_glyph, $ch)
}
GlyphStyle::Bold => {
$rule!(bold_glyph, emoji_glyph, $ch)
}
GlyphStyle::Monospace => {
$rule!(mono_glyph, emoji_glyph, $ch)
}
GlyphStyle::Large => {
$rule!(large_glyph, emoji_large_glyph, $ch)
}
GlyphStyle::ExtraLarge => {
$rule!(extra_large_glyph, emoji_large_glyph, $ch)
}
GlyphStyle::Tall => {
$rule!(tall_glyph, emoji_glyph, $ch)
}
// default to regular
_ => {
$rule!(regular_glyph, emoji_glyph, $ch)
}
}
};
}
macro_rules! zh_rules {
($base_style:expr, $emoji_style:expr, $ch:ident) => {
match zh_glyph($ch) {
Ok(g) => g,
_ => match $base_style($ch) {
Ok(g) => g,
_ => match $emoji_style($ch) {
Ok(g) => g,
_ => match ja_glyph($ch) {
Ok(g) => g,
_ => match kr_glyph($ch) {
Ok(g) => g,
_ => match $base_style(REPLACEMENT) {
Ok(g) => g,
_ => NULL_GLYPH_SPRITE,
},
},
},
},
},
}
};
}
macro_rules! jp_rules {
($base_style:expr, $emoji_style:expr, $ch:ident) => {
match ja_glyph($ch) {
Ok(g) => g,
_ => match $base_style($ch) {
Ok(g) => g,
_ => match $emoji_style($ch) {
Ok(g) => g,
_ => match zh_glyph($ch) {
Ok(g) => g,
_ => match kr_glyph($ch) {
Ok(g) => g,
_ => match $base_style(REPLACEMENT) {
Ok(g) => g,
_ => NULL_GLYPH_SPRITE,
},
},
},
},
},
}
};
}
macro_rules! kr_rules {
($base_style:expr, $emoji_style:expr, $ch:ident) => {
match kr_glyph($ch) {
Ok(g) => g,
_ => match $base_style($ch) {
Ok(g) => g,
_ => match $emoji_style($ch) {
Ok(g) => g,
_ => match ja_glyph($ch) {
Ok(g) => g,
_ => match zh_glyph($ch) {
Ok(g) => g,
_ => match $base_style(REPLACEMENT) {
Ok(g) => g,
_ => NULL_GLYPH_SPRITE,
},
},
},
},
},
}
};
}
macro_rules! en_audio_rules {
($base_style:expr, $emoji_style:expr, $ch:ident) => {
match $base_style($ch) {
Ok(g) => g,
_ => match $emoji_style($ch) {
Ok(g) => g,
_ => match $base_style(REPLACEMENT) {
Ok(g) => g,
_ => NULL_GLYPH_SPRITE,
},
},
}
};
}
macro_rules! english_rules {
($base_style:expr, $emoji_style:expr, $ch:ident) => {
match $base_style($ch) {
Ok(g) => g,
_ => match $emoji_style($ch) {
Ok(g) => g,
_ => match ja_glyph($ch) {
Ok(g) => g,
_ => match zh_glyph($ch) {
Ok(g) => g,
_ => match kr_glyph($ch) {
Ok(g) => g,
_ => match $base_style(REPLACEMENT) {
Ok(g) => g,
_ => NULL_GLYPH_SPRITE,
},
},
},
},
},
}
};
}
pub(crate) use en_audio_rules;
pub(crate) use english_rules;
pub(crate) use jp_rules;
pub(crate) use kr_rules;
pub(crate) use style_wrapper;
pub(crate) use zh_rules;

642
libs/ux-api/src/wordwrap.rs Normal file
View file

@ -0,0 +1,642 @@
/// Wordwrap stratgey
///
/// Strings are submitted to the Wordwrapper, and they are split into lines, and then into lexical words.
///
/// The rule for line splitting is simple: '\n' denotes a new line.
/// The rule for word splitting is done according to Rust's built-in "split_whitespace()" function.
///
/// Once split into words, each word is turned into a `TypesetWord` structure, which is a series of
/// GlyphSprites (e.g. references to bitmap font data), wrapped in a bounding box `bb` that denotes
/// exactly where it would be rendered in absolute coordinates on a canvas as defined by a `max` clipping
/// rectangle (which is the do-not-exceed area based on absolute screen coordinates) and a starting point
/// defined by a `bounds` record.
///
/// The exact GlyphSprite chosen is picked based on a hierarchy that starts with a hint based on
/// `locales::LANG`, then rules based on the `base_style: GlyphStyle` field, which allows for all the
/// text within a given string to be eg. small, regular, monospace, bold (mixing of different styles is
/// not yet supported, but could be in the future if we add some sort of markup parsing to the text
/// stream).
///
/// The location of the GlyphSprites do a "Best effort" to fit the words within the `bounds` based on the
/// designated rule without word-wrapping. If a single word overflows one line width, it will be broken
/// into two words at the closest boundary that does not overflow the text box, otherwise, it will be
/// moved to a new line on its own (e.g. no hyphenation rules are applied).
///
/// If the overall string cannot fit within the absolute bounds defined by the `max` area and/or the
/// `bounds`, the rendering is halted, and ellipses are inserted at the end.
use crate::blitstr2::{self, *};
use crate::minigfx::*;
use crate::platform::FB_SIZE;
use crate::style_macros::*;
impl TypesetWord {
pub fn new(origin: Pt, strpos: usize) -> Self {
TypesetWord {
gs: Vec::<GlyphSprite>::new(),
origin,
width: 0,
height: 0,
non_drawable: false,
strpos,
}
}
pub fn one_glyph(origin: Pt, gs: GlyphSprite, strpos: usize) -> Self {
TypesetWord {
gs: vec![gs],
origin,
width: gs.wide as isize,
height: gs.high as isize,
non_drawable: false,
strpos,
}
}
pub fn push(&mut self, gs: GlyphSprite) {
self.width += (gs.wide + gs.kern) as isize;
self.height = self.height.max(gs.high as isize);
self.gs.push(gs);
}
/// if the pop is invalid, we'll return an invalid character. Just...don't do that. k?
pub fn pop(&mut self) -> GlyphSprite {
let gs = self.gs.pop().unwrap_or(NULL_GLYPH_SPRITE);
self.width -= (gs.wide + gs.kern) as isize;
// we can't undo any height transformations, unfortunately, because we don't know what the previous
// state was but it's fairly minor if text is set funny on a line because text overflowed and
// had e.g. emoji buried amongst small font text...
gs
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum OverflowStrategy {
/// overflow text is truncated and replaced with an ellipsis
Ellipsis,
/// stop rendering at overflow
Abort,
/// render exactly one line of text; reset the renderer for a new line at the top left of the max bb area
/// This will yield only whole words if the next word could fit in a single line; otherwise, it will
/// split a longer-than-one-line word unceremoniously into multiple lines
///
/// This is intended for implementing e.g. scrollable text, where a text area is split
/// into multiple lines of typeset text, and then we can selectively render a range of text at a
/// given offset to simulate scrolling.
///
/// The function hasn't been tested at all, however.
#[allow(dead_code)]
// it's true, we haven't tested this mode yet. Remove this once we have tested it.
OneLineIterator,
}
/// ComposedType is text that has been laid out and wrapped, along with metadata about the bounds
/// of the final composition. ComposedType coordinates are always in screen-space.
pub(crate) struct ComposedType {
words: Vec<TypesetWord>,
bounding_box: ClipRect,
cursor: Cursor,
overflow: bool,
}
impl ComposedType {
pub fn new(words: Vec<TypesetWord>, bounds: ClipRect, cursor: Cursor, overflow: bool) -> Self {
ComposedType { words, bounding_box: bounds, cursor, overflow }
}
pub fn bb_width(&self) -> isize { self.bounding_box.max.x - self.bounding_box.min.x }
pub fn bb_height(&self) -> isize { self.bounding_box.max.y - self.bounding_box.min.y }
/// Note: it is up to the caller to ensure that clip_rect is within the renderable screen area. We do no
/// additional checks around this.
pub fn render(&self, frbuf: &mut [u32; FB_SIZE], offset: Point, invert: bool, clip_rect: Rectangle) {
const MAX_GLYPH_MARGIN: isize = 16;
// let mut strpos; // just for debugging insertion points
for word in self.words.iter() {
// strpos = word.strpos;
let mut point = word.origin.clone();
for glyph in word.gs.iter() {
// strpos += 1;
// the offset can actually be negative for good reasons, e.g., we're doing a scrollable
// buffer, but the blitstr2 was written assuming only positive offsets. Handle
// this here.
let maybe_x = offset.x + point.x as isize;
let maybe_y = offset.y + point.y as isize;
let mut renderable = true;
// allow MAX_GLYPH_MARGIN so we can get partial rendering of text that's slightly off screen
if maybe_x < (clip_rect.tl().x - MAX_GLYPH_MARGIN) || maybe_x > clip_rect.br().x {
log::trace!("not renderable maybe_x: {}, {:?}", maybe_x, clip_rect);
renderable = false;
}
if maybe_y < (clip_rect.tl().y - MAX_GLYPH_MARGIN) || maybe_y > clip_rect.br().y {
log::trace!("not renderable maybe_y: {}, {:?}", maybe_y, clip_rect);
renderable = false;
}
point.x += (glyph.wide + glyph.kern) as isize; // keep scorekeeping on this, because it could eventually become renderable
if !renderable {
// quickly short circuit over any text that is definitely outside of our clipping
// rectangle
continue;
} else {
let cr =
ClipRect::new(clip_rect.tl().x, clip_rect.tl().y, clip_rect.br().x, clip_rect.br().y);
if glyph.large {
blitstr2::xor_glyph_large(
frbuf,
&Point::new(maybe_x, maybe_y),
*glyph,
glyph.invert ^ invert,
cr,
);
} else if !glyph.double {
blitstr2::xor_glyph(
frbuf,
&Point::new(maybe_x, maybe_y),
*glyph,
glyph.invert ^ invert,
cr,
);
} else {
blitstr2::xor_glyph_2x(
frbuf,
&Point::new(maybe_x, maybe_y),
*glyph,
glyph.invert ^ invert,
cr,
);
}
if glyph.insert {
// log::info!("insert at {},{}", glyph.ch, strpos - 1);
// draw the insertion point after the glyph's position
crate::minigfx::op::line(
frbuf,
Line::new(
Point::new(maybe_x as isize - 1, maybe_y as _),
Point::new(maybe_x as isize - 1, maybe_y as isize + glyph.high as isize),
),
Some(clip_rect),
invert,
);
}
}
}
}
}
pub fn final_cursor(&self) -> Cursor { self.cursor }
pub fn final_overflow(&self) -> bool { self.overflow }
}
/// Typesetter takes a string and attempts to lay it out within a region defined by
/// a single point known as the "Extent". This is the maximum extent allowable for
/// the type.
///
/// The string may overflow the provided Extent. The strategy for handling the overflow
/// is specified with the `OverflowStrategy`, specified at the time of the typesetting call.
///
/// The Typesetter can run in essentially two modes: Line-by-line, or Fill-to-overflow. The
/// terminus of Fill-to-overflow can be either ellipses at the end, or a simple truncation.
///
/// When the Typesetter ends, the Typesetter will leave off in a way that Typesetting can
/// resume according to the specified mode. For Line-by-Line, it will resume Typesetting at
/// the top left of the bounding box. For other modes, it will resume at the point of overflow;
/// one must "rewind" the Typesetter, before calling it again.
///
/// The Typesetter returns a `ComposedType` object which is a `Vec` of `TypesetWord` along
/// with a helper field that summarizes the actual bounding box of the text, along with the
/// final cursor position.
///
/// An insertion point cursor will be injected into the TypesetWord stream at the character offset in
/// the input `string` if it is specified as `Some(usize)`.
pub(crate) struct Typesetter {
charpos: usize,
cursor: Cursor, /* indicates the current insertion point for a candidate. it is not updated as the
* candidates are formed. */
candidate: TypesetWord,
bb: ClipRect,
space: GlyphSprite,
ellipsis: GlyphSprite,
large_space: GlyphSprite,
insertion_point: Option<usize>,
s: String,
base_style: GlyphStyle,
overflow: bool,
max_width: isize,
last_line_height: usize, // scorecarding for the very last line on the loop exit
}
impl Typesetter {
pub fn setup(s: &str, extent: &Pt, base_style: &GlyphStyle, insertion_point: Option<usize>) -> Self {
let bb = ClipRect::new(0, 0, extent.x, extent.y);
let mut space = style_glyph(' ', base_style);
space.kern = 0;
let mut ellipsis = style_glyph('…', base_style);
ellipsis.kern = 0;
#[cfg(not(feature = "cramium-soc"))]
let mut large_space = style_glyph(' ', &GlyphStyle::Cjk);
#[cfg(feature = "cramium-soc")]
let mut large_space = style_glyph(' ', &GlyphStyle::Tall);
if cfg!(feature = "cramium-soc") {
large_space.wide = glyph_to_height_hint(GlyphStyle::Tall) as u8;
} else {
large_space.wide = glyph_to_height_hint(GlyphStyle::Cjk) as u8;
}
Typesetter {
charpos: 0,
cursor: Cursor::new(0, 0, 0),
candidate: TypesetWord::new(Pt::new(0, 0), 0), /* first word candidate starts at the top left
* corner */
bb,
space,
ellipsis,
large_space,
base_style: base_style.clone(),
s: String::from(s),
insertion_point,
overflow: false,
max_width: 0,
last_line_height: 0,
}
}
/// Wrap the words in the string until the space overflows, leaving ellipsis at the end.
/// Any prior result in self.words is overwritten.
///
/// For overflow strategies that are not OneLineIterator,
/// the caller must reset the cursor to the desired resume position, otherwise it will pick
/// up again at the overflow position.
///
/// The final Vec::<TypesetWord> is snapped to the top left of the Renderable region. This
/// needs to be transformed into the final screen coordinate space before blitting.
pub fn typeset(&mut self, strat: OverflowStrategy) -> ComposedType {
// a composition only lasts as long as the lifetime of this call, and is passed back to the caller
// at the end. Thus, we create it here and pass it out in the end. It's a bad idea to make it
// part of the core `struct` because then you'd have to wrap it in an Option to deal with the object
// going out of scope at the end of the call.
let mut composition = Vec::<TypesetWord>::new();
if self.bb.max.x - self.bb.min.x < glyph_to_height_hint(GlyphStyle::Regular) as isize {
// we flag this because the typesetter algorithm may never converge if it can't set any characters
// because the region is just too narrow.
log::error!("Words cannot be typset because the width of the typset region is too narrow.");
return ComposedType::new(
composition,
ClipRect::new(self.bb.min.x, self.bb.min.y, self.bb.min.x, self.bb.min.y),
self.cursor,
true,
);
}
// algorithm:
// - If not a whitespace or newline:
// 1. append a character to the current word (candidate)
// 2. Test if the new character would overflow. If so, move the word to a new line, or exit with
// overflow.
// Thus upon entry to this loop, the `candidate` is always guaranteed to be a fitting word at its
// current point of rendering.
// - If a whitespace:
// 1. put the `candidate` into the `words` field
// 2. Test to see if we can fit a whitespace at the end of this line. If we can't, insert a newline,
// or overflow.
// - If a newline:
// 1. put the `candidate` into the `words` field
// 2. Test to see if we can add a newline. If we can't, overflow.
let working_string = self.s.to_string(); // allocate a full copy to avoid interior mutability issues in the loop below. :-/ ugh.
// there's probably a more space-efficient way to deal with this using interior mutability but fuck
// it, I need to get this code working.
for ch in working_string.chars().skip(self.charpos) {
// .skip() allows us to resume typesetting where we last left off
if ch == '\n' {
// handle the explicit newline case
match strat {
OverflowStrategy::OneLineIterator => {
if self.candidate.gs.len() > 0 {
// this test is here in case we have multiple spaces or newlines in a row
self.commit_candidate_word(&mut composition);
}
self.oneline_epilogue();
break;
}
_ => {
if self.candidate.gs.len() > 0 {
// stash the word we just made; if this is 0, we're encounting multiple \n\n in a
// row
self.commit_candidate_word(&mut composition);
}
if self.is_newline_available() {
self.move_candidate_to_newline();
// hard newlines are marked by a non-drawable space at the beginning of a line
// this also allows us to place a cursor to "delete" a stray newline since it has
// the size and shape of a space
if !self.try_append_space(&mut composition) {
log::error!(
"Internal error: cursor was set to a newline, yet no space for new characters??"
);
}
} else {
self.overflow_typesetting(&mut composition, strat);
break;
}
}
}
} else if ch.is_whitespace() && (ch != '\t') {
if self.candidate.gs.len() > 0 {
// this test is here in case we have multiple spaces or newlines in a row
self.commit_candidate_word(&mut composition);
}
if !self.try_append_space(&mut composition) {
match strat {
OverflowStrategy::OneLineIterator => {
self.oneline_epilogue();
break;
}
_ => {
if self.is_newline_available() {
self.move_candidate_to_newline();
// the call below automatically handles the case of non-drawable spaces at the
// beginning of newlines
if !self.try_append_space(&mut composition) {
log::error!(
"Internal error: cursor was set to a newline, yet no space for new characters??"
);
}
} else {
self.overflow_typesetting(&mut composition, strat);
break;
}
}
}
}
} else {
// at this point, we get a glyph. This next glyph can cause one of the following situations to
// occur:
// 1. The evolving word fits.
// 2. The evolving word is longer than a single line, and there are more lines available.
// 3. The evolving word is longer than a single line, and there are no more lines available.
// 4. The evolving word fits a line but doesn't fit this line, and there is space on a new
// line for it.
// 5. The evolving word fits a line but doesn't fit this line, and there is no more space at
// all.
let mut gs =
if ch != '\t' { style_glyph(ch, &self.base_style) } else { self.large_space.clone() };
if self.is_insert_point() {
gs.insert = true;
}
self.candidate.push(gs.clone());
if self.is_word_longer_than_line() {
// cases 2 & 3
match strat {
OverflowStrategy::OneLineIterator => {
// case 2 is not an option with the OneLine strategy
// this exit is weird. We need to commit the partially typeset word, and reset the
// rendering state. remove the character that caused
// the overflow.
let _gs_pop = self.candidate.pop();
self.commit_candidate_word(&mut composition);
// we have to rewind the character position, because the commit_candidate_word
// side effects that, and doesn't know about the
// removed character. This is sort of ugly.
self.charpos -= 1;
// now reset us for the next call
self.oneline_epilogue();
// typesetting will resume at the broken-off word, because the charpos was left
// there.
break;
}
_ => {
// self.char_pos
// v|v gs_pop
// 012345|678
// abcdef|hij
if self.is_newline_available() {
// case 2
// commit the fragment of the word to the current line
let gs_pop = self.candidate.pop();
self.commit_candidate_word(&mut composition);
// set the cursor to the next line
self.move_candidate_to_newline();
// now set the overflowed character on the new line so our state is synched up
self.candidate.push(gs_pop);
} else {
// case 3
// similar to the one-line iterator exit, but with a call to overflow at the
// end.
let _gs_pop = self.candidate.pop();
self.commit_candidate_word(&mut composition);
self.charpos -= 1;
self.overflow_typesetting(&mut composition, strat);
break;
}
}
}
} else if !self.does_word_fit_on_line() {
// this is case 4 & 5 -- candidate is shorter than a line, but doesn't fit on this line
match strat {
OverflowStrategy::OneLineIterator => {
// case 4 is not an option
// now exit
self.reject_candidate_word();
self.oneline_epilogue();
break;
}
_ => {
if self.is_newline_available() {
// case 4
self.move_candidate_to_newline();
self.commit_candidate_glyph(&gs);
} else {
// case 5
self.reject_candidate_word();
self.overflow_typesetting(&mut composition, strat);
break;
}
}
}
} else {
// case 1 -- everything fits
self.commit_candidate_glyph(&gs);
}
}
}
if self.candidate.gs.len() > 0 {
self.commit_candidate_word(&mut composition);
}
let ret = ComposedType::new(
composition,
ClipRect::new(
self.bb.min.x,
self.bb.min.y,
self.max_width as isize,
self.cursor.pt.y + self.last_line_height as isize,
),
self.cursor,
self.overflow,
);
// cleanup any state based on the overflow strategy
match strat {
OverflowStrategy::OneLineIterator => {
self.max_width = 0; // it's a fresh line every time
assert!(
self.cursor.pt.x == self.bb.min.x,
"internal logic did not clean up the cursor state for the one line iterator"
)
}
_ => {
// other states "start where they left off"
}
}
ret
}
fn is_newline_available(&self) -> bool {
// repeated, bare newlines will have a candidate height of 0, as it contains no glyphs. correct for
// that.
let corrected_height =
if self.candidate.height == 0 { self.cursor.line_height as isize } else { self.candidate.height };
log::trace!(
"{} < {}",
corrected_height + self.cursor.pt.y + self.cursor.line_height as isize,
self.bb.max.y
);
corrected_height + self.cursor.pt.y + (self.cursor.line_height as isize) < self.bb.max.y
}
fn does_word_fit_on_line(&self) -> bool { self.candidate.width + self.cursor.pt.x < self.bb.max.x }
fn is_word_longer_than_line(&self) -> bool { self.candidate.width >= (self.bb.max.x - self.bb.min.x) }
fn is_insert_point(&self) -> bool {
if let Some(ip) = self.insertion_point {
if ip == self.charpos { true } else { false }
} else {
false
}
}
fn commit_candidate_word(&mut self, composition: &mut Vec<TypesetWord>) {
if !self.candidate.non_drawable {
// this is mainly for "non-drawable spaces" at the beginning of a line
self.max_width = self.max_width.max(self.candidate.width + self.candidate.origin.x);
self.cursor.pt.x += self.candidate.width;
self.cursor.line_height = self.cursor.line_height.max(self.candidate.height as usize);
self.last_line_height = self.cursor.line_height;
}
if false {
tsw_debug(&self.candidate)
};
composition.push(std::mem::replace(
&mut self.candidate,
// prepare a fresh "candidate" starting at the next character position
TypesetWord::new(self.cursor.pt, self.charpos),
));
}
fn commit_candidate_glyph(&mut self, _gs: &GlyphSprite) { self.charpos += 1; }
fn reject_candidate_word(&mut self) {
// back the rendering up to the starting point of the current word under consideration
self.charpos = self.candidate.strpos;
self.cursor.pt = self.candidate.origin;
self.cursor.line_height = 0;
// discard the current word, since we're going to start over again on the next call
self.candidate = TypesetWord::new(self.cursor.pt, self.charpos);
}
fn move_candidate_to_newline(&mut self) {
// advance the rendering line, without inserting a newline placeholder
self.last_line_height = self.cursor.line_height;
self.cursor.pt.y += self.cursor.line_height as isize;
self.cursor.pt.x = self.bb.min.x;
self.cursor.line_height = self.candidate.height as usize;
// now set the current candidate word's origin to the beginning of this new line
self.candidate.origin = self.cursor.pt;
}
fn overflow_typesetting(&mut self, composition: &mut Vec<TypesetWord>, strat: OverflowStrategy) {
self.overflow = true;
match strat {
OverflowStrategy::Ellipsis => {
let mut ov_word =
TypesetWord::one_glyph(self.cursor.pt.clone(), self.ellipsis.clone(), self.charpos);
if self.is_insert_point() {
ov_word.gs[0].insert = true;
}
composition.push(ov_word);
// the ellipsis is meta, so when we resume rendering it won't exist. thus don't update the
// cursor over it. self.cursor.update_glyph(&self.ellipsis);
}
_ => {
() // do nothing
}
}
}
/// tries to add a space after the current cursor point. If this would overflow the bb bounds,
/// it returns false.
/// The rule is, we must always enter this with a "brand new" TypesetWord entry with the charpos
/// set to our space point, because the caller will have already stashed the previously formed word
fn try_append_space(&mut self, composition: &mut Vec<TypesetWord>) -> bool {
assert!(self.candidate.gs.len() == 0, "self.candidate was not set to a new state prior to this call");
if (self.cursor.pt.x + self.space.wide as isize) < self.bb.max.x {
// our candidate word is "just as space"
let mut candidate_space = self.space.clone();
if self.is_insert_point() {
candidate_space.insert = true;
}
self.candidate.push(candidate_space);
self.commit_candidate_glyph(&candidate_space);
self.cursor.line_height = self.cursor.line_height.max(self.space.high as usize);
// if we're at the beginning of a line, mark the candidate word (that just contains a space) as
// non-drawable
if self.cursor.pt.x == self.bb.min.x {
self.candidate.non_drawable = true;
}
// now commit it
self.commit_candidate_word(composition);
true
} else {
false
}
}
/// resets the cursor state to the top left of the box for the next line to render.
fn oneline_epilogue(&mut self) {
self.cursor.pt.y = 0; // this should be redundant, as we never have more than one line in this mode
self.cursor.pt.x = self.bb.min.x;
if self.cursor.line_height == 0 {
// in case we have successive newlines, just default to the "regular" height
self.cursor.line_height = glyph_to_height_hint(GlyphStyle::Regular);
}
}
}
#[allow(dead_code)]
fn tsw_debug(tsw: &TypesetWord) {
let mut s = String::new();
for gs in tsw.gs.iter() {
s.push(gs.ch);
}
log::info!("{} @ {},{}+{}={}", &s, tsw.origin.x, tsw.origin.y, tsw.height, tsw.origin.y + tsw.height);
}
/// Find glyph for char using latin regular, emoji, ja, zh, and kr font data
pub fn style_glyph(ch: char, base_style: &GlyphStyle) -> GlyphSprite {
match locales::LANG {
"zh" => {
style_wrapper!(zh_rules, base_style, ch)
}
"jp" => {
style_wrapper!(jp_rules, base_style, ch)
}
"kr" => {
style_wrapper!(kr_rules, base_style, ch)
}
"en-tts" => {
style_wrapper!(en_audio_rules, base_style, ch)
}
// default to English rules
_ => {
style_wrapper!(english_rules, base_style, ch)
}
}
}