Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
616 lines
22 KiB
Rust
616 lines
22 KiB
Rust
//! CodepointResolver maps a codepoint to a font. It is more dynamic
|
|
//! than "Collection" since it supports mapping codepoint ranges to
|
|
//! specific fonts, searching for fallback fonts, and more.
|
|
//!
|
|
//! To initialize the codepoint resolver, construct it with
|
|
//! [`CodepointResolver::new`], set the fields you want set, and begin using
|
|
//! the resolver.
|
|
|
|
#![allow(clippy::collapsible_if, clippy::if_not_else, clippy::float_cmp)]
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use crate::{
|
|
Atlas,
|
|
CodepointMap,
|
|
Glyph,
|
|
Presentation,
|
|
SpriteFace,
|
|
Style,
|
|
collection::{AddOptions, Collection, Entry, GetFaceError, Index, PresentationMode, Special},
|
|
discovery::Discover,
|
|
face::{RenderError as FaceRenderError, RenderOptions},
|
|
sprite,
|
|
};
|
|
|
|
/// Packed array of booleans to indicate if a style is enabled or not.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct StyleStatus([bool; 4]);
|
|
|
|
impl StyleStatus {
|
|
/// All styles set to `v`.
|
|
pub fn init_fill(v: bool) -> StyleStatus {
|
|
StyleStatus([v; 4])
|
|
}
|
|
|
|
pub fn get(&self, style: Style) -> bool {
|
|
self.0[style as usize]
|
|
}
|
|
|
|
pub fn set(&mut self, style: Style, v: bool) {
|
|
self.0[style as usize] = v;
|
|
}
|
|
}
|
|
|
|
/// Errors from [`CodepointResolver::render_glyph`].
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum RenderGlyphError {
|
|
#[error(transparent)]
|
|
GetFace(#[from] GetFaceError),
|
|
#[error(transparent)]
|
|
Face(#[from] FaceRenderError),
|
|
#[error(transparent)]
|
|
Sprite(#[from] sprite::face::RenderError),
|
|
/// The sprite face was not configured on the resolver.
|
|
#[error("sprite face not configured")]
|
|
SpriteUnavailable,
|
|
}
|
|
|
|
impl RenderGlyphError {
|
|
/// True if this error is the atlas-full condition (the caller should
|
|
/// grow the atlas and retry).
|
|
pub fn is_atlas_full(&self) -> bool {
|
|
use crate::atlas::Error::AtlasFull;
|
|
matches!(
|
|
self,
|
|
RenderGlyphError::Face(FaceRenderError::Atlas(AtlasFull))
|
|
| RenderGlyphError::Sprite(sprite::face::RenderError::Atlas(AtlasFull))
|
|
)
|
|
}
|
|
}
|
|
|
|
pub struct CodepointResolver {
|
|
/// The underlying collection of fonts. This will be modified as
|
|
/// new fonts are found via the resolver. The resolver takes ownership
|
|
/// of the collection.
|
|
pub collection: Collection,
|
|
|
|
/// The set of statuses and whether they're enabled or not. This defaults
|
|
/// to true. This can be changed at runtime with no ill effect.
|
|
pub styles: StyleStatus,
|
|
|
|
/// If discovery is available, we'll look up fonts where we can't find
|
|
/// the codepoint. This can be set after initialization.
|
|
pub discover: Option<Discover>,
|
|
|
|
/// A map of codepoints to font requests for codepoint-level overrides.
|
|
pub codepoint_map: Option<CodepointMap>,
|
|
|
|
/// The descriptor cache is used to cache the descriptor to font face
|
|
/// mapping for codepoint maps.
|
|
descriptor_cache: DescriptorCache,
|
|
|
|
/// Set this to a non-null value to enable sprite glyph drawing. If this
|
|
/// isn't enabled we'll just fall through to trying to use regular fonts
|
|
/// to render sprite glyphs. But more than likely, if this isn't set then
|
|
/// terminal rendering will look wrong.
|
|
pub sprite: Option<SpriteFace>,
|
|
}
|
|
|
|
/// Map of descriptors to faces. This is used with manual codepoint maps
|
|
/// to ensure that we don't load the same font multiple times.
|
|
///
|
|
/// Note that the current implementation will load the same font multiple
|
|
/// times if the font used for a codepoint map is identical to a font used
|
|
/// for a regular style. That's just an inefficient choice made now because
|
|
/// the implementation is simpler and codepoint maps matching a regular
|
|
/// font is a rare case.
|
|
///
|
|
/// As in upstream, descriptors are compared by their `hashcode()` only.
|
|
#[derive(Default)]
|
|
struct DescriptorCache(HashMap<u64, Option<Index>>);
|
|
|
|
impl CodepointResolver {
|
|
/// A resolver with defaults: all styles enabled, no discovery, no
|
|
/// codepoint map, no sprite face.
|
|
pub fn new(collection: Collection) -> CodepointResolver {
|
|
CodepointResolver {
|
|
collection,
|
|
styles: StyleStatus::init_fill(true),
|
|
discover: None,
|
|
codepoint_map: None,
|
|
descriptor_cache: DescriptorCache::default(),
|
|
sprite: None,
|
|
}
|
|
}
|
|
|
|
/// Looks up the font that should be used for a specific codepoint.
|
|
/// The font index is valid as long as font faces aren't removed. This
|
|
/// isn't cached; it is expected that downstream users handle caching if
|
|
/// that is important.
|
|
///
|
|
/// Optionally, a presentation format can be specified. This presentation
|
|
/// format will be preferred but if it can't be found in this format,
|
|
/// any format will be accepted. If presentation is null, the UCD
|
|
/// (Unicode Character Database) will be used to determine the default
|
|
/// presentation for the codepoint.
|
|
///
|
|
/// This function cannot error! If an error occurs for any reason,
|
|
/// the associated functionality is ignored and the
|
|
/// resolver attempts to use a different method to satisfy the codepoint.
|
|
/// This behavior is intentional to make the resolver apply best-effort
|
|
/// logic to satisfy the codepoint since its better to render something
|
|
/// than nothing.
|
|
///
|
|
/// This logic is relatively complex so the exact algorithm is documented
|
|
/// here. If this gets out of sync with the code, ask questions.
|
|
///
|
|
/// 1. If a font style is requested that is disabled (i.e. bold),
|
|
/// we start over with the regular font style. The regular font style
|
|
/// cannot be disabled, but it can be replaced with a stylized font
|
|
/// face.
|
|
///
|
|
/// 2. If there is a codepoint override for the codepoint, we satisfy
|
|
/// that requirement if we can, no matter what style or presentation.
|
|
///
|
|
/// 3. If this is a sprite codepoint (such as an underline), then the
|
|
/// sprite font always is the result.
|
|
///
|
|
/// 4. If the exact style and presentation request can be satisfied by
|
|
/// one of our loaded fonts, we return that value. We search loaded
|
|
/// fonts in the order they're added to the group, so the caller must
|
|
/// set the priority order.
|
|
///
|
|
/// 5. If the style isn't regular, we restart this process at this point
|
|
/// but with the regular style. This lets us fall back to regular with
|
|
/// our loaded fonts before trying a fallback. We'd rather show a regular
|
|
/// version of a codepoint from a loaded font than find a new font in
|
|
/// the correct style because styles in other fonts often change
|
|
/// metrics like glyph widths.
|
|
///
|
|
/// 6. If the style is regular, and font discovery is enabled, we look
|
|
/// for a fallback font to satisfy our request.
|
|
///
|
|
/// 7. Finally, as a last resort, we fall back to restarting this whole
|
|
/// process with a regular font face satisfying ANY presentation for
|
|
/// the codepoint. If this fails, we return null.
|
|
pub fn get_index(&mut self, cp: u32, style: Style, p: Option<Presentation>) -> Option<Index> {
|
|
// If we've disabled a font style, then fall back to regular.
|
|
if style != Style::Regular && !self.styles.get(style) {
|
|
return self.get_index(cp, Style::Regular, p);
|
|
}
|
|
|
|
// Codepoint overrides.
|
|
if let Some(idx) = self.get_index_codepoint_override(cp) {
|
|
return Some(idx);
|
|
}
|
|
|
|
// If we have sprite drawing enabled, check if our sprite face can
|
|
// handle this.
|
|
if let Some(sprite) = &self.sprite {
|
|
if sprite.has_codepoint(cp, p) {
|
|
return Some(Index::init_special(Special::Sprite));
|
|
}
|
|
}
|
|
|
|
// Build our presentation mode. If we don't have an explicit presentation
|
|
// given then we use the UCD (Unicode Character Database) to determine
|
|
// the default presentation. Note there is some inefficiency here because
|
|
// we'll do this multiple times if we recurse, but this is a cached function
|
|
// call higher up (GroupCache) so this should be rare.
|
|
let p_mode = match p {
|
|
Some(v) => PresentationMode::Explicit(v),
|
|
None => PresentationMode::Default(if ghostty_unicode::emoji_table::table().get(cp) {
|
|
Presentation::Emoji
|
|
} else {
|
|
Presentation::Text
|
|
}),
|
|
};
|
|
|
|
// If we can find the exact value, then return that.
|
|
if let Some(value) = self.collection.get_index(cp, style, p_mode) {
|
|
return Some(value);
|
|
}
|
|
|
|
// If we're not a regular font style, try looking for a regular font
|
|
// that will satisfy this request. Blindly looking for unmatched styled
|
|
// fonts to satisfy one codepoint results in some ugly rendering.
|
|
if style != Style::Regular {
|
|
if let Some(value) = self.get_index(cp, Style::Regular, p) {
|
|
return Some(value);
|
|
}
|
|
}
|
|
|
|
// If we are regular, try looking for a fallback using discovery.
|
|
if style == Style::Regular {
|
|
if let Some(idx) = self.discover_fallback(cp, style, p_mode) {
|
|
return Some(idx);
|
|
}
|
|
}
|
|
|
|
// If this is regular with any matching presentation, then we are done
|
|
// there is nothing more we can do. Otherwise we fall through and do
|
|
// an any presentation search.
|
|
if style == Style::Regular && p_mode == PresentationMode::Any {
|
|
return None;
|
|
}
|
|
|
|
// For non-regular fonts, we fall back to regular with any presentation
|
|
self.collection
|
|
.get_index(cp, Style::Regular, PresentationMode::Any)
|
|
}
|
|
|
|
fn discover_fallback(
|
|
&mut self,
|
|
cp: u32,
|
|
style: Style,
|
|
p_mode: PresentationMode,
|
|
) -> Option<Index> {
|
|
log::debug!("searching for a fallback font for cp={cp:X}");
|
|
let disco = self.discover.as_ref()?;
|
|
let load_opts = self.collection.load_options.as_ref()?;
|
|
let desc = crate::discovery::Descriptor {
|
|
codepoint: cp,
|
|
size: load_opts.size.points,
|
|
bold: style == Style::Bold || style == Style::BoldItalic,
|
|
italic: style == Style::Italic || style == Style::BoldItalic,
|
|
monospace: false,
|
|
..Default::default()
|
|
};
|
|
let mut disco_it = disco.discover_fallback(&mut self.collection, &desc).ok()?;
|
|
|
|
loop {
|
|
let deferred_face = match disco_it.next() {
|
|
Ok(Some(f)) => f,
|
|
Ok(None) => break,
|
|
Err(err) => {
|
|
log::warn!("fallback search failed with error err={err}");
|
|
break;
|
|
}
|
|
};
|
|
|
|
// Discovery is supposed to only return faces that have our
|
|
// codepoint but we can't search presentation in discovery so
|
|
// we have to check it here.
|
|
let entry = Entry {
|
|
face: crate::collection::AnyFace::Deferred(deferred_face),
|
|
fallback: true,
|
|
scale_factor: crate::collection::ScaleFactor::Scale(1.0),
|
|
};
|
|
if !entry.has_codepoint(cp, p_mode) {
|
|
continue;
|
|
}
|
|
let crate::collection::AnyFace::Deferred(deferred_face) = entry.face else {
|
|
unreachable!()
|
|
};
|
|
|
|
log::info!(
|
|
"found codepoint 0x{cp:X} in fallback face={}",
|
|
deferred_face
|
|
.name()
|
|
.unwrap_or_else(|_| "<error>".to_string())
|
|
);
|
|
return self
|
|
.collection
|
|
.add_deferred(
|
|
deferred_face,
|
|
AddOptions {
|
|
style,
|
|
fallback: true,
|
|
size_adjustment: crate::DEFAULT_FALLBACK_ADJUSTMENT,
|
|
},
|
|
)
|
|
.ok();
|
|
}
|
|
|
|
log::debug!("no fallback face found for cp={cp:X}");
|
|
None
|
|
}
|
|
|
|
/// Checks if the codepoint is in the map of codepoint overrides,
|
|
/// finds the override font, and returns it.
|
|
fn get_index_codepoint_override(&mut self, cp: u32) -> Option<Index> {
|
|
// Get our codepoint map. If we have no map set then we have no
|
|
// codepoint overrides and we're done.
|
|
let map = self.codepoint_map.as_ref()?;
|
|
|
|
// If the codepoint is in the map, the map returns a descriptor that
|
|
// can be used with font discovery to search for a matching font.
|
|
let desc = map.get(cp)?;
|
|
let key = desc.hashcode();
|
|
|
|
// Fast path: the descriptor is already loaded. This means that we
|
|
// already did the search before and we have an exact font for this
|
|
// codepoint.
|
|
let idx_: Option<Index> = if let Some(v) = self.descriptor_cache.0.get(&key) {
|
|
*v
|
|
} else {
|
|
// Slow path: we have to find this descriptor and load the font
|
|
let discover = self.discover.as_ref()?;
|
|
let mut disco_it = match discover.discover(desc) {
|
|
Ok(it) => it,
|
|
Err(err) => {
|
|
log::warn!("codepoint override failed codepoint={cp} err={err}");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let face = match disco_it.next() {
|
|
Ok(Some(f)) => f,
|
|
Ok(None) => {
|
|
log::warn!(
|
|
"font lookup for codepoint map failed codepoint={cp} err=FontNotFound"
|
|
);
|
|
|
|
// Add null to the cache so we don't do a lookup again later.
|
|
self.descriptor_cache.0.insert(key, None);
|
|
return None;
|
|
}
|
|
Err(err) => {
|
|
log::warn!("codepoint override failed codepoint={cp} err={err}");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
// Add the font to our list of fonts so we can get an index for it,
|
|
// and ensure the index is stored in the descriptor cache for next time.
|
|
let idx = match self.collection.add_deferred(
|
|
face,
|
|
AddOptions {
|
|
style: Style::Regular,
|
|
fallback: false,
|
|
size_adjustment: crate::DEFAULT_FALLBACK_ADJUSTMENT,
|
|
},
|
|
) {
|
|
Ok(idx) => idx,
|
|
Err(err) => {
|
|
log::warn!("codepoint override failed codepoint={cp} err={err}");
|
|
return None;
|
|
}
|
|
};
|
|
self.descriptor_cache.0.insert(key, Some(idx));
|
|
|
|
Some(idx)
|
|
};
|
|
|
|
// The descriptor cache will populate null if the descriptor is not found
|
|
// to avoid expensive discoveries later, so if it is null then we already
|
|
// searched and found nothing.
|
|
let idx = idx_?;
|
|
|
|
// We need to verify that this index has the codepoint we want.
|
|
if self
|
|
.collection
|
|
.has_codepoint(idx, cp, PresentationMode::Any)
|
|
{
|
|
log::debug!(
|
|
"codepoint override based on config codepoint={cp} family={}",
|
|
desc.family.as_deref().unwrap_or("")
|
|
);
|
|
|
|
return Some(idx);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Returns the presentation for a specific font index. This is useful for
|
|
/// determining what atlas is needed.
|
|
pub fn get_presentation(
|
|
&mut self,
|
|
index: Index,
|
|
glyph_index: u32,
|
|
) -> Result<Presentation, GetFaceError> {
|
|
if let Some(sp) = index.special() {
|
|
return Ok(match sp {
|
|
Special::Sprite => Presentation::Text,
|
|
});
|
|
}
|
|
|
|
let face = self.collection.get_face(index)?;
|
|
Ok(if face.is_color_glyph(glyph_index) {
|
|
Presentation::Emoji
|
|
} else {
|
|
Presentation::Text
|
|
})
|
|
}
|
|
|
|
/// Render a glyph by glyph index into the given font atlas and return
|
|
/// metadata about it.
|
|
///
|
|
/// This performs no caching, it is up to the caller to cache calls to this
|
|
/// if they want. This will also not resize the atlas if it is full.
|
|
///
|
|
/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller
|
|
/// is expected to translate codepoints to glyph indexes in some way. The most
|
|
/// trivial way to do this is to get the Face and call glyph_index. If you're
|
|
/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically
|
|
/// determine glyph indexes for a text run.
|
|
pub fn render_glyph(
|
|
&mut self,
|
|
atlas: &mut Atlas,
|
|
index: Index,
|
|
glyph_index: u32,
|
|
opts: RenderOptions,
|
|
) -> Result<Glyph, RenderGlyphError> {
|
|
// Special-case fonts are rendered directly.
|
|
if let Some(sp) = index.special() {
|
|
match sp {
|
|
Special::Sprite => {
|
|
let sprite = self
|
|
.sprite
|
|
.as_ref()
|
|
.ok_or(RenderGlyphError::SpriteUnavailable)?;
|
|
return Ok(sprite.render_glyph(atlas, glyph_index, opts)?);
|
|
}
|
|
}
|
|
}
|
|
|
|
let face = self.collection.get_face(index)?;
|
|
Ok(face.render_glyph(atlas, glyph_index, opts)?)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
Face,
|
|
Library,
|
|
collection::{LoadOptions, SizeAdjustment},
|
|
embedded,
|
|
face::{DesiredSize, Options},
|
|
};
|
|
|
|
fn add(c: &mut Collection, lib: &Library, data: &'static [u8], style: Style, pts: f32) {
|
|
let size = if pts == 12.0 {
|
|
DesiredSize {
|
|
points: 12.0,
|
|
xdpi: 96,
|
|
ydpi: 96,
|
|
}
|
|
} else {
|
|
DesiredSize::new(pts)
|
|
};
|
|
c.add(
|
|
Face::init(lib, data.into(), Options::new(size)).unwrap(),
|
|
AddOptions {
|
|
style,
|
|
fallback: false,
|
|
size_adjustment: SizeAdjustment::None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn get_index() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
c.load_options = Some(LoadOptions::new(lib.clone()));
|
|
|
|
add(&mut c, &lib, embedded::REGULAR, Style::Regular, 12.0);
|
|
// Coretext doesn't support Noto's format
|
|
#[cfg(not(font_coretext))]
|
|
add(&mut c, &lib, embedded::EMOJI, Style::Regular, 12.0);
|
|
add(&mut c, &lib, embedded::EMOJI_TEXT, Style::Regular, 12.0);
|
|
|
|
let mut r = CodepointResolver::new(c);
|
|
|
|
// Should find all visible ASCII
|
|
for i in 32u32..127 {
|
|
let idx = r.get_index(i, Style::Regular, None).unwrap();
|
|
assert_eq!(Style::Regular, idx.style);
|
|
assert_eq!(0, idx.idx);
|
|
}
|
|
|
|
// Try emoji
|
|
{
|
|
let idx = r.get_index('🥸' as u32, Style::Regular, None).unwrap();
|
|
assert_eq!(Style::Regular, idx.style);
|
|
assert_eq!(1, idx.idx);
|
|
}
|
|
|
|
// Try text emoji
|
|
{
|
|
let idx = r
|
|
.get_index(0x270C, Style::Regular, Some(Presentation::Text))
|
|
.unwrap();
|
|
assert_eq!(Style::Regular, idx.style);
|
|
let text_idx = if cfg!(font_coretext) { 1 } else { 2 };
|
|
assert_eq!(text_idx, idx.idx);
|
|
}
|
|
{
|
|
let idx = r
|
|
.get_index(0x270C, Style::Regular, Some(Presentation::Emoji))
|
|
.unwrap();
|
|
assert_eq!(Style::Regular, idx.style);
|
|
assert_eq!(1, idx.idx);
|
|
}
|
|
|
|
// Box glyph should be null since we didn't set a box font
|
|
assert!(r.get_index(0x1FB00, Style::Regular, None).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn get_index_disabled_font_style() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
c.load_options = Some(LoadOptions::new(lib.clone()));
|
|
|
|
add(&mut c, &lib, embedded::REGULAR, Style::Regular, 12.0);
|
|
add(&mut c, &lib, embedded::REGULAR, Style::Bold, 12.0);
|
|
add(&mut c, &lib, embedded::REGULAR, Style::Italic, 12.0);
|
|
|
|
let mut r = CodepointResolver::new(c);
|
|
r.styles.set(Style::Bold, false); // Disable bold
|
|
|
|
// Regular should work fine
|
|
{
|
|
let idx = r.get_index('A' as u32, Style::Regular, None).unwrap();
|
|
assert_eq!(Style::Regular, idx.style);
|
|
assert_eq!(0, idx.idx);
|
|
}
|
|
|
|
// Bold should go to regular
|
|
{
|
|
let idx = r.get_index('A' as u32, Style::Bold, None).unwrap();
|
|
assert_eq!(Style::Regular, idx.style);
|
|
assert_eq!(0, idx.idx);
|
|
}
|
|
|
|
// Italic should still work
|
|
{
|
|
let idx = r.get_index('A' as u32, Style::Italic, None).unwrap();
|
|
assert_eq!(Style::Italic, idx.style);
|
|
assert_eq!(0, idx.idx);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn get_index_box_glyph() {
|
|
let c = Collection::new();
|
|
|
|
let mut r = CodepointResolver::new(c);
|
|
r.sprite = Some(SpriteFace::new(crate::Metrics::calc(
|
|
crate::metrics::FaceMetrics {
|
|
px_per_em: 30.0,
|
|
cell_width: 18.0,
|
|
ascent: 30.0,
|
|
descent: -6.0,
|
|
line_gap: 0.0,
|
|
..Default::default()
|
|
},
|
|
)));
|
|
|
|
// Should find a box glyph
|
|
let idx = r.get_index(0x2500, Style::Regular, None).unwrap();
|
|
assert_eq!(Style::Regular, idx.style);
|
|
assert_eq!(Index::SPECIAL_START, idx.idx);
|
|
}
|
|
|
|
#[test]
|
|
fn get_presentation_and_render_sprite() {
|
|
let mut r = CodepointResolver::new(Collection::new());
|
|
let metrics = crate::Metrics::calc(crate::metrics::FaceMetrics {
|
|
px_per_em: 30.0,
|
|
cell_width: 18.0,
|
|
ascent: 30.0,
|
|
descent: -6.0,
|
|
line_gap: 0.0,
|
|
..Default::default()
|
|
});
|
|
r.sprite = Some(SpriteFace::new(metrics));
|
|
let sprite_idx = Index::init_special(Special::Sprite);
|
|
assert_eq!(
|
|
Presentation::Text,
|
|
r.get_presentation(sprite_idx, 0x2500).unwrap()
|
|
);
|
|
let mut atlas = Atlas::new(256, crate::atlas::Format::Grayscale);
|
|
let g = r
|
|
.render_glyph(&mut atlas, sprite_idx, 0x2500, RenderOptions::new(metrics))
|
|
.unwrap();
|
|
assert!(g.width > 0 && g.height > 0);
|
|
}
|
|
}
|