Honor variable font instances
This commit is contained in:
1 parent
f61493fbd1
commit
52d2d89cb6
41 files changed
+11484
-65
No files matched your search
Generated
-2
@@ -584,8 +584,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-text"
|
name = "cosmic-text"
|
||||||
version = "0.14.2"
|
version = "0.14.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"fontdb 0.16.2",
|
"fontdb 0.16.2",
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ base64 = "0.22"
|
|||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Keep the grid shrink-to-fit correction local until it lands upstream.
|
# Keep the grid shrink-to-fit correction local until it lands upstream.
|
||||||
taffy = { path = "vendor/taffy" }
|
taffy = { path = "vendor/taffy" }
|
||||||
|
# Carry the same canonical variable-font coordinates through shaping and
|
||||||
|
# rasterization until the upstream API exposes them.
|
||||||
|
cosmic-text = { path = "vendor/cosmic-text" }
|
||||||
|
|
||||||
# Unwinding (the Rust default) is REQUIRED for the anti-panic protocol: ops wrap
|
# Unwinding (the Rust default) is REQUIRED for the anti-panic protocol: ops wrap
|
||||||
# their bodies in std::panic::catch_unwind so a panic degrades to an error return
|
# their bodies in std::panic::catch_unwind so a panic degrades to an error return
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ ab_glyph = { workspace = true, optional = true }
|
|||||||
# fonts (no system-font scan) so layout stays deterministic across hosts.
|
# fonts (no system-font scan) so layout stays deterministic across hosts.
|
||||||
cosmic-text = { version = "=0.14.2", default-features = false, features = ["std", "swash"], optional = true }
|
cosmic-text = { version = "=0.14.2", default-features = false, features = ["std", "swash"], optional = true }
|
||||||
# Keep this exactly aligned with cosmic-text 0.14's raster backend. Cosmic
|
# Keep this exactly aligned with cosmic-text 0.14's raster backend. Cosmic
|
||||||
# does not expose variation coordinates, so variable font instances are
|
# and the renderer consume the same canonical variation coordinates for
|
||||||
# rasterized through Swash directly while Cosmic remains the shaper.
|
# shaping and rasterization.
|
||||||
swash = { version = "=0.2.0", default-features = false, features = ["std", "scale", "render"], optional = true }
|
swash = { version = "=0.2.0", default-features = false, features = ["std", "scale", "render"], optional = true }
|
||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"], optional = true }
|
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"], optional = true }
|
||||||
ureq = { version = "2.9", default-features = false, features = ["tls"], optional = true }
|
ureq = { version = "2.9", default-features = false, features = ["tls"], optional = true }
|
||||||
|
|||||||
@@ -1076,6 +1076,8 @@ pub(crate) fn layout_dom_with_web_fonts(
|
|||||||
font_size: Option<f32>,
|
font_size: Option<f32>,
|
||||||
font_weight: u16,
|
font_weight: u16,
|
||||||
font_family: Option<String>,
|
font_family: Option<String>,
|
||||||
|
font_optical_sizing: crate::FontOpticalSizing,
|
||||||
|
font_variation_settings: Vec<crate::FontVariationSetting>,
|
||||||
letter_spacing: f32,
|
letter_spacing: f32,
|
||||||
letter_spacing_non_normal: bool,
|
letter_spacing_non_normal: bool,
|
||||||
container_type: crate::ContainerType,
|
container_type: crate::ContainerType,
|
||||||
@@ -1110,6 +1112,8 @@ pub(crate) fn layout_dom_with_web_fonts(
|
|||||||
font_size: None,
|
font_size: None,
|
||||||
font_weight: 400,
|
font_weight: 400,
|
||||||
font_family: None,
|
font_family: None,
|
||||||
|
font_optical_sizing: crate::FontOpticalSizing::Auto,
|
||||||
|
font_variation_settings: Vec::new(),
|
||||||
letter_spacing: 0.0,
|
letter_spacing: 0.0,
|
||||||
letter_spacing_non_normal: false,
|
letter_spacing_non_normal: false,
|
||||||
container_type: crate::ContainerType::Normal,
|
container_type: crate::ContainerType::Normal,
|
||||||
@@ -1410,6 +1414,17 @@ pub(crate) fn layout_dom_with_web_fonts(
|
|||||||
style.font_weight = Some(computed_weight.to_string());
|
style.font_weight = Some(computed_weight.to_string());
|
||||||
inh.font_weight = computed_weight;
|
inh.font_weight = computed_weight;
|
||||||
match &style.font_family { Some(f) => inh.font_family = Some(f.clone()), None => style.font_family = inh.font_family.clone() }
|
match &style.font_family { Some(f) => inh.font_family = Some(f.clone()), None => style.font_family = inh.font_family.clone() }
|
||||||
|
match style.font_optical_sizing {
|
||||||
|
Some(value) => inh.font_optical_sizing = value,
|
||||||
|
None => style.font_optical_sizing = Some(inh.font_optical_sizing),
|
||||||
|
}
|
||||||
|
match &style.font_variation_settings {
|
||||||
|
Some(settings) => inh.font_variation_settings.clone_from(settings),
|
||||||
|
None => {
|
||||||
|
style.font_variation_settings =
|
||||||
|
Some(inh.font_variation_settings.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
let is_table = tree
|
let is_table = tree
|
||||||
.get_node(id)
|
.get_node(id)
|
||||||
.map_or(false, |node| {
|
.map_or(false, |node| {
|
||||||
@@ -1566,6 +1581,8 @@ pub(crate) fn layout_dom_with_web_fonts(
|
|||||||
style.letter_spacing_non_normal.unwrap_or(false);
|
style.letter_spacing_non_normal.unwrap_or(false);
|
||||||
let host_weight = crate::style::used_font_weight(style);
|
let host_weight = crate::style::used_font_weight(style);
|
||||||
let host_family = style.font_family.clone();
|
let host_family = style.font_family.clone();
|
||||||
|
let host_optical_sizing = style.font_optical_sizing;
|
||||||
|
let host_variation_settings = style.font_variation_settings.clone();
|
||||||
let host_line_height = style.line_height;
|
let host_line_height = style.line_height;
|
||||||
let host_transform = style.text_transform;
|
let host_transform = style.text_transform;
|
||||||
let host_italic = style.font_style_italic;
|
let host_italic = style.font_style_italic;
|
||||||
@@ -1711,6 +1728,13 @@ pub(crate) fn layout_dom_with_web_fonts(
|
|||||||
if pseudo.font_family.is_none() {
|
if pseudo.font_family.is_none() {
|
||||||
pseudo.font_family = host_family.clone();
|
pseudo.font_family = host_family.clone();
|
||||||
}
|
}
|
||||||
|
if pseudo.font_optical_sizing.is_none() {
|
||||||
|
pseudo.font_optical_sizing = host_optical_sizing;
|
||||||
|
}
|
||||||
|
if pseudo.font_variation_settings.is_none() {
|
||||||
|
pseudo.font_variation_settings =
|
||||||
|
host_variation_settings.clone();
|
||||||
|
}
|
||||||
if let Some(expression) = pseudo.line_height_expression.as_deref() {
|
if let Some(expression) = pseudo.line_height_expression.as_deref() {
|
||||||
if let Some(resolved) = crate::style::resolve_contextual_length(
|
if let Some(resolved) = crate::style::resolve_contextual_length(
|
||||||
expression,
|
expression,
|
||||||
@@ -7334,6 +7358,79 @@ mod tests {
|
|||||||
assert_eq!(pseudo.line_height, host.line_height);
|
assert_eq!(pseudo.line_height, host.line_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_font_properties_inherit_and_reset_through_elements_and_pseudos() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
#parent {
|
||||||
|
font-optical-sizing:none;
|
||||||
|
font-variation-settings:"opsz" 20, "wght" 650;
|
||||||
|
}
|
||||||
|
#parent::before { content:"before" }
|
||||||
|
#reset {
|
||||||
|
font-optical-sizing:initial;
|
||||||
|
font-variation-settings:normal;
|
||||||
|
}
|
||||||
|
#reset::after {
|
||||||
|
content:"after";
|
||||||
|
font-optical-sizing:inherit;
|
||||||
|
font-variation-settings:"wght" 300;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="parent">
|
||||||
|
<span id="inherited"></span>
|
||||||
|
<span id="reset"><b id="reset-child"></b></span>
|
||||||
|
</div>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (1280.0, 720.0));
|
||||||
|
let get = |selector: &str| {
|
||||||
|
let id = tree.query_selector(selector).unwrap().unwrap();
|
||||||
|
&laid.styles[&id]
|
||||||
|
};
|
||||||
|
let parent = get("#parent");
|
||||||
|
let inherited = get("#inherited");
|
||||||
|
assert_eq!(
|
||||||
|
parent.font_optical_sizing,
|
||||||
|
Some(crate::FontOpticalSizing::None)
|
||||||
|
);
|
||||||
|
assert_eq!(inherited.font_optical_sizing, parent.font_optical_sizing);
|
||||||
|
assert_eq!(
|
||||||
|
inherited.font_variation_settings,
|
||||||
|
parent.font_variation_settings
|
||||||
|
);
|
||||||
|
let before = parent.before_pseudo.as_ref().unwrap();
|
||||||
|
assert_eq!(before.font_optical_sizing, parent.font_optical_sizing);
|
||||||
|
assert_eq!(
|
||||||
|
before.font_variation_settings,
|
||||||
|
parent.font_variation_settings
|
||||||
|
);
|
||||||
|
|
||||||
|
let reset = get("#reset");
|
||||||
|
let reset_child = get("#reset-child");
|
||||||
|
assert_eq!(
|
||||||
|
reset.font_optical_sizing,
|
||||||
|
Some(crate::FontOpticalSizing::Auto)
|
||||||
|
);
|
||||||
|
assert_eq!(reset.font_variation_settings, Some(Vec::new()));
|
||||||
|
assert_eq!(reset_child.font_optical_sizing, reset.font_optical_sizing);
|
||||||
|
assert_eq!(
|
||||||
|
reset_child.font_variation_settings,
|
||||||
|
reset.font_variation_settings
|
||||||
|
);
|
||||||
|
let after = reset.after_pseudo.as_ref().unwrap();
|
||||||
|
assert_eq!(after.font_optical_sizing, reset.font_optical_sizing);
|
||||||
|
assert_eq!(
|
||||||
|
after.font_variation_settings.as_deref(),
|
||||||
|
Some(
|
||||||
|
[crate::FontVariationSetting {
|
||||||
|
tag: *b"wght",
|
||||||
|
value: 300.0,
|
||||||
|
}]
|
||||||
|
.as_slice()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn native_select_sizes_for_its_widest_option() {
|
fn native_select_sizes_for_its_widest_option() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ use std::{collections::HashMap, sync::Arc};
|
|||||||
|
|
||||||
use cosmic_text::{
|
use cosmic_text::{
|
||||||
Align, Attrs, Buffer, CacheKey, CacheKeyFlags, Color, Family, FeatureTag, FontFeatures,
|
Align, Attrs, Buffer, CacheKey, CacheKeyFlags, Color, Family, FeatureTag, FontFeatures,
|
||||||
FontSystem, Metrics, Shaping, Style, SwashCache, SwashImage, Weight, Wrap,
|
FontSystem, FontVariations, Metrics, Shaping, Style, SwashCache, SwashImage, VariationTag,
|
||||||
|
Weight, Wrap,
|
||||||
};
|
};
|
||||||
use swash::scale::{image::Content as SwashContent, Render, ScaleContext, Source, StrikeWith};
|
use swash::scale::{image::Content as SwashContent, Render, ScaleContext, Source, StrikeWith};
|
||||||
use swash::zeno::{Angle, Format, Transform, Vector};
|
use swash::zeno::{Angle, Format, Transform, Vector};
|
||||||
@@ -91,19 +92,23 @@ struct LoadedFamily {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct LoadedFace {
|
struct LoadedFace {
|
||||||
name: Arc<str>,
|
name: Arc<str>,
|
||||||
|
font_id: Option<cosmic_text::fontdb::ID>,
|
||||||
min_weight: u16,
|
min_weight: u16,
|
||||||
max_weight: u16,
|
max_weight: u16,
|
||||||
shape_weight: u16,
|
shape_weight: u16,
|
||||||
italic: bool,
|
italic: bool,
|
||||||
|
has_optical_size: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ResolvedFont {
|
struct ResolvedFont {
|
||||||
family: Arc<str>,
|
family: Arc<str>,
|
||||||
|
font_id: Option<cosmic_text::fontdb::ID>,
|
||||||
shape_weight: u16,
|
shape_weight: u16,
|
||||||
/// Authored `wght` coordinate for a ranged @font-face. Cosmic Text 0.12
|
/// Authored `wght` coordinate for a ranged `@font-face`, retained while
|
||||||
/// cannot carry this into shaping, so it is kept separately for raster.
|
/// descriptor selection is converted into the canonical axis tuple.
|
||||||
variation_weight: Option<u16>,
|
variation_weight: Option<u16>,
|
||||||
|
has_optical_size: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -142,19 +147,18 @@ fn resolve_loaded_font(
|
|||||||
.copied()
|
.copied()
|
||||||
.find(|face| (face.min_weight..=face.max_weight).contains(&requested_weight))
|
.find(|face| (face.min_weight..=face.max_weight).contains(&requested_weight))
|
||||||
{
|
{
|
||||||
// cosmic-text 0.12 only accepts the named family when the
|
// The named-family matcher uses fontdb's default weight for
|
||||||
// shaping weight exactly matches fontdb's weight for that
|
// this resource, while a variable face commonly advertises
|
||||||
// file. A variable face commonly advertises `100 900` in
|
// `100 900` in CSS. Preserve the descriptor-selected file
|
||||||
// CSS while fontdb records its default instance as 400.
|
// and its database weight; the authored coordinate enters
|
||||||
// Passing the authored weight makes cosmic-text reject the
|
// the canonical axis tuple below.
|
||||||
// right family and fall through to an unrelated exact-
|
|
||||||
// weight face. Select the file at its database weight;
|
|
||||||
// variable-axis shaping is not exposed by this version.
|
|
||||||
return ResolvedFont {
|
return ResolvedFont {
|
||||||
family: Arc::clone(&face.name),
|
family: Arc::clone(&face.name),
|
||||||
|
font_id: face.font_id,
|
||||||
shape_weight: face.shape_weight,
|
shape_weight: face.shape_weight,
|
||||||
variation_weight: (face.min_weight != face.max_weight)
|
variation_weight: (face.min_weight != face.max_weight)
|
||||||
.then_some(requested_weight),
|
.then_some(requested_weight),
|
||||||
|
has_optical_size: face.has_optical_size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let available: Vec<_> =
|
let available: Vec<_> =
|
||||||
@@ -166,8 +170,10 @@ fn resolve_loaded_font(
|
|||||||
{
|
{
|
||||||
return ResolvedFont {
|
return ResolvedFont {
|
||||||
family: Arc::clone(&face.name),
|
family: Arc::clone(&face.name),
|
||||||
|
font_id: face.font_id,
|
||||||
shape_weight: face.shape_weight,
|
shape_weight: face.shape_weight,
|
||||||
variation_weight: None,
|
variation_weight: None,
|
||||||
|
has_optical_size: face.has_optical_size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,8 +183,10 @@ fn resolve_loaded_font(
|
|||||||
let weights: &[u16] = &[400, 700];
|
let weights: &[u16] = &[400, 700];
|
||||||
ResolvedFont {
|
ResolvedFont {
|
||||||
family: Arc::from(fallback),
|
family: Arc::from(fallback),
|
||||||
|
font_id: None,
|
||||||
shape_weight: match_font_weight(requested_weight, weights),
|
shape_weight: match_font_weight(requested_weight, weights),
|
||||||
variation_weight: None,
|
variation_weight: None,
|
||||||
|
has_optical_size: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,12 +270,15 @@ pub(crate) fn used_line_height(style: &LayoutStyle) -> f32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-glyph flags carried through cosmic-text's `metadata` field. Bit zero is
|
type ClipTextFill = (f32, Vec<([u8; 4], Option<f32>)>);
|
||||||
/// the underline flag; the remaining bits encode an optional one-based index
|
|
||||||
/// into an [`InlineItem`]'s clip-text fills.
|
/// Per-glyph flags carried through cosmic-text's metadata. The common
|
||||||
|
/// non-variable path stays allocation-free: underline and fill remain packed
|
||||||
|
/// directly, while the upper half is a one-based index into an optional
|
||||||
|
/// variation-set table.
|
||||||
const META_UNDERLINE: usize = 1;
|
const META_UNDERLINE: usize = 1;
|
||||||
const META_FILL_SHIFT: usize = 1;
|
const META_FILL_SHIFT: usize = 1;
|
||||||
const META_VARIATION_BITS: usize = 10;
|
const META_VARIATION_BITS: usize = usize::BITS as usize / 2;
|
||||||
const META_VARIATION_SHIFT: usize = usize::BITS as usize - META_VARIATION_BITS;
|
const META_VARIATION_SHIFT: usize = usize::BITS as usize - META_VARIATION_BITS;
|
||||||
const META_VARIATION_MASK: usize =
|
const META_VARIATION_MASK: usize =
|
||||||
((1usize << META_VARIATION_BITS) - 1) << META_VARIATION_SHIFT;
|
((1usize << META_VARIATION_BITS) - 1) << META_VARIATION_SHIFT;
|
||||||
@@ -277,13 +288,10 @@ fn metadata_fill(metadata: usize) -> Option<usize> {
|
|||||||
((metadata & META_FILL_MASK) >> META_FILL_SHIFT).checked_sub(1)
|
((metadata & META_FILL_MASK) >> META_FILL_SHIFT).checked_sub(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata_variation_weight(metadata: usize) -> Option<u16> {
|
fn metadata_variation(metadata: usize) -> Option<usize> {
|
||||||
let weight = ((metadata & META_VARIATION_MASK) >> META_VARIATION_SHIFT) as u16;
|
((metadata & META_VARIATION_MASK) >> META_VARIATION_SHIFT).checked_sub(1)
|
||||||
(weight != 0).then_some(weight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClipTextFill = (f32, Vec<([u8; 4], Option<f32>)>);
|
|
||||||
|
|
||||||
/// One inline formatting context: a shaped cosmic-text buffer plus where to
|
/// One inline formatting context: a shaped cosmic-text buffer plus where to
|
||||||
/// paint it (filled in after layout).
|
/// paint it (filled in after layout).
|
||||||
pub struct InlineItem {
|
pub struct InlineItem {
|
||||||
@@ -300,6 +308,9 @@ pub struct InlineItem {
|
|||||||
/// one entry, allowing an inline accent span to own a gradient without
|
/// one entry, allowing an inline accent span to own a gradient without
|
||||||
/// recoloring the rest of its heading.
|
/// recoloring the rest of its heading.
|
||||||
clip_fills: Vec<ClipTextFill>,
|
clip_fills: Vec<ClipTextFill>,
|
||||||
|
/// Canonical axis tuples, populated only when this IFC actually contains
|
||||||
|
/// variable text. Glyph metadata stores a one-based index.
|
||||||
|
variation_sets: Vec<Arc<FontVariations>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Owns the font set and shaping caches for one render pass, plus every
|
/// Owns the font set and shaping caches for one render pass, plus every
|
||||||
@@ -314,15 +325,15 @@ pub struct TextEngine {
|
|||||||
replaced: Vec<ReplacedItem>,
|
replaced: Vec<ReplacedItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
struct VariableCacheKey {
|
struct VariableCacheKey {
|
||||||
glyph: CacheKey,
|
glyph: CacheKey,
|
||||||
weight: u16,
|
variations: Arc<FontVariations>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Swash's ordinary cache key intentionally has no variation coordinates.
|
/// Swash's ordinary cache key intentionally has no variation coordinates.
|
||||||
/// Keep variable instances in a separate cache so a 400 glyph can never be
|
/// Keep variable instances in a separate cache so different axis tuples can
|
||||||
/// reused for 700 and repeated paint remains O(1) after the first raster.
|
/// never share an outline and repeated paint remains O(1) after first raster.
|
||||||
struct VariableSwashCache {
|
struct VariableSwashCache {
|
||||||
context: ScaleContext,
|
context: ScaleContext,
|
||||||
images: HashMap<VariableCacheKey, Option<SwashImage>>,
|
images: HashMap<VariableCacheKey, Option<SwashImage>>,
|
||||||
@@ -340,22 +351,29 @@ impl VariableSwashCache {
|
|||||||
&mut self,
|
&mut self,
|
||||||
font_system: &mut FontSystem,
|
font_system: &mut FontSystem,
|
||||||
cache_key: CacheKey,
|
cache_key: CacheKey,
|
||||||
weight: u16,
|
variations: Arc<FontVariations>,
|
||||||
base: Color,
|
base: Color,
|
||||||
mut f: F,
|
mut f: F,
|
||||||
) {
|
) {
|
||||||
|
let render_variations = Arc::clone(&variations);
|
||||||
let key = VariableCacheKey {
|
let key = VariableCacheKey {
|
||||||
glyph: cache_key,
|
glyph: cache_key,
|
||||||
weight,
|
variations,
|
||||||
};
|
};
|
||||||
let image = self.images.entry(key).or_insert_with(|| {
|
let image = self.images.entry(key).or_insert_with(|| {
|
||||||
let font = font_system.get_font(cache_key.font_id)?;
|
let font = font_system.get_font(cache_key.font_id)?;
|
||||||
|
let settings = render_variations.iter().map(|variation| {
|
||||||
|
(
|
||||||
|
swash::tag_from_bytes(variation.tag.as_bytes()),
|
||||||
|
variation.value.0,
|
||||||
|
)
|
||||||
|
});
|
||||||
let mut scaler = self
|
let mut scaler = self
|
||||||
.context
|
.context
|
||||||
.builder(font.as_swash())
|
.builder(font.as_swash())
|
||||||
.size(f32::from_bits(cache_key.font_size_bits))
|
.size(f32::from_bits(cache_key.font_size_bits))
|
||||||
.hint(true)
|
.hint(true)
|
||||||
.variations([("wght", weight as f32)])
|
.variations(settings)
|
||||||
.build();
|
.build();
|
||||||
let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float());
|
let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float());
|
||||||
Render::new(&[
|
Render::new(&[
|
||||||
@@ -623,6 +641,14 @@ impl TextEngine {
|
|||||||
let mut loaded_families = HashMap::new();
|
let mut loaded_families = HashMap::new();
|
||||||
for (id, declared_family, declared_weight, declared_italic) in declarations {
|
for (id, declared_family, declared_weight, declared_italic) in declarations {
|
||||||
let Some(face) = db.face(id) else { continue };
|
let Some(face) = db.face(id) else { continue };
|
||||||
|
let has_optical_size = db
|
||||||
|
.with_face_data(id, |data, index| {
|
||||||
|
swash::FontRef::from_index(data, index as usize).map_or(false, |font| {
|
||||||
|
font.variations()
|
||||||
|
.any(|axis| axis.tag() == swash::tag_from_bytes(b"opsz"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
let names = face.families.clone();
|
let names = face.families.clone();
|
||||||
let internal_name = names
|
let internal_name = names
|
||||||
.first()
|
.first()
|
||||||
@@ -641,10 +667,12 @@ impl TextEngine {
|
|||||||
.or_insert_with(|| LoadedFamily { faces: Vec::new() });
|
.or_insert_with(|| LoadedFamily { faces: Vec::new() });
|
||||||
family.faces.push(LoadedFace {
|
family.faces.push(LoadedFace {
|
||||||
name: Arc::clone(&internal_name),
|
name: Arc::clone(&internal_name),
|
||||||
|
font_id: Some(id),
|
||||||
min_weight: weight.0,
|
min_weight: weight.0,
|
||||||
max_weight: weight.1,
|
max_weight: weight.1,
|
||||||
shape_weight,
|
shape_weight,
|
||||||
italic,
|
italic,
|
||||||
|
has_optical_size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,7 +813,8 @@ impl TextEngine {
|
|||||||
letter_spacing: context.letter_spacing,
|
letter_spacing: context.letter_spacing,
|
||||||
letter_spacing_non_normal: context.letter_spacing_non_normal,
|
letter_spacing_non_normal: context.letter_spacing_non_normal,
|
||||||
weight: context.weight,
|
weight: context.weight,
|
||||||
variation_weight: context.variation_weight,
|
font_id: context.font_id,
|
||||||
|
variations: context.variations.clone(),
|
||||||
italic: context.italic,
|
italic: context.italic,
|
||||||
underline: context.underline,
|
underline: context.underline,
|
||||||
color: context.color,
|
color: context.color,
|
||||||
@@ -859,10 +888,34 @@ impl TextEngine {
|
|||||||
// column overflows instead, matching normal `overflow-wrap: normal`.
|
// column overflows instead, matching normal `overflow-wrap: normal`.
|
||||||
buffer.set_wrap(&mut self.font_system, Wrap::Word);
|
buffer.set_wrap(&mut self.font_system, Wrap::Word);
|
||||||
// Always Advanced shaping: Basic mis-maps per-span attribute
|
// Always Advanced shaping: Basic mis-maps per-span attribute
|
||||||
// boundaries in cosmic-text 0.12 (a multi-color run like body text
|
// boundaries in the shaping backend (a multi-color run like body text
|
||||||
// with links ends up coloring the wrong glyphs), and shaping is a
|
// with links ends up coloring the wrong glyphs), and shaping is a
|
||||||
// small fraction of a page's CSS-cascade-dominated render time.
|
// small fraction of a page's CSS-cascade-dominated render time.
|
||||||
let rich = spans.iter().map(|(t, a)| (t.as_str(), a.to_attrs()));
|
let mut variation_sets = Vec::<Arc<FontVariations>>::new();
|
||||||
|
for (_, attrs) in &spans {
|
||||||
|
if let Some(variations) = attrs.variations.as_ref() {
|
||||||
|
if !variation_sets
|
||||||
|
.iter()
|
||||||
|
.any(|existing| **existing == **variations)
|
||||||
|
{
|
||||||
|
variation_sets.push(Arc::clone(variations));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rich = spans.iter().map(|(text, attrs)| {
|
||||||
|
let variation_index = attrs
|
||||||
|
.variations
|
||||||
|
.as_ref()
|
||||||
|
.map(|variations| {
|
||||||
|
variation_sets
|
||||||
|
.iter()
|
||||||
|
.position(|existing| **existing == **variations)
|
||||||
|
.expect("collected span variation set")
|
||||||
|
+ 1
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
(text.as_str(), attrs.to_attrs(variation_index))
|
||||||
|
});
|
||||||
let defaults = Attrs::new().family(Family::Name(FAMILY));
|
let defaults = Attrs::new().family(Family::Name(FAMILY));
|
||||||
buffer.set_rich_text(
|
buffer.set_rich_text(
|
||||||
&mut self.font_system,
|
&mut self.font_system,
|
||||||
@@ -890,6 +943,7 @@ impl TextEngine {
|
|||||||
origin: (0.0, 0.0),
|
origin: (0.0, 0.0),
|
||||||
clip: None,
|
clip: None,
|
||||||
clip_fills,
|
clip_fills,
|
||||||
|
variation_sets,
|
||||||
});
|
});
|
||||||
Some(idx)
|
Some(idx)
|
||||||
}
|
}
|
||||||
@@ -976,7 +1030,8 @@ struct SpanAttrs {
|
|||||||
letter_spacing: f32,
|
letter_spacing: f32,
|
||||||
letter_spacing_non_normal: bool,
|
letter_spacing_non_normal: bool,
|
||||||
weight: u16,
|
weight: u16,
|
||||||
variation_weight: Option<u16>,
|
font_id: Option<cosmic_text::fontdb::ID>,
|
||||||
|
variations: Option<Arc<FontVariations>>,
|
||||||
italic: bool,
|
italic: bool,
|
||||||
underline: bool,
|
underline: bool,
|
||||||
color: [u8; 4],
|
color: [u8; 4],
|
||||||
@@ -985,7 +1040,7 @@ struct SpanAttrs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SpanAttrs {
|
impl SpanAttrs {
|
||||||
fn to_attrs(&self) -> Attrs<'_> {
|
fn to_attrs(&self, variation_index: usize) -> Attrs<'_> {
|
||||||
let mut a = Attrs::new().family(Family::Name(self.family.as_ref()));
|
let mut a = Attrs::new().family(Family::Name(self.family.as_ref()));
|
||||||
// Inline descendants keep their own computed font metrics inside the
|
// Inline descendants keep their own computed font metrics inside the
|
||||||
// enclosing line box. Without per-span metrics, an `<a>`/`<span>`
|
// enclosing line box. Without per-span metrics, an `<a>`/`<span>`
|
||||||
@@ -996,6 +1051,9 @@ impl SpanAttrs {
|
|||||||
self.line_height.max(1.0),
|
self.line_height.max(1.0),
|
||||||
));
|
));
|
||||||
a = a.weight(Weight(self.weight));
|
a = a.weight(Weight(self.weight));
|
||||||
|
if let Some(font_id) = self.font_id {
|
||||||
|
a = a.font_id(font_id);
|
||||||
|
}
|
||||||
a = a.style(if self.italic { Style::Italic } else { Style::Normal });
|
a = a.style(if self.italic { Style::Italic } else { Style::Normal });
|
||||||
if self.letter_spacing.is_finite() && self.letter_spacing != 0.0 {
|
if self.letter_spacing.is_finite() && self.letter_spacing != 0.0 {
|
||||||
a = a.letter_spacing(self.letter_spacing / self.font_size.max(1.0));
|
a = a.letter_spacing(self.letter_spacing / self.font_size.max(1.0));
|
||||||
@@ -1014,13 +1072,15 @@ impl SpanAttrs {
|
|||||||
// reaches paint; the real gradient is selected through metadata.
|
// reaches paint; the real gradient is selected through metadata.
|
||||||
let color = if self.clip_fill.is_some() { [255, 255, 255, 255] } else { self.color };
|
let color = if self.clip_fill.is_some() { [255, 255, 255, 255] } else { self.color };
|
||||||
a = a.color(Color::rgba(color[0], color[1], color[2], color[3]));
|
a = a.color(Color::rgba(color[0], color[1], color[2], color[3]));
|
||||||
// Underline, the optional fill index, and a variable `wght` coordinate
|
let fill = self
|
||||||
// share the per-glyph metadata.
|
.clip_fill
|
||||||
let fill = self.clip_fill.map_or(0, |index| (index + 1) << META_FILL_SHIFT);
|
.map_or(0, |index| (index + 1) << META_FILL_SHIFT);
|
||||||
debug_assert_eq!(fill & !META_FILL_MASK, 0);
|
debug_assert_eq!(fill & !META_FILL_MASK, 0);
|
||||||
let variation = self
|
let variation = variation_index << META_VARIATION_SHIFT;
|
||||||
.variation_weight
|
assert_eq!(variation & !META_VARIATION_MASK, 0);
|
||||||
.map_or(0, |weight| usize::from(weight.clamp(1, 1000)) << META_VARIATION_SHIFT);
|
if let Some(variations) = self.variations.as_ref() {
|
||||||
|
a = a.font_variations((**variations).clone());
|
||||||
|
}
|
||||||
a = a.metadata(fill | variation | usize::from(self.underline));
|
a = a.metadata(fill | variation | usize::from(self.underline));
|
||||||
a
|
a
|
||||||
}
|
}
|
||||||
@@ -1035,7 +1095,10 @@ struct SpanCtx {
|
|||||||
letter_spacing_non_normal: bool,
|
letter_spacing_non_normal: bool,
|
||||||
color: [u8; 4],
|
color: [u8; 4],
|
||||||
weight: u16,
|
weight: u16,
|
||||||
variation_weight: Option<u16>,
|
font_id: Option<cosmic_text::fontdb::ID>,
|
||||||
|
variations: Option<Arc<FontVariations>>,
|
||||||
|
variable_weight: bool,
|
||||||
|
has_optical_size: bool,
|
||||||
italic: bool,
|
italic: bool,
|
||||||
underline: bool,
|
underline: bool,
|
||||||
transform: TextTransform,
|
transform: TextTransform,
|
||||||
@@ -1061,6 +1124,32 @@ struct Collector {
|
|||||||
/// shape the glyphs in opaque white so their coverage renders, then recolor
|
/// shape the glyphs in opaque white so their coverage renders, then recolor
|
||||||
/// them from the background at paint time; otherwise transparent text stays
|
/// them from the background at paint time; otherwise transparent text stays
|
||||||
/// transparent (and invisible), unchanged.
|
/// transparent (and invisible), unchanged.
|
||||||
|
fn resolved_font_variations(
|
||||||
|
style: &LayoutStyle,
|
||||||
|
font: &ResolvedFont,
|
||||||
|
) -> Option<Arc<FontVariations>> {
|
||||||
|
let mut variations = FontVariations::new();
|
||||||
|
if let Some(weight) = font.variation_weight {
|
||||||
|
variations.set(VariationTag::new(b"wght"), weight as f32);
|
||||||
|
}
|
||||||
|
if font.has_optical_size
|
||||||
|
&& style.font_optical_sizing.unwrap_or(crate::FontOpticalSizing::Auto)
|
||||||
|
== crate::FontOpticalSizing::Auto
|
||||||
|
{
|
||||||
|
let optical_size = style.font_size.unwrap_or(16.0);
|
||||||
|
if optical_size.is_finite() {
|
||||||
|
variations.set(VariationTag::new(b"opsz"), optical_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Low-level CSS axes override the corresponding high-level coordinates.
|
||||||
|
for setting in style.font_variation_settings.as_deref().unwrap_or(&[]) {
|
||||||
|
if setting.value.is_finite() {
|
||||||
|
variations.set(VariationTag::new(&setting.tag), setting.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(!variations.is_empty()).then(|| Arc::new(variations))
|
||||||
|
}
|
||||||
|
|
||||||
fn base_span_ctx(
|
fn base_span_ctx(
|
||||||
base: &LayoutStyle,
|
base: &LayoutStyle,
|
||||||
font: ResolvedFont,
|
font: ResolvedFont,
|
||||||
@@ -1071,6 +1160,7 @@ fn base_span_ctx(
|
|||||||
collector.clip_fills.push(fill);
|
collector.clip_fills.push(fill);
|
||||||
index
|
index
|
||||||
});
|
});
|
||||||
|
let variations = resolved_font_variations(base, &font);
|
||||||
SpanCtx {
|
SpanCtx {
|
||||||
font_size: base.font_size.unwrap_or(16.0),
|
font_size: base.font_size.unwrap_or(16.0),
|
||||||
line_height: used_line_height(base),
|
line_height: used_line_height(base),
|
||||||
@@ -1078,7 +1168,10 @@ fn base_span_ctx(
|
|||||||
letter_spacing_non_normal: base.letter_spacing_non_normal.unwrap_or(false),
|
letter_spacing_non_normal: base.letter_spacing_non_normal.unwrap_or(false),
|
||||||
color: base.color.unwrap_or([0, 0, 0, 255]),
|
color: base.color.unwrap_or([0, 0, 0, 255]),
|
||||||
weight: font.shape_weight,
|
weight: font.shape_weight,
|
||||||
variation_weight: font.variation_weight,
|
font_id: font.font_id,
|
||||||
|
variations,
|
||||||
|
variable_weight: font.variation_weight.is_some(),
|
||||||
|
has_optical_size: font.has_optical_size,
|
||||||
italic: base.font_style_italic.unwrap_or(false),
|
italic: base.font_style_italic.unwrap_or(false),
|
||||||
underline: base.underline.unwrap_or(false),
|
underline: base.underline.unwrap_or(false),
|
||||||
transform: base.text_transform.unwrap_or(TextTransform::None),
|
transform: base.text_transform.unwrap_or(TextTransform::None),
|
||||||
@@ -1131,7 +1224,8 @@ fn collect_node_spans(
|
|||||||
letter_spacing: ctx.letter_spacing,
|
letter_spacing: ctx.letter_spacing,
|
||||||
letter_spacing_non_normal: ctx.letter_spacing_non_normal,
|
letter_spacing_non_normal: ctx.letter_spacing_non_normal,
|
||||||
weight: ctx.weight,
|
weight: ctx.weight,
|
||||||
variation_weight: ctx.variation_weight,
|
font_id: ctx.font_id,
|
||||||
|
variations: ctx.variations.clone(),
|
||||||
italic: ctx.italic,
|
italic: ctx.italic,
|
||||||
underline: ctx.underline,
|
underline: ctx.underline,
|
||||||
color: ctx.color,
|
color: ctx.color,
|
||||||
@@ -1153,7 +1247,8 @@ fn collect_node_spans(
|
|||||||
letter_spacing: ctx.letter_spacing,
|
letter_spacing: ctx.letter_spacing,
|
||||||
letter_spacing_non_normal: ctx.letter_spacing_non_normal,
|
letter_spacing_non_normal: ctx.letter_spacing_non_normal,
|
||||||
weight: ctx.weight,
|
weight: ctx.weight,
|
||||||
variation_weight: ctx.variation_weight,
|
font_id: ctx.font_id,
|
||||||
|
variations: ctx.variations.clone(),
|
||||||
italic: ctx.italic,
|
italic: ctx.italic,
|
||||||
underline: ctx.underline,
|
underline: ctx.underline,
|
||||||
color: ctx.color,
|
color: ctx.color,
|
||||||
@@ -1192,15 +1287,20 @@ fn collect_node_spans(
|
|||||||
})
|
})
|
||||||
.unwrap_or_else(|| ResolvedFont {
|
.unwrap_or_else(|| ResolvedFont {
|
||||||
family: Arc::clone(&ctx.family),
|
family: Arc::clone(&ctx.family),
|
||||||
shape_weight: if ctx.variation_weight.is_some() {
|
font_id: ctx.font_id,
|
||||||
|
shape_weight: if ctx.variable_weight {
|
||||||
ctx.weight
|
ctx.weight
|
||||||
} else {
|
} else {
|
||||||
requested_weight
|
requested_weight
|
||||||
},
|
},
|
||||||
variation_weight: ctx
|
variation_weight: ctx
|
||||||
.variation_weight
|
.variable_weight
|
||||||
.map(|_| requested_weight.clamp(1, 1000)),
|
.then_some(requested_weight.clamp(1, 1000)),
|
||||||
|
has_optical_size: ctx.has_optical_size,
|
||||||
});
|
});
|
||||||
|
let variations = style
|
||||||
|
.map(|style| resolved_font_variations(style, &font))
|
||||||
|
.unwrap_or_else(|| ctx.variations.clone());
|
||||||
let child = SpanCtx {
|
let child = SpanCtx {
|
||||||
font_size: style
|
font_size: style
|
||||||
.and_then(|style| style.font_size)
|
.and_then(|style| style.font_size)
|
||||||
@@ -1216,7 +1316,10 @@ fn collect_node_spans(
|
|||||||
.unwrap_or(ctx.letter_spacing_non_normal),
|
.unwrap_or(ctx.letter_spacing_non_normal),
|
||||||
color,
|
color,
|
||||||
weight: font.shape_weight,
|
weight: font.shape_weight,
|
||||||
variation_weight: font.variation_weight,
|
font_id: font.font_id,
|
||||||
|
variations,
|
||||||
|
variable_weight: font.variation_weight.is_some(),
|
||||||
|
has_optical_size: font.has_optical_size,
|
||||||
italic: ctx.italic || style.and_then(|s| s.font_style_italic).unwrap_or(false),
|
italic: ctx.italic || style.and_then(|s| s.font_style_italic).unwrap_or(false),
|
||||||
// Underline propagates in: an ancestor's underline covers
|
// Underline propagates in: an ancestor's underline covers
|
||||||
// descendant text; an element only sets its own via CSS.
|
// descendant text; an element only sets its own via CSS.
|
||||||
@@ -1606,11 +1709,14 @@ impl TextEngine {
|
|||||||
pixels[idx] = tiny_skia::PremultipliedColorU8::from_rgba(out_r as u8, out_g as u8, out_b as u8, out_a as u8)
|
pixels[idx] = tiny_skia::PremultipliedColorU8::from_rgba(out_r as u8, out_g as u8, out_b as u8, out_a as u8)
|
||||||
.unwrap_or(dst);
|
.unwrap_or(dst);
|
||||||
};
|
};
|
||||||
if let Some(weight) = metadata_variation_weight(glyph.metadata) {
|
if let Some(variations) = metadata_variation(glyph.metadata)
|
||||||
|
.and_then(|index| item.variation_sets.get(index))
|
||||||
|
.map(Arc::clone)
|
||||||
|
{
|
||||||
variable_swash.with_pixels(
|
variable_swash.with_pixels(
|
||||||
font_system,
|
font_system,
|
||||||
physical.cache_key,
|
physical.cache_key,
|
||||||
weight,
|
variations,
|
||||||
glyph_color,
|
glyph_color,
|
||||||
&mut draw_pixel,
|
&mut draw_pixel,
|
||||||
);
|
);
|
||||||
@@ -1791,24 +1897,30 @@ mod tests {
|
|||||||
faces: vec![
|
faces: vec![
|
||||||
LoadedFace {
|
LoadedFace {
|
||||||
name: Arc::from("Poppins"),
|
name: Arc::from("Poppins"),
|
||||||
|
font_id: None,
|
||||||
min_weight: 400,
|
min_weight: 400,
|
||||||
max_weight: 400,
|
max_weight: 400,
|
||||||
shape_weight: 400,
|
shape_weight: 400,
|
||||||
italic: false,
|
italic: false,
|
||||||
|
has_optical_size: false,
|
||||||
},
|
},
|
||||||
LoadedFace {
|
LoadedFace {
|
||||||
name: Arc::from("Poppins Medium"),
|
name: Arc::from("Poppins Medium"),
|
||||||
|
font_id: None,
|
||||||
min_weight: 500,
|
min_weight: 500,
|
||||||
max_weight: 500,
|
max_weight: 500,
|
||||||
shape_weight: 500,
|
shape_weight: 500,
|
||||||
italic: false,
|
italic: false,
|
||||||
|
has_optical_size: false,
|
||||||
},
|
},
|
||||||
LoadedFace {
|
LoadedFace {
|
||||||
name: Arc::from("Poppins"),
|
name: Arc::from("Poppins"),
|
||||||
|
font_id: None,
|
||||||
min_weight: 700,
|
min_weight: 700,
|
||||||
max_weight: 700,
|
max_weight: 700,
|
||||||
shape_weight: 700,
|
shape_weight: 700,
|
||||||
italic: false,
|
italic: false,
|
||||||
|
has_optical_size: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -1840,10 +1952,12 @@ mod tests {
|
|||||||
LoadedFamily {
|
LoadedFamily {
|
||||||
faces: vec![LoadedFace {
|
faces: vec![LoadedFace {
|
||||||
name: Arc::from(FAMILY),
|
name: Arc::from(FAMILY),
|
||||||
|
font_id: None,
|
||||||
min_weight: 100,
|
min_weight: 100,
|
||||||
max_weight: 900,
|
max_weight: 900,
|
||||||
shape_weight: 400,
|
shape_weight: 400,
|
||||||
italic: false,
|
italic: false,
|
||||||
|
has_optical_size: false,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1865,6 +1979,79 @@ mod tests {
|
|||||||
assert_eq!(face.weight.0, 400);
|
assert_eq!(face.weight.0, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn descriptor_selected_resource_pins_the_exact_font_face() {
|
||||||
|
let data = SANS_R.to_vec();
|
||||||
|
let mut engine = TextEngine::new_with_web_fonts(&[
|
||||||
|
WebFont {
|
||||||
|
data: data.clone(),
|
||||||
|
family: Some("Pinned Variable".to_string()),
|
||||||
|
weight: Some((100, 400)),
|
||||||
|
italic: Some(false),
|
||||||
|
},
|
||||||
|
WebFont {
|
||||||
|
data,
|
||||||
|
family: Some("Pinned Variable".to_string()),
|
||||||
|
weight: Some((500, 900)),
|
||||||
|
italic: Some(false),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let loaded = &engine.loaded_families["pinned variable"];
|
||||||
|
assert_eq!(loaded.faces.len(), 2);
|
||||||
|
let first_id = loaded.faces[0].font_id.unwrap();
|
||||||
|
let selected = resolve_loaded_font(
|
||||||
|
Some("Pinned Variable"),
|
||||||
|
700,
|
||||||
|
false,
|
||||||
|
&engine.loaded_families,
|
||||||
|
);
|
||||||
|
let selected_id = selected.font_id.unwrap();
|
||||||
|
assert_ne!(
|
||||||
|
first_id, selected_id,
|
||||||
|
"the fixture must contain two distinct database resources"
|
||||||
|
);
|
||||||
|
let selected_font = engine
|
||||||
|
.font_system
|
||||||
|
.get_font(selected_id)
|
||||||
|
.expect("the descriptor-selected database face must be loadable");
|
||||||
|
assert_eq!(selected_font.id(), selected_id);
|
||||||
|
|
||||||
|
let tree = obscura_dom::parse_html(
|
||||||
|
"<p id='copy'>Build whatever you want, without touching your CSS file.</p>",
|
||||||
|
);
|
||||||
|
let copy = tree.get_element_by_id("copy").unwrap();
|
||||||
|
let style = LayoutStyle {
|
||||||
|
display: Display::Block,
|
||||||
|
font_family: Some("Pinned Variable".to_string()),
|
||||||
|
font_weight: Some("700".to_string()),
|
||||||
|
font_size: Some(32.0),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let item = engine
|
||||||
|
.try_build(&tree, copy, &HashMap::from([(copy, style)]))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
engine.items[item].buffer.lines[0]
|
||||||
|
.attrs_list()
|
||||||
|
.get_span(0)
|
||||||
|
.font_id_opt,
|
||||||
|
Some(selected_id),
|
||||||
|
"the resolved face must survive rich-text span construction"
|
||||||
|
);
|
||||||
|
engine.measure(item, None);
|
||||||
|
let shaped_id = engine.items[item]
|
||||||
|
.buffer
|
||||||
|
.layout_runs()
|
||||||
|
.next()
|
||||||
|
.and_then(|run| run.glyphs.first())
|
||||||
|
.map(|glyph| glyph.font_id)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
shaped_id, selected_id,
|
||||||
|
"family/default-weight rematching must not replace the descriptor-selected resource"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ranged_face_preserves_requested_weight_for_variable_raster() {
|
fn ranged_face_preserves_requested_weight_for_variable_raster() {
|
||||||
let loaded = HashMap::from([(
|
let loaded = HashMap::from([(
|
||||||
@@ -1872,10 +2059,12 @@ mod tests {
|
|||||||
LoadedFamily {
|
LoadedFamily {
|
||||||
faces: vec![LoadedFace {
|
faces: vec![LoadedFace {
|
||||||
name: Arc::from("Inter"),
|
name: Arc::from("Inter"),
|
||||||
|
font_id: None,
|
||||||
min_weight: 100,
|
min_weight: 100,
|
||||||
max_weight: 900,
|
max_weight: 900,
|
||||||
shape_weight: 400,
|
shape_weight: 400,
|
||||||
italic: false,
|
italic: false,
|
||||||
|
has_optical_size: true,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
)]);
|
)]);
|
||||||
@@ -1886,24 +2075,82 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn glyph_metadata_keeps_fill_and_variable_weight_independent() {
|
fn glyph_metadata_keeps_fill_and_variations_independent() {
|
||||||
|
let mut variations = FontVariations::new();
|
||||||
|
variations.set(VariationTag::new(b"wght"), 725.0);
|
||||||
let attrs = SpanAttrs {
|
let attrs = SpanAttrs {
|
||||||
font_size: 16.0,
|
font_size: 16.0,
|
||||||
line_height: 18.0,
|
line_height: 18.0,
|
||||||
letter_spacing: 0.0,
|
letter_spacing: 0.0,
|
||||||
letter_spacing_non_normal: false,
|
letter_spacing_non_normal: false,
|
||||||
weight: 400,
|
weight: 400,
|
||||||
variation_weight: Some(725),
|
font_id: None,
|
||||||
|
variations: Some(Arc::new(variations)),
|
||||||
italic: false,
|
italic: false,
|
||||||
underline: true,
|
underline: true,
|
||||||
color: [1, 2, 3, 255],
|
color: [1, 2, 3, 255],
|
||||||
family: Arc::from(FAMILY),
|
family: Arc::from(FAMILY),
|
||||||
clip_fill: Some(37),
|
clip_fill: Some(37),
|
||||||
};
|
};
|
||||||
let metadata = attrs.to_attrs().metadata;
|
let shaped = attrs.to_attrs(42);
|
||||||
assert_ne!(metadata & META_UNDERLINE, 0);
|
assert_ne!(shaped.metadata & META_UNDERLINE, 0);
|
||||||
assert_eq!(metadata_fill(metadata), Some(37));
|
assert_eq!(metadata_fill(shaped.metadata), Some(37));
|
||||||
assert_eq!(metadata_variation_weight(metadata), Some(725));
|
assert_eq!(metadata_variation(shaped.metadata), Some(41));
|
||||||
|
assert_eq!(
|
||||||
|
shaped
|
||||||
|
.font_variations
|
||||||
|
.iter()
|
||||||
|
.find(|variation| variation.tag == VariationTag::new(b"wght"))
|
||||||
|
.map(|variation| variation.value.0),
|
||||||
|
Some(725.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_text_keeps_the_zero_allocation_variation_path() {
|
||||||
|
let tree = obscura_dom::parse_html("<p id='copy'>ordinary text</p>");
|
||||||
|
let copy = tree.get_element_by_id("copy").unwrap();
|
||||||
|
let mut style = LayoutStyle::default();
|
||||||
|
style.display = Display::Block;
|
||||||
|
let static_font = ResolvedFont {
|
||||||
|
family: Arc::from(FAMILY),
|
||||||
|
font_id: None,
|
||||||
|
shape_weight: 400,
|
||||||
|
variation_weight: None,
|
||||||
|
has_optical_size: false,
|
||||||
|
};
|
||||||
|
assert!(resolved_font_variations(&style, &static_font).is_none());
|
||||||
|
let styles = HashMap::from([(copy, style)]);
|
||||||
|
let mut engine = TextEngine::new();
|
||||||
|
let item = engine.try_build(&tree, copy, &styles).unwrap();
|
||||||
|
|
||||||
|
assert!(engine.items[item].variation_sets.is_empty());
|
||||||
|
assert_eq!(engine.items[item].variation_sets.capacity(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn programmatic_nonfinite_variations_never_reach_shape_or_raster() {
|
||||||
|
let style = LayoutStyle {
|
||||||
|
font_variation_settings: Some(vec![
|
||||||
|
crate::FontVariationSetting {
|
||||||
|
tag: *b"opsz",
|
||||||
|
value: f32::NAN,
|
||||||
|
},
|
||||||
|
crate::FontVariationSetting {
|
||||||
|
tag: *b"wght",
|
||||||
|
value: f32::INFINITY,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let font = ResolvedFont {
|
||||||
|
family: Arc::from(FAMILY),
|
||||||
|
font_id: None,
|
||||||
|
shape_weight: 400,
|
||||||
|
variation_weight: None,
|
||||||
|
has_optical_size: false,
|
||||||
|
};
|
||||||
|
assert!(resolved_font_variations(&style, &font).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1914,14 +2161,15 @@ mod tests {
|
|||||||
letter_spacing: 2.0,
|
letter_spacing: 2.0,
|
||||||
letter_spacing_non_normal: true,
|
letter_spacing_non_normal: true,
|
||||||
weight: 400,
|
weight: 400,
|
||||||
variation_weight: None,
|
font_id: None,
|
||||||
|
variations: None,
|
||||||
italic: false,
|
italic: false,
|
||||||
underline: false,
|
underline: false,
|
||||||
color: [0, 0, 0, 255],
|
color: [0, 0, 0, 255],
|
||||||
family: Arc::from(FAMILY),
|
family: Arc::from(FAMILY),
|
||||||
clip_fill: None,
|
clip_fill: None,
|
||||||
};
|
};
|
||||||
let attrs = span.to_attrs();
|
let attrs = span.to_attrs(1);
|
||||||
assert_eq!(attrs.letter_spacing_opt.unwrap().0, 0.1);
|
assert_eq!(attrs.letter_spacing_opt.unwrap().0, 0.1);
|
||||||
assert!(attrs.font_features.features.iter().any(|feature| {
|
assert!(attrs.font_features.features.iter().any(|feature| {
|
||||||
feature.tag == FeatureTag::STANDARD_LIGATURES && feature.value == 0
|
feature.tag == FeatureTag::STANDARD_LIGATURES && feature.value == 0
|
||||||
@@ -1932,7 +2180,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut zero = span;
|
let mut zero = span;
|
||||||
zero.letter_spacing = 0.0;
|
zero.letter_spacing = 0.0;
|
||||||
assert!(zero.to_attrs().font_features.features.is_empty());
|
assert!(zero.to_attrs(1).font_features.features.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn variable_font_fixture() -> Vec<u8> {
|
fn variable_font_fixture() -> Vec<u8> {
|
||||||
@@ -1976,7 +2224,12 @@ mod tests {
|
|||||||
.variable_swash
|
.variable_swash
|
||||||
.images
|
.images
|
||||||
.keys()
|
.keys()
|
||||||
.map(|key| key.weight)
|
.filter_map(|key| {
|
||||||
|
key.variations
|
||||||
|
.iter()
|
||||||
|
.find(|variation| variation.tag == VariationTag::new(b"wght"))
|
||||||
|
.map(|variation| variation.value.0 as u16)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
cached_weights.sort_unstable();
|
cached_weights.sort_unstable();
|
||||||
cached_weights.dedup();
|
cached_weights.dedup();
|
||||||
@@ -1984,10 +2237,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn variable_wght_changes_true_outline_without_changing_layout_geometry() {
|
fn variable_wght_changes_shape_and_true_outline() {
|
||||||
let (regular_geometry, regular_ink, regular_cache) = render_variable_weight(400);
|
let (regular_geometry, regular_ink, regular_cache) = render_variable_weight(400);
|
||||||
let (black_geometry, black_ink, black_cache) = render_variable_weight(900);
|
let (black_geometry, black_ink, black_cache) = render_variable_weight(900);
|
||||||
assert_eq!(regular_geometry, black_geometry);
|
assert!(regular_geometry.0 > 0.0 && black_geometry.0 > 0.0);
|
||||||
assert!(
|
assert!(
|
||||||
black_ink > regular_ink * 13 / 10,
|
black_ink > regular_ink * 13 / 10,
|
||||||
"wght=900 should carry substantially more raster ink: {regular_ink} vs {black_ink}"
|
"wght=900 should carry substantially more raster ink: {regular_ink} vs {black_ink}"
|
||||||
@@ -2034,11 +2287,105 @@ mod tests {
|
|||||||
.variable_swash
|
.variable_swash
|
||||||
.images
|
.images
|
||||||
.keys()
|
.keys()
|
||||||
.map(|key| key.weight)
|
.filter_map(|key| {
|
||||||
|
key.variations
|
||||||
|
.iter()
|
||||||
|
.find(|variation| variation.tag == VariationTag::new(b"wght"))
|
||||||
|
.map(|variation| variation.value.0 as u16)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(weights, std::collections::HashSet::from([400, 900]));
|
assert_eq!(weights, std::collections::HashSet::from([400, 900]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_tailwind_optical_heading(
|
||||||
|
optical_sizing: crate::FontOpticalSizing,
|
||||||
|
explicit_opsz: Option<f32>,
|
||||||
|
width: f32,
|
||||||
|
) -> ((f32, f32), usize, std::collections::HashSet<u32>) {
|
||||||
|
let mut engine = TextEngine::new_with_web_fonts(&[WebFont {
|
||||||
|
data: variable_font_fixture(),
|
||||||
|
family: Some("Obscura VF Test".to_string()),
|
||||||
|
weight: Some((100, 900)),
|
||||||
|
italic: Some(false),
|
||||||
|
}]);
|
||||||
|
let tree = obscura_dom::parse_html(
|
||||||
|
"<h2 id='copy'>Build whatever you want, without touching your CSS file.</h2>",
|
||||||
|
);
|
||||||
|
let copy = tree.get_element_by_id("copy").unwrap();
|
||||||
|
let mut style = LayoutStyle::default();
|
||||||
|
style.display = Display::Block;
|
||||||
|
style.font_family = Some("Obscura VF Test".to_string());
|
||||||
|
style.font_weight = Some("500".to_string());
|
||||||
|
style.font_size = Some(40.0);
|
||||||
|
style.line_height = Some(crate::LineHeight::Px(40.0));
|
||||||
|
style.letter_spacing = Some(-2.0);
|
||||||
|
style.letter_spacing_non_normal = Some(true);
|
||||||
|
style.font_optical_sizing = Some(optical_sizing);
|
||||||
|
style.font_variation_settings = Some(
|
||||||
|
explicit_opsz
|
||||||
|
.map(|value| {
|
||||||
|
vec![crate::FontVariationSetting {
|
||||||
|
tag: *b"opsz",
|
||||||
|
value,
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
let styles = HashMap::from([(copy, style)]);
|
||||||
|
let item = engine.try_build(&tree, copy, &styles).unwrap();
|
||||||
|
let geometry = engine.measure(item, Some(width));
|
||||||
|
let lines = engine.items[item].buffer.layout_runs().count();
|
||||||
|
engine.finalize(item, (0.0, 0.0), width, None);
|
||||||
|
let mut pixmap = tiny_skia::Pixmap::new(512, 140).unwrap();
|
||||||
|
engine.paint_item(item, &mut pixmap, (0.0, 0.0));
|
||||||
|
let optical_values = engine
|
||||||
|
.variable_swash
|
||||||
|
.images
|
||||||
|
.keys()
|
||||||
|
.filter_map(|key| {
|
||||||
|
key.variations
|
||||||
|
.iter()
|
||||||
|
.find(|variation| variation.tag == VariationTag::new(b"opsz"))
|
||||||
|
.map(|variation| variation.value.0.to_bits())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(geometry, lines, optical_values)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn optical_sizing_changes_tailwind_heading_shape_and_raster_axes() {
|
||||||
|
let (auto_geometry, auto_lines, auto_axes) = render_tailwind_optical_heading(
|
||||||
|
crate::FontOpticalSizing::Auto,
|
||||||
|
None,
|
||||||
|
496.0,
|
||||||
|
);
|
||||||
|
let (none_geometry, none_lines, none_axes) = render_tailwind_optical_heading(
|
||||||
|
crate::FontOpticalSizing::None,
|
||||||
|
None,
|
||||||
|
496.0,
|
||||||
|
);
|
||||||
|
let (explicit_geometry, explicit_lines, explicit_axes) =
|
||||||
|
render_tailwind_optical_heading(
|
||||||
|
crate::FontOpticalSizing::Auto,
|
||||||
|
Some(14.0),
|
||||||
|
496.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_ne!(
|
||||||
|
auto_geometry, none_geometry,
|
||||||
|
"automatic opsz must affect the shaped heading advances"
|
||||||
|
);
|
||||||
|
assert_eq!(auto_lines, none_lines);
|
||||||
|
assert_eq!(explicit_lines, none_lines);
|
||||||
|
assert_eq!(explicit_geometry, none_geometry);
|
||||||
|
assert_eq!(auto_axes, std::collections::HashSet::from([40.0f32.to_bits()]));
|
||||||
|
assert!(none_axes.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
explicit_axes,
|
||||||
|
std::collections::HashSet::from([14.0f32.to_bits()])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn text_only_inline_block_keeps_an_internal_shaping_context() {
|
fn text_only_inline_block_keeps_an_internal_shaping_context() {
|
||||||
let tree = obscura_dom::parse_html(
|
let tree = obscura_dom::parse_html(
|
||||||
|
|||||||
@@ -183,6 +183,27 @@ impl Dimension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computed CSS `font-optical-sizing`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum FontOpticalSizing {
|
||||||
|
/// Let a variable font's `opsz` axis follow the computed font size.
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
/// Leave the font's optical-size axis at its normal/default setting.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One canonical CSS `font-variation-settings` axis assignment.
|
||||||
|
///
|
||||||
|
/// The CSS parser guarantees a printable four-byte ASCII tag and a finite
|
||||||
|
/// value. Settings are stored in tag order with duplicate tags collapsed so
|
||||||
|
/// shaping and rasterization can consume one deterministic axis tuple.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct FontVariationSetting {
|
||||||
|
pub tag: [u8; 4],
|
||||||
|
pub value: f32,
|
||||||
|
}
|
||||||
|
|
||||||
/// The subset of CSS that influences box layout. Expanded in later phases.
|
/// The subset of CSS that influences box layout. Expanded in later phases.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct LayoutStyle {
|
pub struct LayoutStyle {
|
||||||
@@ -374,6 +395,12 @@ pub struct LayoutStyle {
|
|||||||
/// resolves it to a bundled face (Liberation Sans/Serif/Mono) the way
|
/// resolves it to a bundled face (Liberation Sans/Serif/Mono) the way
|
||||||
/// Chromium picks a generic family on this host.
|
/// Chromium picks a generic family on this host.
|
||||||
pub font_family: Option<String>,
|
pub font_family: Option<String>,
|
||||||
|
/// Computed inherited `font-optical-sizing`. `None` during cascade means
|
||||||
|
/// inherit; the top-down pass resolves every element to `Some`.
|
||||||
|
pub font_optical_sizing: Option<FontOpticalSizing>,
|
||||||
|
/// Computed inherited `font-variation-settings`. `None` during cascade
|
||||||
|
/// means inherit, while `Some(Vec::new())` is the `normal` initial value.
|
||||||
|
pub font_variation_settings: Option<Vec<FontVariationSetting>>,
|
||||||
/// Inherited `text-align`, represented with the matching horizontal
|
/// Inherited `text-align`, represented with the matching horizontal
|
||||||
/// alignment keywords. Kept separate from flex/grid `align-items`: using
|
/// alignment keywords. Kept separate from flex/grid `align-items`: using
|
||||||
/// one field for both made `text-align:left` shrink-wrap flex children.
|
/// one field for both made `text-align:left` shrink-wrap flex children.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
//! the crate dependency-light.
|
//! the crate dependency-light.
|
||||||
|
|
||||||
use crate::{Display, Edges, LayoutStyle};
|
use crate::{Display, Edges, LayoutStyle};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
/// Compute the layout-relevant style for an element: UA defaults for its tag,
|
/// Compute the layout-relevant style for an element: UA defaults for its tag,
|
||||||
/// overridden by its inline `style="..."` declarations.
|
/// overridden by its inline `style="..."` declarations.
|
||||||
@@ -431,6 +432,106 @@ fn parse_container_shorthand(value: &str) -> Option<(Vec<String>, crate::Contain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_font_variation_settings(value: &str) -> Option<Vec<crate::FontVariationSetting>> {
|
||||||
|
let mut input = cssparser::ParserInput::new(value.trim());
|
||||||
|
let mut parser = cssparser::Parser::new(&mut input);
|
||||||
|
if parser.is_exhausted() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A map both implements CSS's "last duplicate wins" rule and gives the
|
||||||
|
// inline engine a stable tag order independent of author ordering.
|
||||||
|
let mut settings = BTreeMap::new();
|
||||||
|
loop {
|
||||||
|
let tag = parser.expect_string_cloned().ok()?;
|
||||||
|
let bytes = tag.as_bytes();
|
||||||
|
if bytes.len() != 4 || !bytes.iter().all(|byte| (0x20..=0x7e).contains(byte)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value = parse_font_variation_number(&mut parser)?;
|
||||||
|
if !value.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let tag = [bytes[0], bytes[1], bytes[2], bytes[3]];
|
||||||
|
// Keep numerically equivalent signed zeroes canonical for stable cache
|
||||||
|
// keys later in the shaping and rasterization pipeline.
|
||||||
|
settings.insert(tag, if value == 0.0 { 0.0 } else { value });
|
||||||
|
if parser.is_exhausted() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parser.expect_comma().ok()?;
|
||||||
|
if parser.is_exhausted() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
settings
|
||||||
|
.into_iter()
|
||||||
|
.map(|(tag, value)| crate::FontVariationSetting { tag, value })
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_font_variation_number(parser: &mut cssparser::Parser<'_, '_>) -> Option<f32> {
|
||||||
|
if let Ok(value) = parser.try_parse(|input| input.expect_number()) {
|
||||||
|
return value.is_finite().then_some(value);
|
||||||
|
}
|
||||||
|
let start = parser.position();
|
||||||
|
let token = parser.next().ok()?.clone();
|
||||||
|
let cssparser::Token::Function(name) = token else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if !matches!(
|
||||||
|
name.to_ascii_lowercase().as_str(),
|
||||||
|
"calc" | "min" | "max" | "clamp" | "round"
|
||||||
|
) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
parser
|
||||||
|
.parse_nested_block(|input| unitless_math_tokens(input, 0))
|
||||||
|
.ok()?;
|
||||||
|
let expression = parser.slice_from(start);
|
||||||
|
resolve_contextual_length(expression, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||||
|
.filter(|value| value.is_finite())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unitless_math_tokens<'i, 't>(
|
||||||
|
parser: &mut cssparser::Parser<'i, 't>,
|
||||||
|
depth: u8,
|
||||||
|
) -> Result<(), cssparser::ParseError<'i, ()>> {
|
||||||
|
if depth >= 64 {
|
||||||
|
return Err(parser.new_custom_error(()));
|
||||||
|
}
|
||||||
|
while !parser.is_exhausted() {
|
||||||
|
let token = parser
|
||||||
|
.next_including_whitespace_and_comments()?
|
||||||
|
.clone();
|
||||||
|
match token {
|
||||||
|
cssparser::Token::Number { .. }
|
||||||
|
| cssparser::Token::WhiteSpace(_)
|
||||||
|
| cssparser::Token::Comment(_)
|
||||||
|
| cssparser::Token::Comma
|
||||||
|
| cssparser::Token::Delim('+')
|
||||||
|
| cssparser::Token::Delim('-')
|
||||||
|
| cssparser::Token::Delim('*')
|
||||||
|
| cssparser::Token::Delim('/') => {}
|
||||||
|
cssparser::Token::Function(name)
|
||||||
|
if matches!(
|
||||||
|
name.to_ascii_lowercase().as_str(),
|
||||||
|
"calc" | "min" | "max" | "clamp" | "round"
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
parser.parse_nested_block(|input| unitless_math_tokens(input, depth + 1))?;
|
||||||
|
}
|
||||||
|
cssparser::Token::ParenthesisBlock => {
|
||||||
|
parser.parse_nested_block(|input| unitless_math_tokens(input, depth + 1))?;
|
||||||
|
}
|
||||||
|
_ => return Err(parser.new_custom_error(())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||||
match name {
|
match name {
|
||||||
"display" => {
|
"display" => {
|
||||||
@@ -735,6 +836,30 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
style.font_family = Some(v);
|
style.font_family = Some(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"font-optical-sizing" => {
|
||||||
|
let parsed = match value.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"inherit" | "unset" | "revert" | "revert-layer" => Some(None),
|
||||||
|
"auto" | "initial" => Some(Some(crate::FontOpticalSizing::Auto)),
|
||||||
|
"none" => Some(Some(crate::FontOpticalSizing::None)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(value) = parsed {
|
||||||
|
style.font_optical_sizing = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"font-variation-settings" => {
|
||||||
|
let lower = value.trim().to_ascii_lowercase();
|
||||||
|
if matches!(
|
||||||
|
lower.as_str(),
|
||||||
|
"inherit" | "unset" | "revert" | "revert-layer"
|
||||||
|
) {
|
||||||
|
style.font_variation_settings = None;
|
||||||
|
} else if matches!(lower.as_str(), "normal" | "initial") {
|
||||||
|
style.font_variation_settings = Some(Vec::new());
|
||||||
|
} else if let Some(settings) = parse_font_variation_settings(value) {
|
||||||
|
style.font_variation_settings = Some(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Text alignment is inherited and applies to inline line boxes. It is
|
// Text alignment is inherited and applies to inline line boxes. It is
|
||||||
// deliberately separate from flex/grid `align-items`, which positions
|
// deliberately separate from flex/grid `align-items`, which positions
|
||||||
// child boxes rather than text inside them.
|
// child boxes rather than text inside them.
|
||||||
@@ -1242,6 +1367,8 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
| "font-weight"
|
| "font-weight"
|
||||||
| "font-family"
|
| "font-family"
|
||||||
| "font-style"
|
| "font-style"
|
||||||
|
| "font-optical-sizing"
|
||||||
|
| "font-variation-settings"
|
||||||
| "text-align"
|
| "text-align"
|
||||||
| "text-transform"
|
| "text-transform"
|
||||||
| "text-decoration"
|
| "text-decoration"
|
||||||
@@ -1358,6 +1485,14 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
"container-type" => parse_container_type(value).is_some(),
|
"container-type" => parse_container_type(value).is_some(),
|
||||||
"container-name" => parse_container_names(value).is_some(),
|
"container-name" => parse_container_names(value).is_some(),
|
||||||
"container" => parse_container_shorthand(value).is_some(),
|
"container" => parse_container_shorthand(value).is_some(),
|
||||||
|
"font-optical-sizing" => matches!(
|
||||||
|
value.to_ascii_lowercase().as_str(),
|
||||||
|
"auto" | "none"
|
||||||
|
),
|
||||||
|
"font-variation-settings" => {
|
||||||
|
value.eq_ignore_ascii_case("normal")
|
||||||
|
|| parse_font_variation_settings(value).is_some()
|
||||||
|
}
|
||||||
"float" => matches!(
|
"float" => matches!(
|
||||||
value.to_ascii_lowercase().as_str(),
|
value.to_ascii_lowercase().as_str(),
|
||||||
"none" | "left" | "right"
|
"none" | "left" | "right"
|
||||||
@@ -3386,6 +3521,8 @@ fn apply_font_shorthand(style: &mut LayoutStyle, value: &str) {
|
|||||||
// The shorthand resets every constituent before applying supplied values.
|
// The shorthand resets every constituent before applying supplied values.
|
||||||
style.font_style_italic = Some(false);
|
style.font_style_italic = Some(false);
|
||||||
style.font_weight = Some("400".to_string());
|
style.font_weight = Some("400".to_string());
|
||||||
|
style.font_optical_sizing = Some(crate::FontOpticalSizing::Auto);
|
||||||
|
style.font_variation_settings = Some(Vec::new());
|
||||||
style.line_height = Some(crate::LineHeight::Normal);
|
style.line_height = Some(crate::LineHeight::Normal);
|
||||||
style.line_height_expression = None;
|
style.line_height_expression = None;
|
||||||
for token in &tokens[..size_index] {
|
for token in &tokens[..size_index] {
|
||||||
@@ -4508,6 +4645,168 @@ mod tests {
|
|||||||
assert_eq!(computed_font_weight(Some("lighter"), 900), 700);
|
assert_eq!(computed_font_weight(Some("lighter"), 900), 700);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_font_properties_parse_canonically_and_atomically() {
|
||||||
|
let style = compute_style(
|
||||||
|
"span",
|
||||||
|
Some(
|
||||||
|
r#"font-optical-sizing:none;
|
||||||
|
font-variation-settings:"wght" 500, "\6f psz" 14, "wght" 650"#,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
style.font_optical_sizing,
|
||||||
|
Some(crate::FontOpticalSizing::None)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
style.font_variation_settings.as_deref(),
|
||||||
|
Some(
|
||||||
|
[
|
||||||
|
crate::FontVariationSetting {
|
||||||
|
tag: *b"opsz",
|
||||||
|
value: 14.0,
|
||||||
|
},
|
||||||
|
crate::FontVariationSetting {
|
||||||
|
tag: *b"wght",
|
||||||
|
value: 650.0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.as_slice()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let case_sensitive = compute_style(
|
||||||
|
"span",
|
||||||
|
Some(r#"font-variation-settings:"wght" 400, "WGHT" 700"#),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
case_sensitive.font_variation_settings.as_deref(),
|
||||||
|
Some(
|
||||||
|
[
|
||||||
|
crate::FontVariationSetting {
|
||||||
|
tag: *b"WGHT",
|
||||||
|
value: 700.0,
|
||||||
|
},
|
||||||
|
crate::FontVariationSetting {
|
||||||
|
tag: *b"wght",
|
||||||
|
value: 400.0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.as_slice()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for malformed in [
|
||||||
|
r#""abc" 1"#,
|
||||||
|
r#"wght 1"#,
|
||||||
|
r#""wght" 1,"#,
|
||||||
|
r#""wght" calc(1px)"#,
|
||||||
|
r#""wght" 1e999"#,
|
||||||
|
r#""wégt" 1"#,
|
||||||
|
] {
|
||||||
|
let css = format!(
|
||||||
|
r#"font-variation-settings:"opsz" 20;font-variation-settings:{malformed}"#
|
||||||
|
);
|
||||||
|
let unchanged = compute_style("span", Some(&css));
|
||||||
|
assert_eq!(
|
||||||
|
unchanged.font_variation_settings.as_deref(),
|
||||||
|
Some(
|
||||||
|
[crate::FontVariationSetting {
|
||||||
|
tag: *b"opsz",
|
||||||
|
value: 20.0,
|
||||||
|
}]
|
||||||
|
.as_slice()
|
||||||
|
),
|
||||||
|
"invalid declaration was not atomic: {malformed}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_font_css_wide_values_and_font_shorthand_obey_cascade() {
|
||||||
|
let inherited = compute_style(
|
||||||
|
"span",
|
||||||
|
Some(
|
||||||
|
r#"font-optical-sizing:none;font-optical-sizing:unset;
|
||||||
|
font-variation-settings:"wght" 700;font-variation-settings:inherit"#,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(inherited.font_optical_sizing, None);
|
||||||
|
assert_eq!(inherited.font_variation_settings, None);
|
||||||
|
|
||||||
|
let reset = compute_style(
|
||||||
|
"span",
|
||||||
|
Some(
|
||||||
|
r#"font-optical-sizing:none;font-optical-sizing:initial;
|
||||||
|
font-variation-settings:"wght" 700;font-variation-settings:normal"#,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
reset.font_optical_sizing,
|
||||||
|
Some(crate::FontOpticalSizing::Auto)
|
||||||
|
);
|
||||||
|
assert_eq!(reset.font_variation_settings, Some(Vec::new()));
|
||||||
|
|
||||||
|
let reverted = compute_style(
|
||||||
|
"span",
|
||||||
|
Some(
|
||||||
|
r#"font-optical-sizing:none;font-optical-sizing:revert;
|
||||||
|
font-variation-settings:"wght" 700;font-variation-settings:revert-layer"#,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(reverted.font_optical_sizing, None);
|
||||||
|
assert_eq!(reverted.font_variation_settings, None);
|
||||||
|
|
||||||
|
let shorthand = compute_style(
|
||||||
|
"span",
|
||||||
|
Some(
|
||||||
|
r#"font-optical-sizing:none;font-variation-settings:"opsz" 22;
|
||||||
|
font:italic 500 20px/1.2 Inter"#,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
shorthand.font_optical_sizing,
|
||||||
|
Some(crate::FontOpticalSizing::Auto)
|
||||||
|
);
|
||||||
|
assert_eq!(shorthand.font_variation_settings, Some(Vec::new()));
|
||||||
|
|
||||||
|
assert!(supports_declaration("font-optical-sizing", "auto"));
|
||||||
|
assert!(!supports_declaration("font-optical-sizing", "enabled"));
|
||||||
|
assert!(supports_declaration(
|
||||||
|
"font-variation-settings",
|
||||||
|
r#""opsz" 18, "wght" 500"#
|
||||||
|
));
|
||||||
|
assert!(supports_declaration(
|
||||||
|
"font-variation-settings",
|
||||||
|
r#""opsz" calc(18)"#
|
||||||
|
));
|
||||||
|
|
||||||
|
let calculated = compute_style(
|
||||||
|
"span",
|
||||||
|
Some(r#"font-variation-settings:"opsz" calc(10 * 2), "wght" max(400, 500)"#),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
calculated.font_variation_settings.as_deref(),
|
||||||
|
Some(
|
||||||
|
[
|
||||||
|
crate::FontVariationSetting {
|
||||||
|
tag: *b"opsz",
|
||||||
|
value: 20.0,
|
||||||
|
},
|
||||||
|
crate::FontVariationSetting {
|
||||||
|
tag: *b"wght",
|
||||||
|
value: 500.0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.as_slice()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert!(!supports_declaration(
|
||||||
|
"font-variation-settings",
|
||||||
|
r#""opsz" calc(18px)"#
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn letter_spacing_preserves_units_resets_and_invalid_cascade_values() {
|
fn letter_spacing_preserves_units_resets_and_invalid_cascade_values() {
|
||||||
let relative = compute_style("span", Some("letter-spacing:-.05em"));
|
let relative = compute_style("span", Some("letter-spacing:-.05em"));
|
||||||
|
|||||||
Vendored
+187
@@ -0,0 +1,187 @@
|
|||||||
|
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||||
|
#
|
||||||
|
# When uploading crates to the registry Cargo will automatically
|
||||||
|
# "normalize" Cargo.toml files for maximal compatibility
|
||||||
|
# with all versions of Cargo and also rewrite `path` dependencies
|
||||||
|
# to registry (e.g., crates.io) dependencies.
|
||||||
|
#
|
||||||
|
# If you are reading this file be aware that the original Cargo.toml
|
||||||
|
# will likely look very different (and much more reasonable).
|
||||||
|
# See Cargo.toml.orig for the original contents.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.75"
|
||||||
|
name = "cosmic-text"
|
||||||
|
version = "0.14.2"
|
||||||
|
authors = ["Jeremy Soller <[email protected]>"]
|
||||||
|
build = false
|
||||||
|
autolib = false
|
||||||
|
autobins = false
|
||||||
|
autoexamples = false
|
||||||
|
autotests = false
|
||||||
|
autobenches = false
|
||||||
|
description = "Pure Rust multi-line text handling"
|
||||||
|
documentation = "https://docs.rs/cosmic-text/latest/cosmic_text/"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/pop-os/cosmic-text"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
features = ["vi"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [
|
||||||
|
"std",
|
||||||
|
"swash",
|
||||||
|
"fontconfig",
|
||||||
|
]
|
||||||
|
fontconfig = [
|
||||||
|
"fontdb/fontconfig",
|
||||||
|
"std",
|
||||||
|
]
|
||||||
|
monospace_fallback = []
|
||||||
|
no_std = [
|
||||||
|
"rustybuzz/libm",
|
||||||
|
"hashbrown",
|
||||||
|
"dep:libm",
|
||||||
|
]
|
||||||
|
peniko = ["dep:peniko"]
|
||||||
|
shape-run-cache = []
|
||||||
|
std = [
|
||||||
|
"fontdb/memmap",
|
||||||
|
"fontdb/std",
|
||||||
|
"rustybuzz/std",
|
||||||
|
"swash?/std",
|
||||||
|
"sys-locale",
|
||||||
|
"ttf-parser/std",
|
||||||
|
"unicode-bidi/std",
|
||||||
|
]
|
||||||
|
vi = [
|
||||||
|
"modit",
|
||||||
|
"syntect",
|
||||||
|
"cosmic_undo_2",
|
||||||
|
]
|
||||||
|
warn_on_missing_glyphs = []
|
||||||
|
wasm-web = ["sys-locale?/js"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "cosmic_text"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "editor_modified_state"
|
||||||
|
path = "tests/editor_modified_state.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "shaping_and_rendering"
|
||||||
|
path = "tests/shaping_and_rendering.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "wrap_stability"
|
||||||
|
path = "tests/wrap_stability.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "wrap_word_fallback"
|
||||||
|
path = "tests/wrap_word_fallback.rs"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "layout"
|
||||||
|
path = "benches/layout.rs"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[dependencies.bitflags]
|
||||||
|
version = "2.4.1"
|
||||||
|
|
||||||
|
[dependencies.cosmic_undo_2]
|
||||||
|
version = "0.2.0"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.fontdb]
|
||||||
|
version = "0.16"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.hashbrown]
|
||||||
|
version = "0.14.1"
|
||||||
|
optional = true
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.libm]
|
||||||
|
version = "0.2.8"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.log]
|
||||||
|
version = "0.4.20"
|
||||||
|
|
||||||
|
[dependencies.modit]
|
||||||
|
version = "0.1.4"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.peniko]
|
||||||
|
version = "0.3.1"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.rangemap]
|
||||||
|
version = "1.4.0"
|
||||||
|
|
||||||
|
[dependencies.rustc-hash]
|
||||||
|
version = "1.1.0"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.rustybuzz]
|
||||||
|
version = "0.14"
|
||||||
|
features = ["libm"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.self_cell]
|
||||||
|
version = "1.0.1"
|
||||||
|
|
||||||
|
[dependencies.smol_str]
|
||||||
|
version = "0.2.2"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.swash]
|
||||||
|
version = "0.2.0"
|
||||||
|
features = [
|
||||||
|
"render",
|
||||||
|
"scale",
|
||||||
|
]
|
||||||
|
optional = true
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.syntect]
|
||||||
|
version = "5.1.0"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.sys-locale]
|
||||||
|
version = "0.3.1"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.ttf-parser]
|
||||||
|
version = "0.21"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.unicode-bidi]
|
||||||
|
version = "0.3.13"
|
||||||
|
features = ["hardcoded-data"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.unicode-linebreak]
|
||||||
|
version = "0.1.5"
|
||||||
|
|
||||||
|
[dependencies.unicode-script]
|
||||||
|
version = "0.5.5"
|
||||||
|
|
||||||
|
[dependencies.unicode-segmentation]
|
||||||
|
version = "1.10.1"
|
||||||
|
|
||||||
|
[dev-dependencies.criterion]
|
||||||
|
version = "0.5.1"
|
||||||
|
features = ["cargo_bench_support"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dev-dependencies.tiny-skia]
|
||||||
|
version = "0.11.2"
|
||||||
|
|
||||||
|
[profile.test]
|
||||||
|
opt-level = 1
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
[package]
|
||||||
|
name = "cosmic-text"
|
||||||
|
description = "Pure Rust multi-line text handling"
|
||||||
|
version = "0.14.2"
|
||||||
|
authors = ["Jeremy Soller <[email protected]>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
documentation = "https://docs.rs/cosmic-text/latest/cosmic_text/"
|
||||||
|
repository = "https://github.com/pop-os/cosmic-text"
|
||||||
|
rust-version = "1.75"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bitflags = "2.4.1"
|
||||||
|
cosmic_undo_2 = { version = "0.2.0", optional = true }
|
||||||
|
fontdb = { version = "0.16", default-features = false }
|
||||||
|
hashbrown = { version = "0.14.1", optional = true, default-features = false }
|
||||||
|
libm = { version = "0.2.8", optional = true }
|
||||||
|
log = "0.4.20"
|
||||||
|
modit = { version = "0.1.4", optional = true }
|
||||||
|
rangemap = "1.4.0"
|
||||||
|
rustc-hash = { version = "1.1.0", default-features = false }
|
||||||
|
rustybuzz = { version = "0.14", default-features = false, features = ["libm"] }
|
||||||
|
self_cell = "1.0.1"
|
||||||
|
smol_str = { version = "0.2.2", default-features = false }
|
||||||
|
syntect = { version = "5.1.0", optional = true }
|
||||||
|
sys-locale = { version = "0.3.1", optional = true }
|
||||||
|
ttf-parser = { version = "0.21", default-features = false }
|
||||||
|
unicode-linebreak = "0.1.5"
|
||||||
|
unicode-script = "0.5.5"
|
||||||
|
unicode-segmentation = "1.10.1"
|
||||||
|
|
||||||
|
[dependencies.swash]
|
||||||
|
version = "0.2.0"
|
||||||
|
default-features = false
|
||||||
|
features = ["render", "scale"]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.unicode-bidi]
|
||||||
|
version = "0.3.13"
|
||||||
|
default-features = false
|
||||||
|
features = ["hardcoded-data"]
|
||||||
|
|
||||||
|
[dependencies.peniko]
|
||||||
|
version = "0.3.1"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["std", "swash", "fontconfig"]
|
||||||
|
fontconfig = ["fontdb/fontconfig", "std"]
|
||||||
|
monospace_fallback = []
|
||||||
|
no_std = ["rustybuzz/libm", "hashbrown", "dep:libm"]
|
||||||
|
peniko = ["dep:peniko"]
|
||||||
|
shape-run-cache = []
|
||||||
|
std = [
|
||||||
|
"fontdb/memmap",
|
||||||
|
"fontdb/std",
|
||||||
|
"rustybuzz/std",
|
||||||
|
"swash?/std",
|
||||||
|
"sys-locale",
|
||||||
|
"ttf-parser/std",
|
||||||
|
"unicode-bidi/std",
|
||||||
|
]
|
||||||
|
vi = ["modit", "syntect", "cosmic_undo_2"]
|
||||||
|
wasm-web = ["sys-locale?/js"]
|
||||||
|
warn_on_missing_glyphs = []
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "layout"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["examples/*"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tiny-skia = "0.11.2"
|
||||||
|
criterion = { version = "0.5.1", default-features = false, features = [
|
||||||
|
"cargo_bench_support",
|
||||||
|
] }
|
||||||
|
|
||||||
|
[profile.test]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
features = ["vi"]
|
||||||
Vendored
+201
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 System76
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Vendored
+94
@@ -0,0 +1,94 @@
|
|||||||
|
# COSMIC Text
|
||||||
|
|
||||||
|
[](https://crates.io/crates/cosmic-text)
|
||||||
|
[](https://docs.rs/cosmic-text)
|
||||||
|

|
||||||
|
[](https://github.com/pop-os/cosmic-text/actions)
|
||||||
|
|
||||||
|
Pure Rust multi-line text handling.
|
||||||
|
|
||||||
|
COSMIC Text provides advanced text shaping, layout, and rendering wrapped up
|
||||||
|
into a simple abstraction. Shaping is provided by rustybuzz, and supports a
|
||||||
|
wide variety of advanced shaping operations. Rendering is provided by swash,
|
||||||
|
which supports ligatures and color emoji. Layout is implemented custom, in safe
|
||||||
|
Rust, and supports bidirectional text. Font fallback is also a custom
|
||||||
|
implementation, reusing some of the static fallback lists in browsers such as
|
||||||
|
Chromium and Firefox. Linux, macOS, and Windows are supported with the full
|
||||||
|
feature set. Other platforms may need to implement font fallback capabilities.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Arabic translation of Universal Declaration of Human Rights
|
||||||
|
[](screenshots/arabic.png)
|
||||||
|
|
||||||
|
Hindi translation of Universal Declaration of Human Rights
|
||||||
|
[](screenshots/hindi.png)
|
||||||
|
|
||||||
|
Simplified Chinese translation of Universal Declaration of Human Rights
|
||||||
|
[](screenshots/chinese-simplified.png)
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
The following features must be supported before this is "ready":
|
||||||
|
|
||||||
|
- [x] Font loading (using fontdb)
|
||||||
|
- [x] Preset fonts
|
||||||
|
- [x] System fonts
|
||||||
|
- [x] Text styles (bold, italic, etc.)
|
||||||
|
- [x] Per-buffer
|
||||||
|
- [x] Per-span
|
||||||
|
- [x] Font shaping (using rustybuzz)
|
||||||
|
- [x] Cache results
|
||||||
|
- [x] RTL
|
||||||
|
- [x] Bidirectional rendering
|
||||||
|
- [x] Font fallback
|
||||||
|
- [x] Choose font based on locale to work around "unification"
|
||||||
|
- [x] Per-line granularity
|
||||||
|
- [x] Per-character granularity
|
||||||
|
- [x] Font layout
|
||||||
|
- [x] Click detection
|
||||||
|
- [x] Simple wrapping
|
||||||
|
- [ ] Wrapping with indentation
|
||||||
|
- [ ] No wrapping
|
||||||
|
- [ ] Ellipsize
|
||||||
|
- [x] Font rendering (using swash)
|
||||||
|
- [x] Cache results
|
||||||
|
- [x] Font hinting
|
||||||
|
- [x] Ligatures
|
||||||
|
- [x] Color emoji
|
||||||
|
- [x] Text editing
|
||||||
|
- [x] Performance improvements
|
||||||
|
- [x] Text selection
|
||||||
|
- [x] Can automatically recreate https://unicode.org/udhr/ without errors (see below)
|
||||||
|
- [x] Bidirectional selection
|
||||||
|
- [ ] Copy/paste
|
||||||
|
- [x] no_std support (with `default-features = false`)
|
||||||
|
- [ ] no_std font loading
|
||||||
|
- [x] no_std shaping
|
||||||
|
- [x] no_std layout
|
||||||
|
- [ ] no_std rendering
|
||||||
|
|
||||||
|
The UDHR (Universal Declaration of Human Rights) test involves taking the entire
|
||||||
|
set of UDHR translations (almost 500 languages), concatenating them as one file
|
||||||
|
(which ends up being 8 megabytes!), then via the `editor-test` example,
|
||||||
|
automatically simulating the entry of that file into cosmic-text per-character,
|
||||||
|
with the use of backspace and delete tested per character and per line. Then,
|
||||||
|
the final contents of the buffer is compared to the original file. All of the
|
||||||
|
106746 lines are correct.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
|
||||||
|
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||||
|
http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
at your option.
|
||||||
|
|
||||||
|
### Contribution
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||||
|
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||||
|
dual licensed as above, without any additional terms or conditions.
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2012 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+694
@@ -0,0 +1,694 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use core::hash::{Hash, Hasher};
|
||||||
|
use core::ops::Range;
|
||||||
|
use rangemap::RangeMap;
|
||||||
|
use smol_str::SmolStr;
|
||||||
|
|
||||||
|
use crate::{CacheKeyFlags, Metrics};
|
||||||
|
|
||||||
|
pub use fontdb::{Family, Stretch, Style, Weight};
|
||||||
|
|
||||||
|
/// Text color
|
||||||
|
#[derive(Clone, Copy, Debug, PartialOrd, Ord, Eq, Hash, PartialEq)]
|
||||||
|
pub struct Color(pub u32);
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
/// Create new color with red, green, and blue components
|
||||||
|
#[inline]
|
||||||
|
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||||
|
Self::rgba(r, g, b, 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new color with red, green, blue, and alpha components
|
||||||
|
#[inline]
|
||||||
|
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
|
Self(((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a tuple over all of the attributes, in `(r, g, b, a)` order.
|
||||||
|
#[inline]
|
||||||
|
pub fn as_rgba_tuple(self) -> (u8, u8, u8, u8) {
|
||||||
|
(self.r(), self.g(), self.b(), self.a())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an array over all of the components, in `[r, g, b, a]` order.
|
||||||
|
#[inline]
|
||||||
|
pub fn as_rgba(self) -> [u8; 4] {
|
||||||
|
[self.r(), self.g(), self.b(), self.a()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the red component
|
||||||
|
#[inline]
|
||||||
|
pub fn r(&self) -> u8 {
|
||||||
|
((self.0 & 0x00_FF_00_00) >> 16) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the green component
|
||||||
|
#[inline]
|
||||||
|
pub fn g(&self) -> u8 {
|
||||||
|
((self.0 & 0x00_00_FF_00) >> 8) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the blue component
|
||||||
|
#[inline]
|
||||||
|
pub fn b(&self) -> u8 {
|
||||||
|
(self.0 & 0x00_00_00_FF) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the alpha component
|
||||||
|
#[inline]
|
||||||
|
pub fn a(&self) -> u8 {
|
||||||
|
((self.0 & 0xFF_00_00_00) >> 24) as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An owned version of [`Family`]
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum FamilyOwned {
|
||||||
|
Name(SmolStr),
|
||||||
|
Serif,
|
||||||
|
SansSerif,
|
||||||
|
Cursive,
|
||||||
|
Fantasy,
|
||||||
|
Monospace,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FamilyOwned {
|
||||||
|
pub fn new(family: Family) -> Self {
|
||||||
|
match family {
|
||||||
|
Family::Name(name) => FamilyOwned::Name(SmolStr::from(name)),
|
||||||
|
Family::Serif => FamilyOwned::Serif,
|
||||||
|
Family::SansSerif => FamilyOwned::SansSerif,
|
||||||
|
Family::Cursive => FamilyOwned::Cursive,
|
||||||
|
Family::Fantasy => FamilyOwned::Fantasy,
|
||||||
|
Family::Monospace => FamilyOwned::Monospace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_family(&self) -> Family {
|
||||||
|
match self {
|
||||||
|
FamilyOwned::Name(name) => Family::Name(name),
|
||||||
|
FamilyOwned::Serif => Family::Serif,
|
||||||
|
FamilyOwned::SansSerif => Family::SansSerif,
|
||||||
|
FamilyOwned::Cursive => Family::Cursive,
|
||||||
|
FamilyOwned::Fantasy => Family::Fantasy,
|
||||||
|
FamilyOwned::Monospace => Family::Monospace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metrics, but implementing Eq and Hash using u32 representation of f32
|
||||||
|
//TODO: what are the edge cases of this?
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct CacheMetrics {
|
||||||
|
font_size_bits: u32,
|
||||||
|
line_height_bits: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Metrics> for CacheMetrics {
|
||||||
|
fn from(metrics: Metrics) -> Self {
|
||||||
|
Self {
|
||||||
|
font_size_bits: metrics.font_size.to_bits(),
|
||||||
|
line_height_bits: metrics.line_height.to_bits(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CacheMetrics> for Metrics {
|
||||||
|
fn from(metrics: CacheMetrics) -> Self {
|
||||||
|
Self {
|
||||||
|
font_size: f32::from_bits(metrics.font_size_bits),
|
||||||
|
line_height: f32::from_bits(metrics.line_height_bits),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A 4-byte `OpenType` feature tag identifier
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct FeatureTag([u8; 4]);
|
||||||
|
|
||||||
|
impl FeatureTag {
|
||||||
|
pub const fn new(tag: &[u8; 4]) -> Self {
|
||||||
|
Self(*tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kerning adjusts spacing between specific character pairs
|
||||||
|
pub const KERNING: Self = Self::new(b"kern");
|
||||||
|
/// Standard ligatures (fi, fl, etc.)
|
||||||
|
pub const STANDARD_LIGATURES: Self = Self::new(b"liga");
|
||||||
|
/// Contextual ligatures (context-dependent ligatures)
|
||||||
|
pub const CONTEXTUAL_LIGATURES: Self = Self::new(b"clig");
|
||||||
|
/// Contextual alternates (glyph substitutions based on context)
|
||||||
|
pub const CONTEXTUAL_ALTERNATES: Self = Self::new(b"calt");
|
||||||
|
/// Discretionary ligatures (optional stylistic ligatures)
|
||||||
|
pub const DISCRETIONARY_LIGATURES: Self = Self::new(b"dlig");
|
||||||
|
/// Small caps (lowercase to small capitals)
|
||||||
|
pub const SMALL_CAPS: Self = Self::new(b"smcp");
|
||||||
|
/// All small caps (uppercase and lowercase to small capitals)
|
||||||
|
pub const ALL_SMALL_CAPS: Self = Self::new(b"c2sc");
|
||||||
|
/// Stylistic Set 1 (font-specific alternate glyphs)
|
||||||
|
pub const STYLISTIC_SET_1: Self = Self::new(b"ss01");
|
||||||
|
/// Stylistic Set 2 (font-specific alternate glyphs)
|
||||||
|
pub const STYLISTIC_SET_2: Self = Self::new(b"ss02");
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> &[u8; 4] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct Feature {
|
||||||
|
pub tag: FeatureTag,
|
||||||
|
pub value: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
|
||||||
|
pub struct FontFeatures {
|
||||||
|
pub features: Vec<Feature>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontFeatures {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
features: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, tag: FeatureTag, value: u32) -> &mut Self {
|
||||||
|
self.features.push(Feature { tag, value });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable a feature (set to 1)
|
||||||
|
pub fn enable(&mut self, tag: FeatureTag) -> &mut Self {
|
||||||
|
self.set(tag, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable a feature (set to 0)
|
||||||
|
pub fn disable(&mut self, tag: FeatureTag) -> &mut Self {
|
||||||
|
self.set(tag, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 4-byte `OpenType` variation axis tag.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct VariationTag([u8; 4]);
|
||||||
|
|
||||||
|
impl VariationTag {
|
||||||
|
/// Create a variation axis tag from its four-byte representation.
|
||||||
|
pub const fn new(tag: &[u8; 4]) -> Self {
|
||||||
|
Self(*tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the four-byte representation of this axis tag.
|
||||||
|
pub const fn as_bytes(&self) -> &[u8; 4] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A variation coordinate with stable equality and hashing for shape caches.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct VariationValue(pub f32);
|
||||||
|
|
||||||
|
impl PartialEq for VariationValue {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
if self.0.is_nan() {
|
||||||
|
other.0.is_nan()
|
||||||
|
} else {
|
||||||
|
self.0 == other.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for VariationValue {}
|
||||||
|
|
||||||
|
impl Hash for VariationValue {
|
||||||
|
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||||
|
const CANONICAL_NAN_BITS: u32 = 0x7fc0_0000;
|
||||||
|
|
||||||
|
let bits = if self.0.is_nan() {
|
||||||
|
CANONICAL_NAN_BITS
|
||||||
|
} else {
|
||||||
|
// Add +0.0 to canonicalize -0.0 to +0.0.
|
||||||
|
(self.0 + 0.0).to_bits()
|
||||||
|
};
|
||||||
|
|
||||||
|
bits.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `OpenType` variation axis coordinate.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct FontVariation {
|
||||||
|
pub tag: VariationTag,
|
||||||
|
pub value: VariationValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variation coordinates applied while shaping a text span.
|
||||||
|
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
|
||||||
|
pub struct FontVariations {
|
||||||
|
variations: Vec<FontVariation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontVariations {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
variations: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set an axis coordinate. Setting the same tag again replaces its value,
|
||||||
|
/// matching the last-value-wins behavior of CSS variation settings.
|
||||||
|
pub fn set(&mut self, tag: VariationTag, value: f32) -> &mut Self {
|
||||||
|
match self
|
||||||
|
.variations
|
||||||
|
.binary_search_by_key(&tag, |variation| variation.tag)
|
||||||
|
{
|
||||||
|
Ok(index) => self.variations[index].value = VariationValue(value),
|
||||||
|
Err(index) => self.variations.insert(
|
||||||
|
index,
|
||||||
|
FontVariation {
|
||||||
|
tag,
|
||||||
|
value: VariationValue(value),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.variations.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.variations.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &FontVariation> {
|
||||||
|
self.variations.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper for letter spacing to get around that f32 doesn't implement Eq and Hash
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct LetterSpacing(pub f32);
|
||||||
|
|
||||||
|
impl PartialEq for LetterSpacing {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
if self.0.is_nan() {
|
||||||
|
other.0.is_nan()
|
||||||
|
} else {
|
||||||
|
self.0 == other.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for LetterSpacing {}
|
||||||
|
|
||||||
|
impl Hash for LetterSpacing {
|
||||||
|
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||||
|
const CANONICAL_NAN_BITS: u32 = 0x7fc0_0000;
|
||||||
|
|
||||||
|
let bits = if self.0.is_nan() {
|
||||||
|
CANONICAL_NAN_BITS
|
||||||
|
} else {
|
||||||
|
// Add +0.0 to canonicalize -0.0 to +0.0
|
||||||
|
(self.0 + 0.0).to_bits()
|
||||||
|
};
|
||||||
|
|
||||||
|
bits.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Text attributes
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct Attrs<'a> {
|
||||||
|
//TODO: should this be an option?
|
||||||
|
pub color_opt: Option<Color>,
|
||||||
|
pub family: Family<'a>,
|
||||||
|
/// A specific database face selected by the caller. Font fallback remains
|
||||||
|
/// available when that face does not contain a requested glyph.
|
||||||
|
pub font_id_opt: Option<fontdb::ID>,
|
||||||
|
pub stretch: Stretch,
|
||||||
|
pub style: Style,
|
||||||
|
pub weight: Weight,
|
||||||
|
pub metadata: usize,
|
||||||
|
pub cache_key_flags: CacheKeyFlags,
|
||||||
|
pub metrics_opt: Option<CacheMetrics>,
|
||||||
|
/// Letter spacing (tracking) in EM
|
||||||
|
pub letter_spacing_opt: Option<LetterSpacing>,
|
||||||
|
pub font_features: FontFeatures,
|
||||||
|
pub font_variations: FontVariations,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Attrs<'a> {
|
||||||
|
/// Create a new set of attributes with sane defaults
|
||||||
|
///
|
||||||
|
/// This defaults to a regular Sans-Serif font.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
color_opt: None,
|
||||||
|
family: Family::SansSerif,
|
||||||
|
font_id_opt: None,
|
||||||
|
stretch: Stretch::Normal,
|
||||||
|
style: Style::Normal,
|
||||||
|
weight: Weight::NORMAL,
|
||||||
|
metadata: 0,
|
||||||
|
cache_key_flags: CacheKeyFlags::empty(),
|
||||||
|
metrics_opt: None,
|
||||||
|
letter_spacing_opt: None,
|
||||||
|
font_features: FontFeatures::new(),
|
||||||
|
font_variations: FontVariations::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set [Color]
|
||||||
|
pub fn color(mut self, color: Color) -> Self {
|
||||||
|
self.color_opt = Some(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set [Family]
|
||||||
|
pub fn family(mut self, family: Family<'a>) -> Self {
|
||||||
|
self.family = family;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefer one exact database face before ordinary family matching.
|
||||||
|
pub fn font_id(mut self, font_id: fontdb::ID) -> Self {
|
||||||
|
self.font_id_opt = Some(font_id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set [Stretch]
|
||||||
|
pub fn stretch(mut self, stretch: Stretch) -> Self {
|
||||||
|
self.stretch = stretch;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set [Style]
|
||||||
|
pub fn style(mut self, style: Style) -> Self {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set [Weight]
|
||||||
|
pub fn weight(mut self, weight: Weight) -> Self {
|
||||||
|
self.weight = weight;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set metadata
|
||||||
|
pub fn metadata(mut self, metadata: usize) -> Self {
|
||||||
|
self.metadata = metadata;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set [`CacheKeyFlags`]
|
||||||
|
pub fn cache_key_flags(mut self, cache_key_flags: CacheKeyFlags) -> Self {
|
||||||
|
self.cache_key_flags = cache_key_flags;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set [`Metrics`], overriding values in buffer
|
||||||
|
pub fn metrics(mut self, metrics: Metrics) -> Self {
|
||||||
|
self.metrics_opt = Some(metrics.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set letter spacing (tracking) in EM
|
||||||
|
pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
|
||||||
|
self.letter_spacing_opt = Some(LetterSpacing(letter_spacing));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set [`FontFeatures`]
|
||||||
|
pub fn font_features(mut self, font_features: FontFeatures) -> Self {
|
||||||
|
self.font_features = font_features;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set all variation coordinates used while shaping.
|
||||||
|
pub fn font_variations(mut self, font_variations: FontVariations) -> Self {
|
||||||
|
self.font_variations = font_variations;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set one variation coordinate used while shaping.
|
||||||
|
pub fn font_variation(mut self, tag: VariationTag, value: f32) -> Self {
|
||||||
|
self.font_variations.set(tag, value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if font matches
|
||||||
|
pub fn matches(&self, face: &fontdb::FaceInfo) -> bool {
|
||||||
|
//TODO: smarter way of including emoji
|
||||||
|
face.post_script_name.contains("Emoji")
|
||||||
|
|| (face.style == self.style && face.stretch == self.stretch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this set of attributes can be shaped with another
|
||||||
|
pub fn compatible(&self, other: &Self) -> bool {
|
||||||
|
self.family == other.family
|
||||||
|
&& self.font_id_opt == other.font_id_opt
|
||||||
|
&& self.stretch == other.stretch
|
||||||
|
&& self.style == other.style
|
||||||
|
&& self.weight == other.weight
|
||||||
|
&& self.font_variations == other.font_variations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Font-specific part of [`Attrs`] to be used for matching
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct FontMatchAttrs {
|
||||||
|
family: FamilyOwned,
|
||||||
|
stretch: Stretch,
|
||||||
|
style: Style,
|
||||||
|
weight: Weight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&Attrs<'a>> for FontMatchAttrs {
|
||||||
|
fn from(attrs: &Attrs<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
family: FamilyOwned::new(attrs.family),
|
||||||
|
stretch: attrs.stretch,
|
||||||
|
style: attrs.style,
|
||||||
|
weight: attrs.weight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An owned version of [`Attrs`]
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct AttrsOwned {
|
||||||
|
//TODO: should this be an option?
|
||||||
|
pub color_opt: Option<Color>,
|
||||||
|
pub family_owned: FamilyOwned,
|
||||||
|
pub font_id_opt: Option<fontdb::ID>,
|
||||||
|
pub stretch: Stretch,
|
||||||
|
pub style: Style,
|
||||||
|
pub weight: Weight,
|
||||||
|
pub metadata: usize,
|
||||||
|
pub cache_key_flags: CacheKeyFlags,
|
||||||
|
pub metrics_opt: Option<CacheMetrics>,
|
||||||
|
/// Letter spacing (tracking) in EM
|
||||||
|
pub letter_spacing_opt: Option<LetterSpacing>,
|
||||||
|
pub font_features: FontFeatures,
|
||||||
|
pub font_variations: FontVariations,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrsOwned {
|
||||||
|
pub fn new(attrs: &Attrs) -> Self {
|
||||||
|
Self {
|
||||||
|
color_opt: attrs.color_opt,
|
||||||
|
family_owned: FamilyOwned::new(attrs.family),
|
||||||
|
font_id_opt: attrs.font_id_opt,
|
||||||
|
stretch: attrs.stretch,
|
||||||
|
style: attrs.style,
|
||||||
|
weight: attrs.weight,
|
||||||
|
metadata: attrs.metadata,
|
||||||
|
cache_key_flags: attrs.cache_key_flags,
|
||||||
|
metrics_opt: attrs.metrics_opt,
|
||||||
|
letter_spacing_opt: attrs.letter_spacing_opt,
|
||||||
|
font_features: attrs.font_features.clone(),
|
||||||
|
font_variations: attrs.font_variations.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_attrs(&self) -> Attrs {
|
||||||
|
Attrs {
|
||||||
|
color_opt: self.color_opt,
|
||||||
|
family: self.family_owned.as_family(),
|
||||||
|
font_id_opt: self.font_id_opt,
|
||||||
|
stretch: self.stretch,
|
||||||
|
style: self.style,
|
||||||
|
weight: self.weight,
|
||||||
|
metadata: self.metadata,
|
||||||
|
cache_key_flags: self.cache_key_flags,
|
||||||
|
metrics_opt: self.metrics_opt,
|
||||||
|
letter_spacing_opt: self.letter_spacing_opt,
|
||||||
|
font_features: self.font_features.clone(),
|
||||||
|
font_variations: self.font_variations.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List of text attributes to apply to a line
|
||||||
|
//TODO: have this clean up the spans when changes are made
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct AttrsList {
|
||||||
|
defaults: AttrsOwned,
|
||||||
|
pub(crate) spans: RangeMap<usize, AttrsOwned>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrsList {
|
||||||
|
/// Create a new attributes list with a set of default [Attrs]
|
||||||
|
pub fn new(defaults: &Attrs) -> Self {
|
||||||
|
Self {
|
||||||
|
defaults: AttrsOwned::new(defaults),
|
||||||
|
spans: RangeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default [Attrs]
|
||||||
|
pub fn defaults(&self) -> Attrs {
|
||||||
|
self.defaults.as_attrs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current attribute spans
|
||||||
|
pub fn spans(&self) -> Vec<(&Range<usize>, &AttrsOwned)> {
|
||||||
|
self.spans_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an iterator over the current attribute spans
|
||||||
|
pub fn spans_iter(&self) -> impl Iterator<Item = (&Range<usize>, &AttrsOwned)> + '_ {
|
||||||
|
self.spans.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the current attribute spans
|
||||||
|
pub fn clear_spans(&mut self) {
|
||||||
|
self.spans.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an attribute span, removes any previous matching parts of spans
|
||||||
|
pub fn add_span(&mut self, range: Range<usize>, attrs: &Attrs) {
|
||||||
|
//do not support 1..1 or 2..1 even if by accident.
|
||||||
|
if range.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.spans.insert(range, AttrsOwned::new(attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the attribute span for an index
|
||||||
|
///
|
||||||
|
/// This returns a span that contains the index
|
||||||
|
pub fn get_span(&self, index: usize) -> Attrs {
|
||||||
|
self.spans
|
||||||
|
.get(&index)
|
||||||
|
.map(|v| v.as_attrs())
|
||||||
|
.unwrap_or(self.defaults.as_attrs())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split attributes list at an offset
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
|
pub fn split_off(&mut self, index: usize) -> Self {
|
||||||
|
let mut new = Self::new(&self.defaults.as_attrs());
|
||||||
|
let mut removes = Vec::new();
|
||||||
|
|
||||||
|
//get the keys we need to remove or fix.
|
||||||
|
for span in self.spans.iter() {
|
||||||
|
if span.0.end <= index {
|
||||||
|
continue;
|
||||||
|
} else if span.0.start >= index {
|
||||||
|
removes.push((span.0.clone(), false));
|
||||||
|
} else {
|
||||||
|
removes.push((span.0.clone(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, resize) in removes {
|
||||||
|
let (range, attrs) = self
|
||||||
|
.spans
|
||||||
|
.get_key_value(&key.start)
|
||||||
|
.map(|v| (v.0.clone(), v.1.clone()))
|
||||||
|
.expect("attrs span not found");
|
||||||
|
self.spans.remove(key);
|
||||||
|
|
||||||
|
if resize {
|
||||||
|
new.spans.insert(0..range.end - index, attrs.clone());
|
||||||
|
self.spans.insert(range.start..index, attrs);
|
||||||
|
} else {
|
||||||
|
new.spans
|
||||||
|
.insert(range.start - index..range.end - index, attrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the attributes with new defaults.
|
||||||
|
pub(crate) fn reset(mut self, default: &Attrs) -> Self {
|
||||||
|
self.defaults = AttrsOwned::new(default);
|
||||||
|
self.spans.clear();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Attrs, AttrsOwned, FontVariations, VariationTag};
|
||||||
|
use core::hash::{Hash, Hasher};
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
|
||||||
|
fn hash(value: impl Hash) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
value.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variation_coordinates_round_trip_and_change_shape_identity() {
|
||||||
|
let defaults = Attrs::new();
|
||||||
|
let varied = Attrs::new()
|
||||||
|
.font_variation(VariationTag::new(b"opsz"), 32.0)
|
||||||
|
.font_variation(VariationTag::new(b"wght"), 500.0);
|
||||||
|
|
||||||
|
assert!(!defaults.compatible(&varied));
|
||||||
|
assert_ne!(AttrsOwned::new(&defaults), AttrsOwned::new(&varied));
|
||||||
|
assert_ne!(
|
||||||
|
hash(AttrsOwned::new(&defaults)),
|
||||||
|
hash(AttrsOwned::new(&varied))
|
||||||
|
);
|
||||||
|
assert_eq!(AttrsOwned::new(&varied).as_attrs(), varied);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn setting_an_axis_twice_replaces_its_coordinate() {
|
||||||
|
let mut variations = FontVariations::new();
|
||||||
|
variations
|
||||||
|
.set(VariationTag::new(b"opsz"), 14.0)
|
||||||
|
.set(VariationTag::new(b"opsz"), 32.0);
|
||||||
|
|
||||||
|
assert_eq!(variations.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
variations.iter().next().map(|variation| variation.value.0),
|
||||||
|
Some(32.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variation_order_does_not_change_shape_identity() {
|
||||||
|
let left = Attrs::new()
|
||||||
|
.font_variation(VariationTag::new(b"wght"), 500.0)
|
||||||
|
.font_variation(VariationTag::new(b"opsz"), 32.0);
|
||||||
|
let right = Attrs::new()
|
||||||
|
.font_variation(VariationTag::new(b"opsz"), 32.0)
|
||||||
|
.font_variation(VariationTag::new(b"wght"), 500.0);
|
||||||
|
|
||||||
|
assert_eq!(left, right);
|
||||||
|
assert!(left.compatible(&right));
|
||||||
|
assert_eq!(hash(AttrsOwned::new(&left)), hash(AttrsOwned::new(&right)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+40
@@ -0,0 +1,40 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use unicode_bidi::{bidi_class, BidiClass, BidiInfo, ParagraphInfo};
|
||||||
|
|
||||||
|
/// An iterator over the paragraphs in the input text.
|
||||||
|
/// It is equivalent to [`core::str::Lines`] but follows `unicode-bidi` behaviour.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BidiParagraphs<'text> {
|
||||||
|
text: &'text str,
|
||||||
|
info: alloc::vec::IntoIter<ParagraphInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'text> BidiParagraphs<'text> {
|
||||||
|
/// Create an iterator to split the input text into paragraphs
|
||||||
|
/// in accordance with `unicode-bidi` behaviour.
|
||||||
|
pub fn new(text: &'text str) -> Self {
|
||||||
|
let info = BidiInfo::new(text, None);
|
||||||
|
let info = info.paragraphs.into_iter();
|
||||||
|
Self { text, info }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'text> Iterator for BidiParagraphs<'text> {
|
||||||
|
type Item = &'text str;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let para = self.info.next()?;
|
||||||
|
let paragraph = &self.text[para.range];
|
||||||
|
// `para.range` includes the newline that splits the line, so remove it if present
|
||||||
|
let mut char_indices = paragraph.char_indices();
|
||||||
|
if let Some(i) = char_indices.next_back().and_then(|(i, c)| {
|
||||||
|
// `BidiClass::B` is a Paragraph_Separator (various newline characters)
|
||||||
|
(bidi_class(c) == BidiClass::B).then_some(i)
|
||||||
|
}) {
|
||||||
|
Some(¶graph[0..i])
|
||||||
|
} else {
|
||||||
|
Some(paragraph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1485
File diff suppressed because it is too large.
Load diff
+310
@@ -0,0 +1,310 @@
|
|||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
use core::mem;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Align, Attrs, AttrsList, Cached, FontSystem, LayoutLine, LineEnding, ShapeLine, Shaping, Wrap,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A line (or paragraph) of text that is shaped and laid out
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BufferLine {
|
||||||
|
text: String,
|
||||||
|
ending: LineEnding,
|
||||||
|
attrs_list: AttrsList,
|
||||||
|
align: Option<Align>,
|
||||||
|
shape_opt: Cached<ShapeLine>,
|
||||||
|
layout_opt: Cached<Vec<LayoutLine>>,
|
||||||
|
shaping: Shaping,
|
||||||
|
metadata: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BufferLine {
|
||||||
|
/// Create a new line with the given text and attributes list
|
||||||
|
/// Cached shaping and layout can be done using the [`Self::shape`] and
|
||||||
|
/// [`Self::layout`] functions
|
||||||
|
pub fn new<T: Into<String>>(
|
||||||
|
text: T,
|
||||||
|
ending: LineEnding,
|
||||||
|
attrs_list: AttrsList,
|
||||||
|
shaping: Shaping,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
ending,
|
||||||
|
attrs_list,
|
||||||
|
align: None,
|
||||||
|
shape_opt: Cached::Empty,
|
||||||
|
layout_opt: Cached::Empty,
|
||||||
|
shaping,
|
||||||
|
metadata: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the current line with new internal values.
|
||||||
|
///
|
||||||
|
/// Avoids deallocating internal caches so they can be reused.
|
||||||
|
pub fn reset_new<T: Into<String>>(
|
||||||
|
&mut self,
|
||||||
|
text: T,
|
||||||
|
ending: LineEnding,
|
||||||
|
attrs_list: AttrsList,
|
||||||
|
shaping: Shaping,
|
||||||
|
) {
|
||||||
|
self.text = text.into();
|
||||||
|
self.ending = ending;
|
||||||
|
self.attrs_list = attrs_list;
|
||||||
|
self.align = None;
|
||||||
|
self.shape_opt.set_unused();
|
||||||
|
self.layout_opt.set_unused();
|
||||||
|
self.shaping = shaping;
|
||||||
|
self.metadata = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current text
|
||||||
|
pub fn text(&self) -> &str {
|
||||||
|
&self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set text and attributes list
|
||||||
|
///
|
||||||
|
/// Will reset shape and layout if it differs from current text and attributes list.
|
||||||
|
/// Returns true if the line was reset
|
||||||
|
pub fn set_text<T: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
text: T,
|
||||||
|
ending: LineEnding,
|
||||||
|
attrs_list: AttrsList,
|
||||||
|
) -> bool {
|
||||||
|
let text = text.as_ref();
|
||||||
|
if text != self.text || ending != self.ending || attrs_list != self.attrs_list {
|
||||||
|
self.text.clear();
|
||||||
|
self.text.push_str(text);
|
||||||
|
self.ending = ending;
|
||||||
|
self.attrs_list = attrs_list;
|
||||||
|
self.reset();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume this line, returning only its text contents as a String.
|
||||||
|
pub fn into_text(self) -> String {
|
||||||
|
self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get line ending
|
||||||
|
pub fn ending(&self) -> LineEnding {
|
||||||
|
self.ending
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set line ending
|
||||||
|
///
|
||||||
|
/// Will reset shape and layout if it differs from current line ending.
|
||||||
|
/// Returns true if the line was reset
|
||||||
|
pub fn set_ending(&mut self, ending: LineEnding) -> bool {
|
||||||
|
if ending != self.ending {
|
||||||
|
self.ending = ending;
|
||||||
|
self.reset_shaping();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get attributes list
|
||||||
|
pub fn attrs_list(&self) -> &AttrsList {
|
||||||
|
&self.attrs_list
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set attributes list
|
||||||
|
///
|
||||||
|
/// Will reset shape and layout if it differs from current attributes list.
|
||||||
|
/// Returns true if the line was reset
|
||||||
|
pub fn set_attrs_list(&mut self, attrs_list: AttrsList) -> bool {
|
||||||
|
if attrs_list != self.attrs_list {
|
||||||
|
self.attrs_list = attrs_list;
|
||||||
|
self.reset_shaping();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the Text alignment
|
||||||
|
pub fn align(&self) -> Option<Align> {
|
||||||
|
self.align
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the text alignment
|
||||||
|
///
|
||||||
|
/// Will reset layout if it differs from current alignment.
|
||||||
|
/// Setting to None will use `Align::Right` for RTL lines, and `Align::Left` for LTR lines.
|
||||||
|
/// Returns true if the line was reset
|
||||||
|
pub fn set_align(&mut self, align: Option<Align>) -> bool {
|
||||||
|
if align != self.align {
|
||||||
|
self.align = align;
|
||||||
|
self.reset_layout();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append line at end of this line
|
||||||
|
///
|
||||||
|
/// The wrap setting of the appended line will be lost
|
||||||
|
pub fn append(&mut self, other: Self) {
|
||||||
|
let len = self.text.len();
|
||||||
|
self.text.push_str(other.text());
|
||||||
|
|
||||||
|
if other.attrs_list.defaults() != self.attrs_list.defaults() {
|
||||||
|
// If default formatting does not match, make a new span for it
|
||||||
|
self.attrs_list
|
||||||
|
.add_span(len..len + other.text().len(), &other.attrs_list.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (other_range, attrs) in other.attrs_list.spans_iter() {
|
||||||
|
// Add previous attrs spans
|
||||||
|
let range = other_range.start + len..other_range.end + len;
|
||||||
|
self.attrs_list.add_span(range, &attrs.as_attrs());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split off new line at index
|
||||||
|
pub fn split_off(&mut self, index: usize) -> Self {
|
||||||
|
let text = self.text.split_off(index);
|
||||||
|
let attrs_list = self.attrs_list.split_off(index);
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
let mut new = Self::new(text, self.ending, attrs_list, self.shaping);
|
||||||
|
new.align = self.align;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset shaping, layout, and metadata caches
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.metadata = None;
|
||||||
|
self.reset_shaping();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset shaping and layout caches
|
||||||
|
pub fn reset_shaping(&mut self) {
|
||||||
|
self.shape_opt.set_unused();
|
||||||
|
self.reset_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset only layout cache
|
||||||
|
pub fn reset_layout(&mut self) {
|
||||||
|
self.layout_opt.set_unused();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shape line, will cache results
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
|
pub fn shape(&mut self, font_system: &mut FontSystem, tab_width: u16) -> &ShapeLine {
|
||||||
|
if self.shape_opt.is_unused() {
|
||||||
|
let mut line = self
|
||||||
|
.shape_opt
|
||||||
|
.take_unused()
|
||||||
|
.unwrap_or_else(ShapeLine::empty);
|
||||||
|
line.build(
|
||||||
|
font_system,
|
||||||
|
&self.text,
|
||||||
|
&self.attrs_list,
|
||||||
|
self.shaping,
|
||||||
|
tab_width,
|
||||||
|
);
|
||||||
|
self.shape_opt.set_used(line);
|
||||||
|
self.layout_opt.set_unused();
|
||||||
|
}
|
||||||
|
self.shape_opt.get().expect("shape not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get line shaping cache
|
||||||
|
pub fn shape_opt(&self) -> Option<&ShapeLine> {
|
||||||
|
self.shape_opt.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout line, will cache results
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
|
pub fn layout(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
font_size: f32,
|
||||||
|
width_opt: Option<f32>,
|
||||||
|
wrap: Wrap,
|
||||||
|
match_mono_width: Option<f32>,
|
||||||
|
tab_width: u16,
|
||||||
|
) -> &[LayoutLine] {
|
||||||
|
if self.layout_opt.is_unused() {
|
||||||
|
let align = self.align;
|
||||||
|
let mut layout = self
|
||||||
|
.layout_opt
|
||||||
|
.take_unused()
|
||||||
|
.unwrap_or_else(|| Vec::with_capacity(1));
|
||||||
|
let shape = self.shape(font_system, tab_width);
|
||||||
|
shape.layout_to_buffer(
|
||||||
|
&mut font_system.shape_buffer,
|
||||||
|
font_size,
|
||||||
|
width_opt,
|
||||||
|
wrap,
|
||||||
|
align,
|
||||||
|
&mut layout,
|
||||||
|
match_mono_width,
|
||||||
|
);
|
||||||
|
self.layout_opt.set_used(layout);
|
||||||
|
}
|
||||||
|
self.layout_opt.get().expect("layout not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get line layout cache
|
||||||
|
pub fn layout_opt(&self) -> Option<&Vec<LayoutLine>> {
|
||||||
|
self.layout_opt.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get line metadata. This will be None if [`BufferLine::set_metadata`] has not been called
|
||||||
|
/// after the last reset of shaping and layout caches
|
||||||
|
pub fn metadata(&self) -> Option<usize> {
|
||||||
|
self.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set line metadata. This is stored until the next line reset
|
||||||
|
pub fn set_metadata(&mut self, metadata: usize) {
|
||||||
|
self.metadata = Some(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes an empty buffer line.
|
||||||
|
///
|
||||||
|
/// The buffer line is in an invalid state after this is called. See [`Self::reset_new`].
|
||||||
|
pub(crate) fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
text: String::default(),
|
||||||
|
ending: LineEnding::default(),
|
||||||
|
attrs_list: AttrsList::new(&Attrs::new()),
|
||||||
|
align: None,
|
||||||
|
shape_opt: Cached::Empty,
|
||||||
|
layout_opt: Cached::Empty,
|
||||||
|
shaping: Shaping::Advanced,
|
||||||
|
metadata: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reclaim attributes list memory that isn't needed any longer.
|
||||||
|
///
|
||||||
|
/// The buffer line is in an invalid state after this is called. See [`Self::reset_new`].
|
||||||
|
pub(crate) fn reclaim_attrs(&mut self) -> AttrsList {
|
||||||
|
mem::replace(&mut self.attrs_list, AttrsList::new(&Attrs::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reclaim text memory that isn't needed any longer.
|
||||||
|
///
|
||||||
|
/// The buffer line is in an invalid state after this is called. See [`Self::reset_new`].
|
||||||
|
pub(crate) fn reclaim_text(&mut self) -> String {
|
||||||
|
let mut text = mem::take(&mut self.text);
|
||||||
|
text.clear();
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+81
@@ -0,0 +1,81 @@
|
|||||||
|
use core::fmt::Debug;
|
||||||
|
use core::mem;
|
||||||
|
|
||||||
|
/// Helper for caching a value when the value is optionally present in the 'unused' state.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Cached<T: Clone + Debug> {
|
||||||
|
Empty,
|
||||||
|
Unused(T),
|
||||||
|
Used(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Debug> Cached<T> {
|
||||||
|
/// Gets the value if in state `Self::Used`.
|
||||||
|
pub fn get(&self) -> Option<&T> {
|
||||||
|
match self {
|
||||||
|
Self::Empty | Self::Unused(_) => None,
|
||||||
|
Self::Used(t) => Some(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the value mutably if in state `Self::Used`.
|
||||||
|
pub fn get_mut(&mut self) -> Option<&mut T> {
|
||||||
|
match self {
|
||||||
|
Self::Empty | Self::Unused(_) => None,
|
||||||
|
Self::Used(t) => Some(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the value is empty or unused.
|
||||||
|
pub fn is_unused(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Empty | Self::Unused(_) => true,
|
||||||
|
Self::Used(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the value is used (i.e. cached for access).
|
||||||
|
pub fn is_used(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Empty | Self::Unused(_) => false,
|
||||||
|
Self::Used(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the buffered value if in state `Self::Unused`.
|
||||||
|
pub fn take_unused(&mut self) -> Option<T> {
|
||||||
|
if matches!(*self, Self::Unused(_)) {
|
||||||
|
let Self::Unused(val) = mem::replace(self, Self::Empty) else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
Some(val)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the cached value if in state `Self::Used`.
|
||||||
|
pub fn take_used(&mut self) -> Option<T> {
|
||||||
|
if matches!(*self, Self::Used(_)) {
|
||||||
|
let Self::Used(val) = mem::replace(self, Self::Empty) else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
Some(val)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the value from `Self::Used` to `Self::Unused`.
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
|
pub fn set_unused(&mut self) {
|
||||||
|
if matches!(*self, Self::Used(_)) {
|
||||||
|
*self = Self::Unused(self.take_used().expect("cached value should be used"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the value to `Self::Used`.
|
||||||
|
pub fn set_used(&mut self, val: T) {
|
||||||
|
*self = Self::Used(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+156
@@ -0,0 +1,156 @@
|
|||||||
|
/// Current cursor location
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub struct Cursor {
|
||||||
|
/// Index of [`BufferLine`] in [`Buffer::lines`]
|
||||||
|
pub line: usize,
|
||||||
|
/// First-byte-index of glyph at cursor (will insert behind this glyph)
|
||||||
|
pub index: usize,
|
||||||
|
/// Whether to associate the cursor with the run before it or the run after it if placed at the
|
||||||
|
/// boundary between two runs
|
||||||
|
pub affinity: Affinity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cursor {
|
||||||
|
/// Create a new cursor
|
||||||
|
pub const fn new(line: usize, index: usize) -> Self {
|
||||||
|
Self::new_with_affinity(line, index, Affinity::Before)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new cursor, specifying the affinity
|
||||||
|
pub const fn new_with_affinity(line: usize, index: usize, affinity: Affinity) -> Self {
|
||||||
|
Self {
|
||||||
|
line,
|
||||||
|
index,
|
||||||
|
affinity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to associate cursors placed at a boundary between runs with the run before or after it.
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub enum Affinity {
|
||||||
|
#[default]
|
||||||
|
Before,
|
||||||
|
After,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Affinity {
|
||||||
|
pub fn before(&self) -> bool {
|
||||||
|
*self == Self::Before
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn after(&self) -> bool {
|
||||||
|
*self == Self::After
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_before(before: bool) -> Self {
|
||||||
|
if before {
|
||||||
|
Self::Before
|
||||||
|
} else {
|
||||||
|
Self::After
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_after(after: bool) -> Self {
|
||||||
|
if after {
|
||||||
|
Self::After
|
||||||
|
} else {
|
||||||
|
Self::Before
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The position of a cursor within a [`Buffer`].
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub struct LayoutCursor {
|
||||||
|
/// Index of [`BufferLine`] in [`Buffer::lines`]
|
||||||
|
pub line: usize,
|
||||||
|
/// Index of [`LayoutLine`] in [`BufferLine::layout`]
|
||||||
|
pub layout: usize,
|
||||||
|
/// Index of [`LayoutGlyph`] in [`LayoutLine::glyphs`]
|
||||||
|
pub glyph: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutCursor {
|
||||||
|
/// Create a new [`LayoutCursor`]
|
||||||
|
pub const fn new(line: usize, layout: usize, glyph: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
line,
|
||||||
|
layout,
|
||||||
|
glyph,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A motion to perform on a [`Cursor`]
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Motion {
|
||||||
|
/// Apply specific [`LayoutCursor`]
|
||||||
|
LayoutCursor(LayoutCursor),
|
||||||
|
/// Move cursor to previous character ([`Self::Left`] in LTR, [`Self::Right`] in RTL)
|
||||||
|
Previous,
|
||||||
|
/// Move cursor to next character ([`Self::Right`] in LTR, [`Self::Left`] in RTL)
|
||||||
|
Next,
|
||||||
|
/// Move cursor left
|
||||||
|
Left,
|
||||||
|
/// Move cursor right
|
||||||
|
Right,
|
||||||
|
/// Move cursor up
|
||||||
|
Up,
|
||||||
|
/// Move cursor down
|
||||||
|
Down,
|
||||||
|
/// Move cursor to start of line
|
||||||
|
Home,
|
||||||
|
/// Move cursor to start of line, skipping whitespace
|
||||||
|
SoftHome,
|
||||||
|
/// Move cursor to end of line
|
||||||
|
End,
|
||||||
|
/// Move cursor to start of paragraph
|
||||||
|
ParagraphStart,
|
||||||
|
/// Move cursor to end of paragraph
|
||||||
|
ParagraphEnd,
|
||||||
|
/// Move cursor up one page
|
||||||
|
PageUp,
|
||||||
|
/// Move cursor down one page
|
||||||
|
PageDown,
|
||||||
|
/// Move cursor up or down by a number of pixels
|
||||||
|
Vertical(i32),
|
||||||
|
/// Move cursor to previous word boundary
|
||||||
|
PreviousWord,
|
||||||
|
/// Move cursor to next word boundary
|
||||||
|
NextWord,
|
||||||
|
/// Move cursor to next word boundary to the left
|
||||||
|
LeftWord,
|
||||||
|
/// Move cursor to next word boundary to the right
|
||||||
|
RightWord,
|
||||||
|
/// Move cursor to the start of the document
|
||||||
|
BufferStart,
|
||||||
|
/// Move cursor to the end of the document
|
||||||
|
BufferEnd,
|
||||||
|
/// Move cursor to specific line
|
||||||
|
GotoLine(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll position in [`Buffer`]
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
|
||||||
|
pub struct Scroll {
|
||||||
|
/// Index of [`BufferLine`] in [`Buffer::lines`]. This will be adjusted as needed if layout is
|
||||||
|
/// out of bounds
|
||||||
|
pub line: usize,
|
||||||
|
/// Pixel offset from the start of the [`BufferLine`]. This will be adjusted as needed
|
||||||
|
/// if it is negative or exceeds the height of the [`BufferLine::layout`] lines.
|
||||||
|
pub vertical: f32,
|
||||||
|
/// The horizontal position of scroll in fractional pixels
|
||||||
|
pub horizontal: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scroll {
|
||||||
|
/// Create a new scroll
|
||||||
|
pub const fn new(line: usize, vertical: f32, horizontal: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
line,
|
||||||
|
vertical,
|
||||||
|
horizontal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+922
@@ -0,0 +1,922 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{
|
||||||
|
string::{String, ToString},
|
||||||
|
vec::Vec,
|
||||||
|
};
|
||||||
|
use core::{cmp, iter::once};
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
use crate::Color;
|
||||||
|
use crate::{
|
||||||
|
Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem,
|
||||||
|
Cursor, Edit, FontSystem, LayoutRun, Selection, Shaping,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A wrapper of [`Buffer`] for easy editing
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Editor<'buffer> {
|
||||||
|
buffer_ref: BufferRef<'buffer>,
|
||||||
|
cursor: Cursor,
|
||||||
|
cursor_x_opt: Option<i32>,
|
||||||
|
selection: Selection,
|
||||||
|
cursor_moved: bool,
|
||||||
|
auto_indent: bool,
|
||||||
|
change: Option<Change>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_glyph_opt(cursor: &Cursor, run: &LayoutRun) -> Option<(usize, f32)> {
|
||||||
|
if cursor.line == run.line_i {
|
||||||
|
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
|
||||||
|
if cursor.index == glyph.start {
|
||||||
|
return Some((glyph_i, 0.0));
|
||||||
|
} else if cursor.index > glyph.start && cursor.index < glyph.end {
|
||||||
|
// Guess x offset based on characters
|
||||||
|
let mut before = 0;
|
||||||
|
let mut total = 0;
|
||||||
|
|
||||||
|
let cluster = &run.text[glyph.start..glyph.end];
|
||||||
|
for (i, _) in cluster.grapheme_indices(true) {
|
||||||
|
if glyph.start + i < cursor.index {
|
||||||
|
before += 1;
|
||||||
|
}
|
||||||
|
total += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = glyph.w * (before as f32) / (total as f32);
|
||||||
|
return Some((glyph_i, offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match run.glyphs.last() {
|
||||||
|
Some(glyph) => {
|
||||||
|
if cursor.index == glyph.end {
|
||||||
|
return Some((run.glyphs.len(), 0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Some((0, 0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_position(cursor: &Cursor, run: &LayoutRun) -> Option<(i32, i32)> {
|
||||||
|
let (cursor_glyph, cursor_glyph_offset) = cursor_glyph_opt(cursor, run)?;
|
||||||
|
let x = match run.glyphs.get(cursor_glyph) {
|
||||||
|
Some(glyph) => {
|
||||||
|
// Start of detected glyph
|
||||||
|
if glyph.level.is_rtl() {
|
||||||
|
(glyph.x + glyph.w - cursor_glyph_offset) as i32
|
||||||
|
} else {
|
||||||
|
(glyph.x + cursor_glyph_offset) as i32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => match run.glyphs.last() {
|
||||||
|
Some(glyph) => {
|
||||||
|
// End of last glyph
|
||||||
|
if glyph.level.is_rtl() {
|
||||||
|
glyph.x as i32
|
||||||
|
} else {
|
||||||
|
(glyph.x + glyph.w) as i32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Start of empty line
|
||||||
|
0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((x, run.line_top as i32))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'buffer> Editor<'buffer> {
|
||||||
|
/// Create a new [`Editor`] with the provided [`Buffer`]
|
||||||
|
pub fn new(buffer: impl Into<BufferRef<'buffer>>) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer_ref: buffer.into(),
|
||||||
|
cursor: Cursor::default(),
|
||||||
|
cursor_x_opt: None,
|
||||||
|
selection: Selection::None,
|
||||||
|
cursor_moved: false,
|
||||||
|
auto_indent: false,
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the editor
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn draw<F>(
|
||||||
|
&self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
cache: &mut crate::SwashCache,
|
||||||
|
text_color: Color,
|
||||||
|
cursor_color: Color,
|
||||||
|
selection_color: Color,
|
||||||
|
selected_text_color: Color,
|
||||||
|
mut f: F,
|
||||||
|
) where
|
||||||
|
F: FnMut(i32, i32, u32, u32, Color),
|
||||||
|
{
|
||||||
|
let selection_bounds = self.selection_bounds();
|
||||||
|
self.with_buffer(|buffer| {
|
||||||
|
for run in buffer.layout_runs() {
|
||||||
|
let line_i = run.line_i;
|
||||||
|
let line_y = run.line_y;
|
||||||
|
let line_top = run.line_top;
|
||||||
|
let line_height = run.line_height;
|
||||||
|
|
||||||
|
// Highlight selection
|
||||||
|
if let Some((start, end)) = selection_bounds {
|
||||||
|
if line_i >= start.line && line_i <= end.line {
|
||||||
|
let mut range_opt = None;
|
||||||
|
for glyph in run.glyphs.iter() {
|
||||||
|
// Guess x offset based on characters
|
||||||
|
let cluster = &run.text[glyph.start..glyph.end];
|
||||||
|
let total = cluster.grapheme_indices(true).count();
|
||||||
|
let mut c_x = glyph.x;
|
||||||
|
let c_w = glyph.w / total as f32;
|
||||||
|
for (i, c) in cluster.grapheme_indices(true) {
|
||||||
|
let c_start = glyph.start + i;
|
||||||
|
let c_end = glyph.start + i + c.len();
|
||||||
|
if (start.line != line_i || c_end > start.index)
|
||||||
|
&& (end.line != line_i || c_start < end.index)
|
||||||
|
{
|
||||||
|
range_opt = match range_opt.take() {
|
||||||
|
Some((min, max)) => Some((
|
||||||
|
cmp::min(min, c_x as i32),
|
||||||
|
cmp::max(max, (c_x + c_w) as i32),
|
||||||
|
)),
|
||||||
|
None => Some((c_x as i32, (c_x + c_w) as i32)),
|
||||||
|
};
|
||||||
|
} else if let Some((min, max)) = range_opt.take() {
|
||||||
|
f(
|
||||||
|
min,
|
||||||
|
line_top as i32,
|
||||||
|
cmp::max(0, max - min) as u32,
|
||||||
|
line_height as u32,
|
||||||
|
selection_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
c_x += c_w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if run.glyphs.is_empty() && end.line > line_i {
|
||||||
|
// Highlight all of internal empty lines
|
||||||
|
range_opt = Some((0, buffer.size().0.unwrap_or(0.0) as i32));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((mut min, mut max)) = range_opt.take() {
|
||||||
|
if end.line > line_i {
|
||||||
|
// Draw to end of line
|
||||||
|
if run.rtl {
|
||||||
|
min = 0;
|
||||||
|
} else {
|
||||||
|
max = buffer.size().0.unwrap_or(0.0) as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f(
|
||||||
|
min,
|
||||||
|
line_top as i32,
|
||||||
|
cmp::max(0, max - min) as u32,
|
||||||
|
line_height as u32,
|
||||||
|
selection_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw cursor
|
||||||
|
if let Some((x, y)) = cursor_position(&self.cursor, &run) {
|
||||||
|
f(x, y, 1, line_height as u32, cursor_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
for glyph in run.glyphs.iter() {
|
||||||
|
let physical_glyph = glyph.physical((0., 0.), 1.0);
|
||||||
|
|
||||||
|
let mut glyph_color = match glyph.color_opt {
|
||||||
|
Some(some) => some,
|
||||||
|
None => text_color,
|
||||||
|
};
|
||||||
|
if text_color != selected_text_color {
|
||||||
|
if let Some((start, end)) = selection_bounds {
|
||||||
|
if line_i >= start.line
|
||||||
|
&& line_i <= end.line
|
||||||
|
&& (start.line != line_i || glyph.end > start.index)
|
||||||
|
&& (end.line != line_i || glyph.start < end.index)
|
||||||
|
{
|
||||||
|
glyph_color = selected_text_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.with_pixels(
|
||||||
|
font_system,
|
||||||
|
physical_glyph.cache_key,
|
||||||
|
glyph_color,
|
||||||
|
|x, y, color| {
|
||||||
|
f(
|
||||||
|
physical_glyph.x + x,
|
||||||
|
line_y as i32 + physical_glyph.y + y,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'buffer> Edit<'buffer> for Editor<'buffer> {
|
||||||
|
fn buffer_ref(&self) -> &BufferRef<'buffer> {
|
||||||
|
&self.buffer_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer> {
|
||||||
|
&mut self.buffer_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor(&self) -> Cursor {
|
||||||
|
self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cursor(&mut self, cursor: Cursor) {
|
||||||
|
if self.cursor != cursor {
|
||||||
|
self.cursor = cursor;
|
||||||
|
self.cursor_moved = true;
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection(&self) -> Selection {
|
||||||
|
self.selection
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selection(&mut self, selection: Selection) {
|
||||||
|
if self.selection != selection {
|
||||||
|
self.selection = selection;
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_indent(&self) -> bool {
|
||||||
|
self.auto_indent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_auto_indent(&mut self, auto_indent: bool) {
|
||||||
|
self.auto_indent = auto_indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_width(&self) -> u16 {
|
||||||
|
self.with_buffer(|buffer| buffer.tab_width())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) {
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_tab_width(font_system, tab_width));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) {
|
||||||
|
if self.cursor_moved {
|
||||||
|
let cursor = self.cursor;
|
||||||
|
self.with_buffer_mut(|buffer| buffer.shape_until_cursor(font_system, cursor, prune));
|
||||||
|
self.cursor_moved = false;
|
||||||
|
} else {
|
||||||
|
self.with_buffer_mut(|buffer| buffer.shape_until_scroll(font_system, prune));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_range(&mut self, start: Cursor, end: Cursor) {
|
||||||
|
let change_item = self.with_buffer_mut(|buffer| {
|
||||||
|
// Collect removed data for change tracking
|
||||||
|
let mut change_lines = Vec::new();
|
||||||
|
|
||||||
|
// Delete the selection from the last line
|
||||||
|
let end_line_opt = if end.line > start.line {
|
||||||
|
// Get part of line after selection
|
||||||
|
let after = buffer.lines[end.line].split_off(end.index);
|
||||||
|
|
||||||
|
// Remove end line
|
||||||
|
let removed = buffer.lines.remove(end.line);
|
||||||
|
change_lines.insert(0, removed.text().to_string());
|
||||||
|
|
||||||
|
Some(after)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete interior lines (in reverse for safety)
|
||||||
|
for line_i in (start.line + 1..end.line).rev() {
|
||||||
|
let removed = buffer.lines.remove(line_i);
|
||||||
|
change_lines.insert(0, removed.text().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the selection from the first line
|
||||||
|
{
|
||||||
|
// Get part after selection if start line is also end line
|
||||||
|
let after_opt = if start.line == end.line {
|
||||||
|
Some(buffer.lines[start.line].split_off(end.index))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete selected part of line
|
||||||
|
let removed = buffer.lines[start.line].split_off(start.index);
|
||||||
|
change_lines.insert(0, removed.text().to_string());
|
||||||
|
|
||||||
|
// Re-add part of line after selection
|
||||||
|
if let Some(after) = after_opt {
|
||||||
|
buffer.lines[start.line].append(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-add valid parts of end line
|
||||||
|
if let Some(end_line) = end_line_opt {
|
||||||
|
buffer.lines[start.line].append(end_line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangeItem {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
text: change_lines.join("\n"),
|
||||||
|
insert: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref mut change) = self.change {
|
||||||
|
change.items.push(change_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_at(
|
||||||
|
&mut self,
|
||||||
|
mut cursor: Cursor,
|
||||||
|
data: &str,
|
||||||
|
attrs_list: Option<AttrsList>,
|
||||||
|
) -> Cursor {
|
||||||
|
let mut remaining_split_len = data.len();
|
||||||
|
if remaining_split_len == 0 {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
let change_item = self.with_buffer_mut(|buffer| {
|
||||||
|
// Save cursor for change tracking
|
||||||
|
let start = cursor;
|
||||||
|
|
||||||
|
// Ensure there are enough lines in the buffer to handle this cursor
|
||||||
|
while cursor.line >= buffer.lines.len() {
|
||||||
|
let ending = buffer
|
||||||
|
.lines
|
||||||
|
.last()
|
||||||
|
.map(|line| line.ending())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let line = BufferLine::new(
|
||||||
|
String::new(),
|
||||||
|
ending,
|
||||||
|
AttrsList::new(&attrs_list.as_ref().map_or_else(
|
||||||
|
|| {
|
||||||
|
buffer
|
||||||
|
.lines
|
||||||
|
.last()
|
||||||
|
.map_or(Attrs::new(), |line| line.attrs_list().defaults())
|
||||||
|
},
|
||||||
|
|x| x.defaults(),
|
||||||
|
)),
|
||||||
|
Shaping::Advanced,
|
||||||
|
);
|
||||||
|
buffer.lines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
let line: &mut BufferLine = &mut buffer.lines[cursor.line];
|
||||||
|
let insert_line = cursor.line + 1;
|
||||||
|
let ending = line.ending();
|
||||||
|
|
||||||
|
// Collect text after insertion as a line
|
||||||
|
let after: BufferLine = line.split_off(cursor.index);
|
||||||
|
let after_len = after.text().len();
|
||||||
|
|
||||||
|
// Collect attributes
|
||||||
|
let mut final_attrs = attrs_list.unwrap_or_else(|| {
|
||||||
|
AttrsList::new(&line.attrs_list().get_span(cursor.index.saturating_sub(1)))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append the inserted text, line by line
|
||||||
|
// we want to see a blank entry if the string ends with a newline
|
||||||
|
//TODO: adjust this to get line ending from data?
|
||||||
|
let addendum = once("").filter(|_| data.ends_with('\n'));
|
||||||
|
let mut lines_iter = data.split_inclusive('\n').chain(addendum);
|
||||||
|
if let Some(data_line) = lines_iter.next() {
|
||||||
|
let mut these_attrs = final_attrs.split_off(data_line.len());
|
||||||
|
remaining_split_len -= data_line.len();
|
||||||
|
core::mem::swap(&mut these_attrs, &mut final_attrs);
|
||||||
|
line.append(BufferLine::new(
|
||||||
|
data_line
|
||||||
|
.strip_suffix(char::is_control)
|
||||||
|
.unwrap_or(data_line),
|
||||||
|
ending,
|
||||||
|
these_attrs,
|
||||||
|
Shaping::Advanced,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
panic!("str::lines() did not yield any elements");
|
||||||
|
}
|
||||||
|
if let Some(data_line) = lines_iter.next_back() {
|
||||||
|
remaining_split_len -= data_line.len();
|
||||||
|
let mut tmp = BufferLine::new(
|
||||||
|
data_line
|
||||||
|
.strip_suffix(char::is_control)
|
||||||
|
.unwrap_or(data_line),
|
||||||
|
ending,
|
||||||
|
final_attrs.split_off(remaining_split_len),
|
||||||
|
Shaping::Advanced,
|
||||||
|
);
|
||||||
|
tmp.append(after);
|
||||||
|
buffer.lines.insert(insert_line, tmp);
|
||||||
|
cursor.line += 1;
|
||||||
|
} else {
|
||||||
|
line.append(after);
|
||||||
|
}
|
||||||
|
for data_line in lines_iter.rev() {
|
||||||
|
remaining_split_len -= data_line.len();
|
||||||
|
let tmp = BufferLine::new(
|
||||||
|
data_line
|
||||||
|
.strip_suffix(char::is_control)
|
||||||
|
.unwrap_or(data_line),
|
||||||
|
ending,
|
||||||
|
final_attrs.split_off(remaining_split_len),
|
||||||
|
Shaping::Advanced,
|
||||||
|
);
|
||||||
|
buffer.lines.insert(insert_line, tmp);
|
||||||
|
cursor.line += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(remaining_split_len, 0);
|
||||||
|
|
||||||
|
// Append the text after insertion
|
||||||
|
cursor.index = buffer.lines[cursor.line].text().len() - after_len;
|
||||||
|
|
||||||
|
ChangeItem {
|
||||||
|
start,
|
||||||
|
end: cursor,
|
||||||
|
text: data.to_string(),
|
||||||
|
insert: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref mut change) = self.change {
|
||||||
|
change.items.push(change_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_selection(&self) -> Option<String> {
|
||||||
|
let (start, end) = self.selection_bounds()?;
|
||||||
|
self.with_buffer(|buffer| {
|
||||||
|
let mut selection = String::new();
|
||||||
|
// Take the selection from the first line
|
||||||
|
{
|
||||||
|
// Add selected part of line to string
|
||||||
|
if start.line == end.line {
|
||||||
|
selection.push_str(&buffer.lines[start.line].text()[start.index..end.index]);
|
||||||
|
} else {
|
||||||
|
selection.push_str(&buffer.lines[start.line].text()[start.index..]);
|
||||||
|
selection.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the selection from all interior lines (if they exist)
|
||||||
|
for line_i in start.line + 1..end.line {
|
||||||
|
selection.push_str(buffer.lines[line_i].text());
|
||||||
|
selection.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the selection from the last line
|
||||||
|
if end.line > start.line {
|
||||||
|
// Add selected part of line to string
|
||||||
|
selection.push_str(&buffer.lines[end.line].text()[..end.index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(selection)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_selection(&mut self) -> bool {
|
||||||
|
let (start, end) = match self.selection_bounds() {
|
||||||
|
Some(some) => some,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset cursor to start of selection
|
||||||
|
self.cursor = start;
|
||||||
|
|
||||||
|
// Reset selection to None
|
||||||
|
self.selection = Selection::None;
|
||||||
|
|
||||||
|
// Delete from start to end of selection
|
||||||
|
self.delete_range(start, end);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_change(&mut self, change: &Change) -> bool {
|
||||||
|
// Cannot apply changes if there is a pending change
|
||||||
|
if let Some(pending) = self.change.take() {
|
||||||
|
if !pending.items.is_empty() {
|
||||||
|
//TODO: is this a good idea?
|
||||||
|
log::warn!("pending change caused apply_change to be ignored!");
|
||||||
|
self.change = Some(pending);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in change.items.iter() {
|
||||||
|
//TODO: edit cursor if needed?
|
||||||
|
if item.insert {
|
||||||
|
self.cursor = self.insert_at(item.start, &item.text, None);
|
||||||
|
} else {
|
||||||
|
self.cursor = item.start;
|
||||||
|
self.delete_range(item.start, item.end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_change(&mut self) {
|
||||||
|
if self.change.is_none() {
|
||||||
|
self.change = Some(Change::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_change(&mut self) -> Option<Change> {
|
||||||
|
self.change.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
||||||
|
let old_cursor = self.cursor;
|
||||||
|
|
||||||
|
match action {
|
||||||
|
Action::Motion(motion) => {
|
||||||
|
let cursor = self.cursor;
|
||||||
|
let cursor_x_opt = self.cursor_x_opt;
|
||||||
|
if let Some((new_cursor, new_cursor_x_opt)) = self.with_buffer_mut(|buffer| {
|
||||||
|
buffer.cursor_motion(font_system, cursor, cursor_x_opt, motion)
|
||||||
|
}) {
|
||||||
|
self.cursor = new_cursor;
|
||||||
|
self.cursor_x_opt = new_cursor_x_opt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Escape => {
|
||||||
|
match self.selection {
|
||||||
|
Selection::None => {}
|
||||||
|
_ => self.with_buffer_mut(|buffer| buffer.set_redraw(true)),
|
||||||
|
}
|
||||||
|
self.selection = Selection::None;
|
||||||
|
}
|
||||||
|
Action::Insert(character) => {
|
||||||
|
if character.is_control() && !['\t', '\n', '\u{92}'].contains(&character) {
|
||||||
|
// Filter out special chars (except for tab), use Action instead
|
||||||
|
log::debug!("Refusing to insert control character {:?}", character);
|
||||||
|
} else if character == '\n' {
|
||||||
|
self.action(font_system, Action::Enter);
|
||||||
|
} else {
|
||||||
|
let mut str_buf = [0u8; 8];
|
||||||
|
let str_ref = character.encode_utf8(&mut str_buf);
|
||||||
|
self.insert_string(str_ref, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Enter => {
|
||||||
|
//TODO: what about indenting more after opening brackets or parentheses?
|
||||||
|
if self.auto_indent {
|
||||||
|
let mut string = String::from("\n");
|
||||||
|
self.with_buffer(|buffer| {
|
||||||
|
let line = &buffer.lines[self.cursor.line];
|
||||||
|
let text = line.text();
|
||||||
|
for c in text.chars() {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
string.push(c);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.insert_string(&string, None);
|
||||||
|
} else {
|
||||||
|
self.insert_string("\n", None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure line is properly shaped and laid out (for potential immediate commands)
|
||||||
|
let line_i = self.cursor.line;
|
||||||
|
self.with_buffer_mut(|buffer| {
|
||||||
|
buffer.line_layout(font_system, line_i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Action::Backspace => {
|
||||||
|
if self.delete_selection() {
|
||||||
|
// Deleted selection
|
||||||
|
} else {
|
||||||
|
// Save current cursor as end
|
||||||
|
let end = self.cursor;
|
||||||
|
|
||||||
|
if self.cursor.index > 0 {
|
||||||
|
// Move cursor to previous character index
|
||||||
|
self.cursor.index = self.with_buffer(|buffer| {
|
||||||
|
buffer.lines[self.cursor.line].text()[..self.cursor.index]
|
||||||
|
.char_indices()
|
||||||
|
.next_back()
|
||||||
|
.map_or(0, |(i, _)| i)
|
||||||
|
});
|
||||||
|
} else if self.cursor.line > 0 {
|
||||||
|
// Move cursor to previous line
|
||||||
|
self.cursor.line -= 1;
|
||||||
|
self.cursor.index =
|
||||||
|
self.with_buffer(|buffer| buffer.lines[self.cursor.line].text().len());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.cursor != end {
|
||||||
|
// Delete range
|
||||||
|
self.delete_range(self.cursor, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Delete => {
|
||||||
|
if self.delete_selection() {
|
||||||
|
// Deleted selection
|
||||||
|
} else {
|
||||||
|
// Save current cursor as start and end
|
||||||
|
let mut start = self.cursor;
|
||||||
|
let mut end = self.cursor;
|
||||||
|
|
||||||
|
self.with_buffer(|buffer| {
|
||||||
|
if start.index < buffer.lines[start.line].text().len() {
|
||||||
|
let line = &buffer.lines[start.line];
|
||||||
|
|
||||||
|
let range_opt = line
|
||||||
|
.text()
|
||||||
|
.grapheme_indices(true)
|
||||||
|
.take_while(|(i, _)| *i <= start.index)
|
||||||
|
.last()
|
||||||
|
.map(|(i, c)| i..(i + c.len()));
|
||||||
|
|
||||||
|
if let Some(range) = range_opt {
|
||||||
|
start.index = range.start;
|
||||||
|
end.index = range.end;
|
||||||
|
}
|
||||||
|
} else if start.line + 1 < buffer.lines.len() {
|
||||||
|
end.line += 1;
|
||||||
|
end.index = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if start != end {
|
||||||
|
self.cursor = start;
|
||||||
|
self.delete_range(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Indent => {
|
||||||
|
// Get start and end of selection
|
||||||
|
let (start, end) = match self.selection_bounds() {
|
||||||
|
Some(some) => some,
|
||||||
|
None => (self.cursor, self.cursor),
|
||||||
|
};
|
||||||
|
|
||||||
|
// For every line in selection
|
||||||
|
let tab_width: usize = self.tab_width().into();
|
||||||
|
for line_i in start.line..=end.line {
|
||||||
|
// Determine indexes of last indent and first character after whitespace
|
||||||
|
let mut after_whitespace = 0;
|
||||||
|
let mut required_indent = 0;
|
||||||
|
self.with_buffer(|buffer| {
|
||||||
|
let line = &buffer.lines[line_i];
|
||||||
|
let text = line.text();
|
||||||
|
// Default to end of line if no non-whitespace found
|
||||||
|
after_whitespace = text.len();
|
||||||
|
for (count, (index, c)) in text.char_indices().enumerate() {
|
||||||
|
if !c.is_whitespace() {
|
||||||
|
after_whitespace = index;
|
||||||
|
required_indent = tab_width - (count % tab_width);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// No indent required (not possible?)
|
||||||
|
if required_indent == 0 {
|
||||||
|
required_indent = tab_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.insert_at(
|
||||||
|
Cursor::new(line_i, after_whitespace),
|
||||||
|
&" ".repeat(required_indent),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adjust cursor
|
||||||
|
if self.cursor.line == line_i {
|
||||||
|
//TODO: should we be forcing cursor index to current indent location?
|
||||||
|
if self.cursor.index < after_whitespace {
|
||||||
|
self.cursor.index = after_whitespace;
|
||||||
|
}
|
||||||
|
self.cursor.index += required_indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust selection
|
||||||
|
match self.selection {
|
||||||
|
Selection::None => {}
|
||||||
|
Selection::Normal(ref mut select)
|
||||||
|
| Selection::Line(ref mut select)
|
||||||
|
| Selection::Word(ref mut select) => {
|
||||||
|
if select.line == line_i && select.index >= after_whitespace {
|
||||||
|
select.index += required_indent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request redraw
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Unindent => {
|
||||||
|
// Get start and end of selection
|
||||||
|
let (start, end) = match self.selection_bounds() {
|
||||||
|
Some(some) => some,
|
||||||
|
None => (self.cursor, self.cursor),
|
||||||
|
};
|
||||||
|
|
||||||
|
// For every line in selection
|
||||||
|
let tab_width: usize = self.tab_width().into();
|
||||||
|
for line_i in start.line..=end.line {
|
||||||
|
// Determine indexes of last indent and first character after whitespace
|
||||||
|
let mut last_indent = 0;
|
||||||
|
let mut after_whitespace = 0;
|
||||||
|
self.with_buffer(|buffer| {
|
||||||
|
let line = &buffer.lines[line_i];
|
||||||
|
let text = line.text();
|
||||||
|
// Default to end of line if no non-whitespace found
|
||||||
|
after_whitespace = text.len();
|
||||||
|
for (count, (index, c)) in text.char_indices().enumerate() {
|
||||||
|
if !c.is_whitespace() {
|
||||||
|
after_whitespace = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if count % tab_width == 0 {
|
||||||
|
last_indent = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// No de-indent required
|
||||||
|
if last_indent == after_whitespace {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete one indent
|
||||||
|
self.delete_range(
|
||||||
|
Cursor::new(line_i, last_indent),
|
||||||
|
Cursor::new(line_i, after_whitespace),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adjust cursor
|
||||||
|
if self.cursor.line == line_i && self.cursor.index > last_indent {
|
||||||
|
self.cursor.index -= after_whitespace - last_indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust selection
|
||||||
|
match self.selection {
|
||||||
|
Selection::None => {}
|
||||||
|
Selection::Normal(ref mut select)
|
||||||
|
| Selection::Line(ref mut select)
|
||||||
|
| Selection::Word(ref mut select) => {
|
||||||
|
if select.line == line_i && select.index > last_indent {
|
||||||
|
select.index -= after_whitespace - last_indent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request redraw
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Click { x, y } => {
|
||||||
|
self.set_selection(Selection::None);
|
||||||
|
|
||||||
|
if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32))
|
||||||
|
{
|
||||||
|
if new_cursor != self.cursor {
|
||||||
|
self.cursor = new_cursor;
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::DoubleClick { x, y } => {
|
||||||
|
self.set_selection(Selection::None);
|
||||||
|
|
||||||
|
if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32))
|
||||||
|
{
|
||||||
|
if new_cursor != self.cursor {
|
||||||
|
self.cursor = new_cursor;
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
self.selection = Selection::Word(self.cursor);
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::TripleClick { x, y } => {
|
||||||
|
self.set_selection(Selection::None);
|
||||||
|
|
||||||
|
if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32))
|
||||||
|
{
|
||||||
|
if new_cursor != self.cursor {
|
||||||
|
self.cursor = new_cursor;
|
||||||
|
}
|
||||||
|
self.selection = Selection::Line(self.cursor);
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Drag { x, y } => {
|
||||||
|
if self.selection == Selection::None {
|
||||||
|
self.selection = Selection::Normal(self.cursor);
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32))
|
||||||
|
{
|
||||||
|
if new_cursor != self.cursor {
|
||||||
|
self.cursor = new_cursor;
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Scroll { lines } => {
|
||||||
|
self.with_buffer_mut(|buffer| {
|
||||||
|
let mut scroll = buffer.scroll();
|
||||||
|
//TODO: align to layout lines
|
||||||
|
scroll.vertical += lines as f32 * buffer.metrics().line_height;
|
||||||
|
buffer.set_scroll(scroll);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if old_cursor != self.cursor {
|
||||||
|
self.cursor_moved = true;
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(true));
|
||||||
|
|
||||||
|
/*TODO
|
||||||
|
if let Some(glyph) = run.glyphs.get(new_cursor_glyph) {
|
||||||
|
let font_opt = self.buffer.font_system().get_font(glyph.cache_key.font_id);
|
||||||
|
let text_glyph = &run.text[glyph.start..glyph.end];
|
||||||
|
log::debug!(
|
||||||
|
"{}, {}: '{}' ('{}'): '{}' ({:?})",
|
||||||
|
self.cursor.line,
|
||||||
|
self.cursor.index,
|
||||||
|
font_opt.as_ref().map_or("?", |font| font.info.family.as_str()),
|
||||||
|
font_opt.as_ref().map_or("?", |font| font.info.post_script_name.as_str()),
|
||||||
|
text_glyph,
|
||||||
|
text_glyph
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_position(&self) -> Option<(i32, i32)> {
|
||||||
|
self.with_buffer(|buffer| {
|
||||||
|
buffer
|
||||||
|
.layout_runs()
|
||||||
|
.find_map(|run| cursor_position(&self.cursor, &run))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorrowedWithFontSystem<'_, Editor<'_>> {
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
pub fn draw<F>(
|
||||||
|
&mut self,
|
||||||
|
cache: &mut crate::SwashCache,
|
||||||
|
text_color: Color,
|
||||||
|
cursor_color: Color,
|
||||||
|
selection_color: Color,
|
||||||
|
selected_text_color: Color,
|
||||||
|
f: F,
|
||||||
|
) where
|
||||||
|
F: FnMut(i32, i32, u32, u32, Color),
|
||||||
|
{
|
||||||
|
self.inner.draw(
|
||||||
|
self.font_system,
|
||||||
|
cache,
|
||||||
|
text_color,
|
||||||
|
cursor_color,
|
||||||
|
selection_color,
|
||||||
|
selected_text_color,
|
||||||
|
f,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+366
@@ -0,0 +1,366 @@
|
|||||||
|
use alloc::sync::Arc;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
use core::cmp;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::{AttrsList, BorrowedWithFontSystem, Buffer, Cursor, FontSystem, Motion};
|
||||||
|
|
||||||
|
pub use self::editor::*;
|
||||||
|
mod editor;
|
||||||
|
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
pub use self::syntect::*;
|
||||||
|
#[cfg(feature = "syntect")]
|
||||||
|
mod syntect;
|
||||||
|
|
||||||
|
#[cfg(feature = "vi")]
|
||||||
|
pub use self::vi::*;
|
||||||
|
#[cfg(feature = "vi")]
|
||||||
|
mod vi;
|
||||||
|
|
||||||
|
/// An action to perform on an [`Editor`]
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Action {
|
||||||
|
/// Move the cursor with some motion
|
||||||
|
Motion(Motion),
|
||||||
|
/// Escape, clears selection
|
||||||
|
Escape,
|
||||||
|
/// Insert character at cursor
|
||||||
|
Insert(char),
|
||||||
|
/// Create new line
|
||||||
|
Enter,
|
||||||
|
/// Delete text behind cursor
|
||||||
|
Backspace,
|
||||||
|
/// Delete text in front of cursor
|
||||||
|
Delete,
|
||||||
|
// Indent text (typically Tab)
|
||||||
|
Indent,
|
||||||
|
// Unindent text (typically Shift+Tab)
|
||||||
|
Unindent,
|
||||||
|
/// Mouse click at specified position
|
||||||
|
Click {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
},
|
||||||
|
/// Mouse double click at specified position
|
||||||
|
DoubleClick {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
},
|
||||||
|
/// Mouse triple click at specified position
|
||||||
|
TripleClick {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
},
|
||||||
|
/// Mouse drag to specified position
|
||||||
|
Drag {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
},
|
||||||
|
/// Scroll specified number of lines
|
||||||
|
Scroll {
|
||||||
|
lines: i32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BufferRef<'buffer> {
|
||||||
|
Owned(Buffer),
|
||||||
|
Borrowed(&'buffer mut Buffer),
|
||||||
|
Arc(Arc<Buffer>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for BufferRef<'_> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Owned(buffer) => Self::Owned(buffer.clone()),
|
||||||
|
Self::Borrowed(buffer) => Self::Owned((*buffer).clone()),
|
||||||
|
Self::Arc(buffer) => Self::Arc(buffer.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Buffer> for BufferRef<'_> {
|
||||||
|
fn from(buffer: Buffer) -> Self {
|
||||||
|
Self::Owned(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'buffer> From<&'buffer mut Buffer> for BufferRef<'buffer> {
|
||||||
|
fn from(buffer: &'buffer mut Buffer) -> Self {
|
||||||
|
Self::Borrowed(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<Buffer>> for BufferRef<'_> {
|
||||||
|
fn from(arc: Arc<Buffer>) -> Self {
|
||||||
|
Self::Arc(arc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A unique change to an editor
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ChangeItem {
|
||||||
|
/// Cursor indicating start of change
|
||||||
|
pub start: Cursor,
|
||||||
|
/// Cursor indicating end of change
|
||||||
|
pub end: Cursor,
|
||||||
|
/// Text to be inserted or deleted
|
||||||
|
pub text: String,
|
||||||
|
/// Insert if true, delete if false
|
||||||
|
pub insert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeItem {
|
||||||
|
// Reverse change item (in place)
|
||||||
|
pub fn reverse(&mut self) {
|
||||||
|
self.insert = !self.insert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of change items grouped into one logical change
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Change {
|
||||||
|
/// Change items grouped into one change
|
||||||
|
pub items: Vec<ChangeItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Change {
|
||||||
|
// Reverse change (in place)
|
||||||
|
pub fn reverse(&mut self) {
|
||||||
|
self.items.reverse();
|
||||||
|
for item in self.items.iter_mut() {
|
||||||
|
item.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selection mode
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Selection {
|
||||||
|
/// No selection
|
||||||
|
None,
|
||||||
|
/// Normal selection
|
||||||
|
Normal(Cursor),
|
||||||
|
/// Select by lines
|
||||||
|
Line(Cursor),
|
||||||
|
/// Select by words
|
||||||
|
Word(Cursor),
|
||||||
|
//TODO: Select block
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor`
|
||||||
|
pub trait Edit<'buffer> {
|
||||||
|
/// Mutably borrows `self` together with an [`FontSystem`] for more convenient methods
|
||||||
|
fn borrow_with<'font_system>(
|
||||||
|
&'font_system mut self,
|
||||||
|
font_system: &'font_system mut FontSystem,
|
||||||
|
) -> BorrowedWithFontSystem<'font_system, Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
BorrowedWithFontSystem {
|
||||||
|
inner: self,
|
||||||
|
font_system,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the internal [`BufferRef`]
|
||||||
|
fn buffer_ref(&self) -> &BufferRef<'buffer>;
|
||||||
|
|
||||||
|
/// Get the internal [`BufferRef`]
|
||||||
|
fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer>;
|
||||||
|
|
||||||
|
/// Get the internal [`Buffer`]
|
||||||
|
fn with_buffer<F: FnOnce(&Buffer) -> T, T>(&self, f: F) -> T {
|
||||||
|
match self.buffer_ref() {
|
||||||
|
BufferRef::Owned(buffer) => f(buffer),
|
||||||
|
BufferRef::Borrowed(buffer) => f(buffer),
|
||||||
|
BufferRef::Arc(buffer) => f(buffer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the internal [`Buffer`], mutably
|
||||||
|
fn with_buffer_mut<F: FnOnce(&mut Buffer) -> T, T>(&mut self, f: F) -> T {
|
||||||
|
match self.buffer_ref_mut() {
|
||||||
|
BufferRef::Owned(buffer) => f(buffer),
|
||||||
|
BufferRef::Borrowed(buffer) => f(buffer),
|
||||||
|
BufferRef::Arc(buffer) => f(Arc::make_mut(buffer)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [`Buffer`] redraw flag
|
||||||
|
fn redraw(&self) -> bool {
|
||||||
|
self.with_buffer(|buffer| buffer.redraw())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the [`Buffer`] redraw flag
|
||||||
|
fn set_redraw(&mut self, redraw: bool) {
|
||||||
|
self.with_buffer_mut(|buffer| buffer.set_redraw(redraw));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current cursor
|
||||||
|
fn cursor(&self) -> Cursor;
|
||||||
|
|
||||||
|
/// Set the current cursor
|
||||||
|
fn set_cursor(&mut self, cursor: Cursor);
|
||||||
|
|
||||||
|
/// Get the current selection position
|
||||||
|
fn selection(&self) -> Selection;
|
||||||
|
|
||||||
|
/// Set the current selection position
|
||||||
|
fn set_selection(&mut self, selection: Selection);
|
||||||
|
|
||||||
|
/// Get the bounds of the current selection
|
||||||
|
//TODO: will not work with Block select
|
||||||
|
fn selection_bounds(&self) -> Option<(Cursor, Cursor)> {
|
||||||
|
self.with_buffer(|buffer| {
|
||||||
|
let cursor = self.cursor();
|
||||||
|
match self.selection() {
|
||||||
|
Selection::None => None,
|
||||||
|
Selection::Normal(select) => match select.line.cmp(&cursor.line) {
|
||||||
|
cmp::Ordering::Greater => Some((cursor, select)),
|
||||||
|
cmp::Ordering::Less => Some((select, cursor)),
|
||||||
|
cmp::Ordering::Equal => {
|
||||||
|
/* select.line == cursor.line */
|
||||||
|
if select.index < cursor.index {
|
||||||
|
Some((select, cursor))
|
||||||
|
} else {
|
||||||
|
/* select.index >= cursor.index */
|
||||||
|
Some((cursor, select))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Selection::Line(select) => {
|
||||||
|
let start_line = cmp::min(select.line, cursor.line);
|
||||||
|
let end_line = cmp::max(select.line, cursor.line);
|
||||||
|
let end_index = buffer.lines[end_line].text().len();
|
||||||
|
Some((Cursor::new(start_line, 0), Cursor::new(end_line, end_index)))
|
||||||
|
}
|
||||||
|
Selection::Word(select) => {
|
||||||
|
let (mut start, mut end) = match select.line.cmp(&cursor.line) {
|
||||||
|
cmp::Ordering::Greater => (cursor, select),
|
||||||
|
cmp::Ordering::Less => (select, cursor),
|
||||||
|
cmp::Ordering::Equal => {
|
||||||
|
/* select.line == cursor.line */
|
||||||
|
if select.index < cursor.index {
|
||||||
|
(select, cursor)
|
||||||
|
} else {
|
||||||
|
/* select.index >= cursor.index */
|
||||||
|
(cursor, select)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move start to beginning of word
|
||||||
|
{
|
||||||
|
let line = &buffer.lines[start.line];
|
||||||
|
start.index = line
|
||||||
|
.text()
|
||||||
|
.unicode_word_indices()
|
||||||
|
.rev()
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.find(|&i| i < start.index)
|
||||||
|
.unwrap_or(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move end to end of word
|
||||||
|
{
|
||||||
|
let line = &buffer.lines[end.line];
|
||||||
|
end.index = line
|
||||||
|
.text()
|
||||||
|
.unicode_word_indices()
|
||||||
|
.map(|(i, word)| i + word.len())
|
||||||
|
.find(|&i| i > end.index)
|
||||||
|
.unwrap_or(line.text().len());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((start, end))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current automatic indentation setting
|
||||||
|
fn auto_indent(&self) -> bool;
|
||||||
|
|
||||||
|
/// Enable or disable automatic indentation
|
||||||
|
fn set_auto_indent(&mut self, auto_indent: bool);
|
||||||
|
|
||||||
|
/// Get the current tab width
|
||||||
|
fn tab_width(&self) -> u16;
|
||||||
|
|
||||||
|
/// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored
|
||||||
|
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16);
|
||||||
|
|
||||||
|
/// Shape lines until scroll, after adjusting scroll if the cursor moved
|
||||||
|
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool);
|
||||||
|
|
||||||
|
/// Delete text starting at start Cursor and ending at end Cursor
|
||||||
|
fn delete_range(&mut self, start: Cursor, end: Cursor);
|
||||||
|
|
||||||
|
/// Insert text at specified cursor with specified `attrs_list`
|
||||||
|
fn insert_at(&mut self, cursor: Cursor, data: &str, attrs_list: Option<AttrsList>) -> Cursor;
|
||||||
|
|
||||||
|
/// Copy selection
|
||||||
|
fn copy_selection(&self) -> Option<String>;
|
||||||
|
|
||||||
|
/// Delete selection, adjusting cursor and returning true if there was a selection
|
||||||
|
// Also used by backspace, delete, insert, and enter when there is a selection
|
||||||
|
fn delete_selection(&mut self) -> bool;
|
||||||
|
|
||||||
|
/// Insert a string at the current cursor or replacing the current selection with the given
|
||||||
|
/// attributes, or with the previous character's attributes if None is given.
|
||||||
|
fn insert_string(&mut self, data: &str, attrs_list: Option<AttrsList>) {
|
||||||
|
self.delete_selection();
|
||||||
|
let new_cursor = self.insert_at(self.cursor(), data, attrs_list);
|
||||||
|
self.set_cursor(new_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a change
|
||||||
|
fn apply_change(&mut self, change: &Change) -> bool;
|
||||||
|
|
||||||
|
/// Start collecting change
|
||||||
|
fn start_change(&mut self);
|
||||||
|
|
||||||
|
/// Get completed change
|
||||||
|
fn finish_change(&mut self) -> Option<Change>;
|
||||||
|
|
||||||
|
/// Perform an [Action] on the editor
|
||||||
|
fn action(&mut self, font_system: &mut FontSystem, action: Action);
|
||||||
|
|
||||||
|
/// Get X and Y position of the top left corner of the cursor
|
||||||
|
fn cursor_position(&self) -> Option<(i32, i32)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'buffer, E: Edit<'buffer>> BorrowedWithFontSystem<'_, E> {
|
||||||
|
/// Get the internal [`Buffer`], mutably
|
||||||
|
pub fn with_buffer_mut<F: FnOnce(&mut BorrowedWithFontSystem<Buffer>) -> T, T>(
|
||||||
|
&mut self,
|
||||||
|
f: F,
|
||||||
|
) -> T {
|
||||||
|
self.inner.with_buffer_mut(|buffer| {
|
||||||
|
let mut borrowed = BorrowedWithFontSystem {
|
||||||
|
inner: buffer,
|
||||||
|
font_system: self.font_system,
|
||||||
|
};
|
||||||
|
f(&mut borrowed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored
|
||||||
|
pub fn set_tab_width(&mut self, tab_width: u16) {
|
||||||
|
self.inner.set_tab_width(self.font_system, tab_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shape lines until scroll, after adjusting scroll if the cursor moved
|
||||||
|
pub fn shape_as_needed(&mut self, prune: bool) {
|
||||||
|
self.inner.shape_as_needed(self.font_system, prune);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform an [Action] on the editor
|
||||||
|
pub fn action(&mut self, action: Action) {
|
||||||
|
self.inner.action(self.font_system, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
+463
@@ -0,0 +1,463 @@
|
|||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::{fs, io, path::Path};
|
||||||
|
use syntect::highlighting::{
|
||||||
|
FontStyle, HighlightState, Highlighter, RangedHighlightIterator, ThemeSet,
|
||||||
|
};
|
||||||
|
use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, Editor,
|
||||||
|
FontSystem, Selection, Shaping, Style, Weight,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use syntect::highlighting::Theme as SyntaxTheme;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SyntaxSystem {
|
||||||
|
pub syntax_set: SyntaxSet,
|
||||||
|
pub theme_set: ThemeSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyntaxSystem {
|
||||||
|
/// Create a new [`SyntaxSystem`]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
//TODO: store newlines in buffer
|
||||||
|
syntax_set: SyntaxSet::load_defaults_nonewlines(),
|
||||||
|
theme_set: ThemeSet::load_defaults(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper of [`Editor`] with syntax highlighting provided by [`SyntaxSystem`]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SyntaxEditor<'syntax_system, 'buffer> {
|
||||||
|
editor: Editor<'buffer>,
|
||||||
|
syntax_system: &'syntax_system SyntaxSystem,
|
||||||
|
syntax: &'syntax_system SyntaxReference,
|
||||||
|
theme: &'syntax_system SyntaxTheme,
|
||||||
|
highlighter: Highlighter<'syntax_system>,
|
||||||
|
syntax_cache: Vec<(ParseState, ScopeStack)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> {
|
||||||
|
/// Create a new [`SyntaxEditor`] with the provided [`Buffer`], [`SyntaxSystem`], and theme name.
|
||||||
|
///
|
||||||
|
/// A good default theme name is "base16-eighties.dark".
|
||||||
|
///
|
||||||
|
/// Returns None if theme not found
|
||||||
|
pub fn new(
|
||||||
|
buffer: impl Into<BufferRef<'buffer>>,
|
||||||
|
syntax_system: &'syntax_system SyntaxSystem,
|
||||||
|
theme_name: &str,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let editor = Editor::new(buffer);
|
||||||
|
let syntax = syntax_system.syntax_set.find_syntax_plain_text();
|
||||||
|
let theme = syntax_system.theme_set.themes.get(theme_name)?;
|
||||||
|
let highlighter = Highlighter::new(theme);
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
editor,
|
||||||
|
syntax_system,
|
||||||
|
syntax,
|
||||||
|
theme,
|
||||||
|
highlighter,
|
||||||
|
syntax_cache: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing
|
||||||
|
pub fn update_theme(&mut self, theme_name: &str) -> bool {
|
||||||
|
if let Some(theme) = self.syntax_system.theme_set.themes.get(theme_name) {
|
||||||
|
if self.theme != theme {
|
||||||
|
self.theme = theme;
|
||||||
|
self.highlighter = Highlighter::new(theme);
|
||||||
|
self.syntax_cache.clear();
|
||||||
|
|
||||||
|
// Reset attrs to match default foreground and no highlighting
|
||||||
|
self.with_buffer_mut(|buffer| {
|
||||||
|
for line in buffer.lines.iter_mut() {
|
||||||
|
let mut attrs = line.attrs_list().defaults();
|
||||||
|
if let Some(foreground) = self.theme.settings.foreground {
|
||||||
|
attrs = attrs.color(Color::rgba(
|
||||||
|
foreground.r,
|
||||||
|
foreground.g,
|
||||||
|
foreground.b,
|
||||||
|
foreground.a,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
line.set_attrs_list(AttrsList::new(&attrs));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load text from a file, and also set syntax to the best option
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
///
|
||||||
|
/// Returns an [`io::Error`] if reading the file fails
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub fn load_text<P: AsRef<Path>>(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
path: P,
|
||||||
|
mut attrs: crate::Attrs,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
// Set attrs to match default foreground
|
||||||
|
if let Some(foreground) = self.theme.settings.foreground {
|
||||||
|
attrs = attrs.color(Color::rgba(
|
||||||
|
foreground.r,
|
||||||
|
foreground.g,
|
||||||
|
foreground.b,
|
||||||
|
foreground.a,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = fs::read_to_string(path)?;
|
||||||
|
self.editor.with_buffer_mut(|buffer| {
|
||||||
|
buffer.set_text(font_system, &text, &attrs, Shaping::Advanced);
|
||||||
|
});
|
||||||
|
|
||||||
|
//TODO: re-use text
|
||||||
|
self.syntax = match self.syntax_system.syntax_set.find_syntax_for_file(path) {
|
||||||
|
Ok(Some(some)) => some,
|
||||||
|
Ok(None) => {
|
||||||
|
log::warn!("no syntax found for {:?}", path);
|
||||||
|
self.syntax_system.syntax_set.find_syntax_plain_text()
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to determine syntax for {:?}: {:?}", path, err);
|
||||||
|
self.syntax_system.syntax_set.find_syntax_plain_text()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear syntax cache
|
||||||
|
self.syntax_cache.clear();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set syntax highlighting by file extension
|
||||||
|
pub fn syntax_by_extension(&mut self, extension: &str) {
|
||||||
|
self.syntax = match self
|
||||||
|
.syntax_system
|
||||||
|
.syntax_set
|
||||||
|
.find_syntax_by_extension(extension)
|
||||||
|
{
|
||||||
|
Some(some) => some,
|
||||||
|
None => {
|
||||||
|
log::warn!("no syntax found for {}", extension);
|
||||||
|
self.syntax_system.syntax_set.find_syntax_plain_text()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.syntax_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default background color
|
||||||
|
pub fn background_color(&self) -> Color {
|
||||||
|
if let Some(background) = self.theme.settings.background {
|
||||||
|
Color::rgba(background.r, background.g, background.b, background.a)
|
||||||
|
} else {
|
||||||
|
Color::rgb(0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default foreground (text) color
|
||||||
|
pub fn foreground_color(&self) -> Color {
|
||||||
|
if let Some(foreground) = self.theme.settings.foreground {
|
||||||
|
Color::rgba(foreground.r, foreground.g, foreground.b, foreground.a)
|
||||||
|
} else {
|
||||||
|
Color::rgb(0xFF, 0xFF, 0xFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default cursor color
|
||||||
|
pub fn cursor_color(&self) -> Color {
|
||||||
|
if let Some(some) = self.theme.settings.caret {
|
||||||
|
Color::rgba(some.r, some.g, some.b, some.a)
|
||||||
|
} else {
|
||||||
|
self.foreground_color()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default selection color
|
||||||
|
pub fn selection_color(&self) -> Color {
|
||||||
|
if let Some(some) = self.theme.settings.selection {
|
||||||
|
Color::rgba(some.r, some.g, some.b, some.a)
|
||||||
|
} else {
|
||||||
|
let foreground_color = self.foreground_color();
|
||||||
|
Color::rgba(
|
||||||
|
foreground_color.r(),
|
||||||
|
foreground_color.g(),
|
||||||
|
foreground_color.b(),
|
||||||
|
0x33,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current syntect theme
|
||||||
|
pub fn theme(&self) -> &SyntaxTheme {
|
||||||
|
self.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the editor
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
pub fn draw<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, mut f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(i32, i32, u32, u32, Color),
|
||||||
|
{
|
||||||
|
let size = self.with_buffer(|buffer| buffer.size());
|
||||||
|
if let Some(width) = size.0 {
|
||||||
|
if let Some(height) = size.1 {
|
||||||
|
f(0, 0, width as u32, height as u32, self.background_color());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.editor.draw(
|
||||||
|
font_system,
|
||||||
|
cache,
|
||||||
|
self.foreground_color(),
|
||||||
|
self.cursor_color(),
|
||||||
|
self.selection_color(),
|
||||||
|
self.foreground_color(),
|
||||||
|
f,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'buffer> Edit<'buffer> for SyntaxEditor<'_, 'buffer> {
|
||||||
|
fn buffer_ref(&self) -> &BufferRef<'buffer> {
|
||||||
|
self.editor.buffer_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer> {
|
||||||
|
self.editor.buffer_ref_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor(&self) -> Cursor {
|
||||||
|
self.editor.cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cursor(&mut self, cursor: Cursor) {
|
||||||
|
self.editor.set_cursor(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection(&self) -> Selection {
|
||||||
|
self.editor.selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selection(&mut self, selection: Selection) {
|
||||||
|
self.editor.set_selection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_indent(&self) -> bool {
|
||||||
|
self.editor.auto_indent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_auto_indent(&mut self, auto_indent: bool) {
|
||||||
|
self.editor.set_auto_indent(auto_indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_width(&self) -> u16 {
|
||||||
|
self.editor.tab_width()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) {
|
||||||
|
self.editor.set_tab_width(font_system, tab_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) {
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
let cursor = self.cursor();
|
||||||
|
self.editor.with_buffer_mut(|buffer| {
|
||||||
|
let metrics = buffer.metrics();
|
||||||
|
let scroll = buffer.scroll();
|
||||||
|
let scroll_end = scroll.vertical + buffer.size().1.unwrap_or(f32::INFINITY);
|
||||||
|
let mut total_height = 0.0;
|
||||||
|
let mut highlighted = 0;
|
||||||
|
for line_i in 0..buffer.lines.len() {
|
||||||
|
// Break out if we have reached the end of scroll and are past the cursor
|
||||||
|
if total_height > scroll_end && line_i > cursor.line {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = &mut buffer.lines[line_i];
|
||||||
|
if line.metadata().is_some() && line_i < self.syntax_cache.len() {
|
||||||
|
//TODO: duplicated code!
|
||||||
|
if line_i >= scroll.line && total_height < scroll_end {
|
||||||
|
// Perform shaping and layout of this line in order to count if we have reached scroll
|
||||||
|
match buffer.line_layout(font_system, line_i) {
|
||||||
|
Some(layout_lines) => {
|
||||||
|
for layout_line in layout_lines.iter() {
|
||||||
|
total_height +=
|
||||||
|
layout_line.line_height_opt.unwrap_or(metrics.line_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
//TODO: should this be possible?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
highlighted += 1;
|
||||||
|
|
||||||
|
let (mut parse_state, scope_stack) =
|
||||||
|
if line_i > 0 && line_i <= self.syntax_cache.len() {
|
||||||
|
self.syntax_cache[line_i - 1].clone()
|
||||||
|
} else {
|
||||||
|
(ParseState::new(self.syntax), ScopeStack::new())
|
||||||
|
};
|
||||||
|
let mut highlight_state = HighlightState::new(&self.highlighter, scope_stack);
|
||||||
|
let ops = parse_state
|
||||||
|
.parse_line(line.text(), &self.syntax_system.syntax_set)
|
||||||
|
.expect("failed to parse syntax");
|
||||||
|
let ranges = RangedHighlightIterator::new(
|
||||||
|
&mut highlight_state,
|
||||||
|
&ops,
|
||||||
|
line.text(),
|
||||||
|
&self.highlighter,
|
||||||
|
);
|
||||||
|
|
||||||
|
let attrs = line.attrs_list().defaults();
|
||||||
|
let mut attrs_list = AttrsList::new(&attrs);
|
||||||
|
let original_attrs = attrs.clone(); // Store a clone for comparison
|
||||||
|
for (style, _, range) in ranges {
|
||||||
|
let span_attrs = attrs
|
||||||
|
.clone() // Clone attrs for modification
|
||||||
|
.color(Color::rgba(
|
||||||
|
style.foreground.r,
|
||||||
|
style.foreground.g,
|
||||||
|
style.foreground.b,
|
||||||
|
style.foreground.a,
|
||||||
|
))
|
||||||
|
//TODO: background
|
||||||
|
.style(if style.font_style.contains(FontStyle::ITALIC) {
|
||||||
|
Style::Italic
|
||||||
|
} else {
|
||||||
|
Style::Normal
|
||||||
|
})
|
||||||
|
.weight(if style.font_style.contains(FontStyle::BOLD) {
|
||||||
|
Weight::BOLD
|
||||||
|
} else {
|
||||||
|
Weight::NORMAL
|
||||||
|
}); //TODO: underline
|
||||||
|
if span_attrs != original_attrs {
|
||||||
|
attrs_list.add_span(range, &span_attrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update line attributes. This operation only resets if the line changes
|
||||||
|
line.set_attrs_list(attrs_list);
|
||||||
|
|
||||||
|
// Perform shaping and layout of this line in order to count if we have reached scroll
|
||||||
|
if line_i >= scroll.line && total_height < scroll_end {
|
||||||
|
match buffer.line_layout(font_system, line_i) {
|
||||||
|
Some(layout_lines) => {
|
||||||
|
for layout_line in layout_lines.iter() {
|
||||||
|
total_height +=
|
||||||
|
layout_line.line_height_opt.unwrap_or(metrics.line_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
//TODO: should this be possible?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache_item = (parse_state.clone(), highlight_state.path.clone());
|
||||||
|
if line_i < self.syntax_cache.len() {
|
||||||
|
if self.syntax_cache[line_i] != cache_item {
|
||||||
|
self.syntax_cache[line_i] = cache_item;
|
||||||
|
if line_i + 1 < buffer.lines.len() {
|
||||||
|
buffer.lines[line_i + 1].reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer.lines[line_i].set_metadata(self.syntax_cache.len());
|
||||||
|
self.syntax_cache.push(cache_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if highlighted > 0 {
|
||||||
|
buffer.set_redraw(true);
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
log::debug!(
|
||||||
|
"Syntax highlighted {} lines in {:?}",
|
||||||
|
highlighted,
|
||||||
|
now.elapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.editor.shape_as_needed(font_system, prune);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_range(&mut self, start: Cursor, end: Cursor) {
|
||||||
|
self.editor.delete_range(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_at(&mut self, cursor: Cursor, data: &str, attrs_list: Option<AttrsList>) -> Cursor {
|
||||||
|
self.editor.insert_at(cursor, data, attrs_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_selection(&self) -> Option<String> {
|
||||||
|
self.editor.copy_selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_selection(&mut self) -> bool {
|
||||||
|
self.editor.delete_selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_change(&mut self, change: &Change) -> bool {
|
||||||
|
self.editor.apply_change(change)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_change(&mut self) {
|
||||||
|
self.editor.start_change();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_change(&mut self) -> Option<Change> {
|
||||||
|
self.editor.finish_change()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
||||||
|
self.editor.action(font_system, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_position(&self) -> Option<(i32, i32)> {
|
||||||
|
self.editor.cursor_position()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorrowedWithFontSystem<'_, SyntaxEditor<'_, '_>> {
|
||||||
|
/// Load text from a file, and also set syntax to the best option
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
///
|
||||||
|
/// Returns an [`io::Error`] if reading the file fails
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub fn load_text<P: AsRef<Path>>(&mut self, path: P, attrs: crate::Attrs) -> io::Result<()> {
|
||||||
|
self.inner.load_text(self.font_system, path, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
pub fn draw<F>(&mut self, cache: &mut crate::SwashCache, f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(i32, i32, u32, u32, Color),
|
||||||
|
{
|
||||||
|
self.inner.draw(self.font_system, cache, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1173
File diff suppressed because it is too large.
Load diff
+110
@@ -0,0 +1,110 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use unicode_script::Script;
|
||||||
|
|
||||||
|
use super::Fallback;
|
||||||
|
|
||||||
|
/// A platform-specific font fallback list, for `MacOS`.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PlatformFallback;
|
||||||
|
|
||||||
|
impl Fallback for PlatformFallback {
|
||||||
|
fn common_fallback(&self) -> &'static [&'static str] {
|
||||||
|
common_fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forbidden_fallback(&self) -> &'static [&'static str] {
|
||||||
|
forbidden_fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_fallback(
|
||||||
|
&self,
|
||||||
|
script: unicode_script::Script,
|
||||||
|
locale: &str,
|
||||||
|
) -> &'static [&'static str] {
|
||||||
|
script_fallback(script, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to use after any script specific fallbacks
|
||||||
|
fn common_fallback() -> &'static [&'static str] {
|
||||||
|
&[
|
||||||
|
".SF NS",
|
||||||
|
"Menlo",
|
||||||
|
"Apple Color Emoji",
|
||||||
|
"Geneva",
|
||||||
|
"Arial Unicode MS",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to never use
|
||||||
|
fn forbidden_fallback() -> &'static [&'static str] {
|
||||||
|
&[".LastResort"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||||
|
match locale {
|
||||||
|
// Japan
|
||||||
|
"ja" => &["Hiragino Sans"],
|
||||||
|
// Korea
|
||||||
|
"ko" => &["Apple SD Gothic Neo"],
|
||||||
|
// Hong Kong
|
||||||
|
"zh-HK" => &["PingFang HK"],
|
||||||
|
// Taiwan
|
||||||
|
"zh-TW" => &["PingFang TC"],
|
||||||
|
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||||
|
_ => &["PingFang SC"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to use per script
|
||||||
|
fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||||
|
//TODO: abstract style (sans/serif/monospaced)
|
||||||
|
//TODO: pull more data from about:config font.name-list.sans-serif in Firefox
|
||||||
|
match script {
|
||||||
|
Script::Adlam => &["Noto Sans Adlam"],
|
||||||
|
Script::Arabic => &["Geeza Pro"],
|
||||||
|
Script::Armenian => &["Noto Sans Armenian"],
|
||||||
|
Script::Bengali => &["Bangla Sangam MN"],
|
||||||
|
Script::Buhid => &["Noto Sans Buhid"],
|
||||||
|
Script::Canadian_Aboriginal => &["Euphemia UCAS"],
|
||||||
|
Script::Chakma => &["Noto Sans Chakma"],
|
||||||
|
Script::Devanagari => &["Devanagari Sangam MN"],
|
||||||
|
Script::Ethiopic => &["Kefa"],
|
||||||
|
Script::Gothic => &["Noto Sans Gothic"],
|
||||||
|
Script::Grantha => &["Grantha Sangam MN"],
|
||||||
|
Script::Gujarati => &["Gujarati Sangam MN"],
|
||||||
|
Script::Gurmukhi => &["Gurmukhi Sangam MN"],
|
||||||
|
Script::Han => han_unification(locale),
|
||||||
|
Script::Hangul => han_unification("ko"),
|
||||||
|
Script::Hanunoo => &["Noto Sans Hanunoo"],
|
||||||
|
Script::Hebrew => &["Arial"],
|
||||||
|
Script::Hiragana => han_unification("ja"),
|
||||||
|
Script::Javanese => &["Noto Sans Javanese"],
|
||||||
|
Script::Kannada => &["Noto Sans Kannada"],
|
||||||
|
Script::Katakana => han_unification("ja"),
|
||||||
|
Script::Khmer => &["Khmer Sangam MN"],
|
||||||
|
Script::Lao => &["Lao Sangam MN"],
|
||||||
|
Script::Malayalam => &["Malayalam Sangam MN"],
|
||||||
|
Script::Mongolian => &["Noto Sans Mongolian"],
|
||||||
|
Script::Myanmar => &["Noto Sans Myanmar"],
|
||||||
|
Script::Oriya => &["Noto Sans Oriya"],
|
||||||
|
Script::Sinhala => &["Sinhala Sangam MN"],
|
||||||
|
Script::Syriac => &["Noto Sans Syriac"],
|
||||||
|
Script::Tagalog => &["Noto Sans Tagalog"],
|
||||||
|
Script::Tagbanwa => &["Noto Sans Tagbanwa"],
|
||||||
|
Script::Tai_Le => &["Noto Sans Tai Le"],
|
||||||
|
Script::Tai_Tham => &["Noto Sans Tai Tham"],
|
||||||
|
Script::Tai_Viet => &["Noto Sans Tai Viet"],
|
||||||
|
Script::Tamil => &["InaiMathi"],
|
||||||
|
Script::Telugu => &["Telugu Sangam MN"],
|
||||||
|
Script::Thaana => &["Noto Sans Thaana"],
|
||||||
|
Script::Thai => &["Ayuthaya"],
|
||||||
|
Script::Tibetan => &["Kailasa"],
|
||||||
|
Script::Tifinagh => &["Noto Sans Tifinagh"],
|
||||||
|
Script::Vai => &["Noto Sans Vai"],
|
||||||
|
//TODO: Use han_unification?
|
||||||
|
Script::Yi => &["Noto Sans Yi", "PingFang SC"],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
+480
@@ -0,0 +1,480 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use alloc::borrow::ToOwned;
|
||||||
|
use alloc::string::String;
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use core::{mem, ops::Range};
|
||||||
|
use fontdb::Family;
|
||||||
|
use unicode_script::Script;
|
||||||
|
|
||||||
|
use crate::{BuildHasher, Font, FontMatchKey, FontSystem, HashMap, ShapeBuffer};
|
||||||
|
|
||||||
|
#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "windows")))]
|
||||||
|
#[path = "other.rs"]
|
||||||
|
mod platform;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[path = "macos.rs"]
|
||||||
|
mod platform;
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
|
||||||
|
#[path = "unix.rs"]
|
||||||
|
mod platform;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[path = "windows.rs"]
|
||||||
|
mod platform;
|
||||||
|
|
||||||
|
/// The `Fallback` trait allows for configurable font fallback lists to be set during construction of the [`FontSystem`].
|
||||||
|
///
|
||||||
|
/// A custom fallback list can be added via the [`FontSystem::new_with_locale_and_db_and_fallback`] constructor.
|
||||||
|
///
|
||||||
|
/// A default implementation is provided by the [`PlatformFallback`] struct, which encapsulates the target platform's pre-configured fallback lists.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use unicode_script::Script;
|
||||||
|
/// # use cosmic_text::{Fallback, FontSystem};
|
||||||
|
/// struct MyFallback;
|
||||||
|
/// impl Fallback for MyFallback {
|
||||||
|
/// fn common_fallback(&self) -> &[&'static str] {
|
||||||
|
/// &[
|
||||||
|
/// "Segoe UI",
|
||||||
|
/// "Segoe UI Emoji",
|
||||||
|
/// "Segoe UI Symbol",
|
||||||
|
/// "Segoe UI Historic",
|
||||||
|
/// ]
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn forbidden_fallback(&self) -> &[&'static str] {
|
||||||
|
/// &[]
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn script_fallback(&self, script: Script, locale: &str) -> &[&'static str] {
|
||||||
|
/// match script {
|
||||||
|
/// Script::Adlam => &["Ebrima"],
|
||||||
|
/// Script::Bengali => &["Nirmala UI"],
|
||||||
|
/// Script::Canadian_Aboriginal => &["Gadugi"],
|
||||||
|
/// // ...
|
||||||
|
/// _ => &[],
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let locale = "en-US".to_string();
|
||||||
|
/// let db = fontdb::Database::new();
|
||||||
|
/// let font_system = FontSystem::new_with_locale_and_db_and_fallback(locale, db, MyFallback);
|
||||||
|
/// ```
|
||||||
|
pub trait Fallback: Send + Sync {
|
||||||
|
/// Fallbacks to use after any script specific fallbacks
|
||||||
|
fn common_fallback(&self) -> &[&'static str];
|
||||||
|
|
||||||
|
/// Fallbacks to never use
|
||||||
|
fn forbidden_fallback(&self) -> &[&'static str];
|
||||||
|
|
||||||
|
/// Fallbacks to use per script
|
||||||
|
fn script_fallback(&self, script: Script, locale: &str) -> &[&'static str];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct Fallbacks {
|
||||||
|
lists: Vec<&'static str>,
|
||||||
|
common_fallback_range: Range<usize>,
|
||||||
|
forbidden_fallback_range: Range<usize>,
|
||||||
|
// PERF: Consider using NoHashHasher since Script is just an integer
|
||||||
|
script_fallback_ranges: HashMap<Script, Range<usize>>,
|
||||||
|
locale: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fallbacks {
|
||||||
|
pub(crate) fn new(fallbacks: &dyn Fallback, scripts: &[Script], locale: &str) -> Self {
|
||||||
|
let common_fallback = fallbacks.common_fallback();
|
||||||
|
|
||||||
|
let forbidden_fallback = fallbacks.forbidden_fallback();
|
||||||
|
|
||||||
|
let mut lists =
|
||||||
|
Vec::with_capacity(common_fallback.len() + forbidden_fallback.len() + scripts.len());
|
||||||
|
|
||||||
|
let mut index = lists.len();
|
||||||
|
let mut new_range = |lists: &Vec<&str>| {
|
||||||
|
let old_index = index;
|
||||||
|
index = lists.len();
|
||||||
|
old_index..index
|
||||||
|
};
|
||||||
|
|
||||||
|
lists.extend_from_slice(common_fallback);
|
||||||
|
let common_fallback_range = new_range(&lists);
|
||||||
|
|
||||||
|
lists.extend_from_slice(forbidden_fallback);
|
||||||
|
let forbidden_fallback_range = new_range(&lists);
|
||||||
|
|
||||||
|
let mut script_fallback_ranges =
|
||||||
|
HashMap::with_capacity_and_hasher(scripts.len(), BuildHasher::default());
|
||||||
|
for &script in scripts {
|
||||||
|
let script_fallback = fallbacks.script_fallback(script, locale);
|
||||||
|
lists.extend_from_slice(script_fallback);
|
||||||
|
let script_fallback_range = new_range(&lists);
|
||||||
|
script_fallback_ranges.insert(script, script_fallback_range);
|
||||||
|
}
|
||||||
|
|
||||||
|
let locale = locale.to_owned();
|
||||||
|
Self {
|
||||||
|
lists,
|
||||||
|
common_fallback_range,
|
||||||
|
forbidden_fallback_range,
|
||||||
|
script_fallback_ranges,
|
||||||
|
locale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn extend(&mut self, fallbacks: &dyn Fallback, scripts: &[Script]) {
|
||||||
|
self.lists.reserve(scripts.len());
|
||||||
|
|
||||||
|
let mut index = self.lists.len();
|
||||||
|
let mut new_range = |lists: &Vec<&str>| {
|
||||||
|
let old_index = index;
|
||||||
|
index = lists.len();
|
||||||
|
old_index..index
|
||||||
|
};
|
||||||
|
|
||||||
|
for &script in scripts {
|
||||||
|
self.script_fallback_ranges
|
||||||
|
.entry(script)
|
||||||
|
.or_insert_with_key(|&script| {
|
||||||
|
let script_fallback = fallbacks.script_fallback(script, &self.locale);
|
||||||
|
self.lists.extend_from_slice(script_fallback);
|
||||||
|
new_range(&self.lists)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn common_fallback(&self) -> &[&'static str] {
|
||||||
|
&self.lists[self.common_fallback_range.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn forbidden_fallback(&self) -> &[&'static str] {
|
||||||
|
&self.lists[self.forbidden_fallback_range.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn script_fallback(&self, script: Script) -> &[&'static str] {
|
||||||
|
self.script_fallback_ranges
|
||||||
|
.get(&script)
|
||||||
|
.map_or(&[], |range| &self.lists[range.clone()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use platform::PlatformFallback;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "warn_on_missing_glyphs"))]
|
||||||
|
use log::debug as missing_warn;
|
||||||
|
#[cfg(feature = "warn_on_missing_glyphs")]
|
||||||
|
use log::warn as missing_warn;
|
||||||
|
|
||||||
|
// Match on lowest font_weight_diff, then script_non_matches, then font_weight
|
||||||
|
// Default font gets None for both `weight_offset` and `script_non_matches`, and thus, it is
|
||||||
|
// always the first to be popped from the set.
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub(crate) struct MonospaceFallbackInfo {
|
||||||
|
font_weight_diff: Option<u16>,
|
||||||
|
codepoint_non_matches: Option<usize>,
|
||||||
|
font_weight: u16,
|
||||||
|
id: fontdb::ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FontFallbackIter<'a> {
|
||||||
|
font_system: &'a mut FontSystem,
|
||||||
|
font_match_keys: &'a [FontMatchKey],
|
||||||
|
skip_font_id: Option<fontdb::ID>,
|
||||||
|
default_families: &'a [&'a Family<'a>],
|
||||||
|
default_i: usize,
|
||||||
|
scripts: &'a [Script],
|
||||||
|
word: &'a str,
|
||||||
|
script_i: (usize, usize),
|
||||||
|
common_i: usize,
|
||||||
|
other_i: usize,
|
||||||
|
end: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FontFallbackIter<'a> {
|
||||||
|
pub fn new(
|
||||||
|
font_system: &'a mut FontSystem,
|
||||||
|
font_match_keys: &'a [FontMatchKey],
|
||||||
|
skip_font_id: Option<fontdb::ID>,
|
||||||
|
default_families: &'a [&'a Family<'a>],
|
||||||
|
scripts: &'a [Script],
|
||||||
|
word: &'a str,
|
||||||
|
) -> Self {
|
||||||
|
font_system
|
||||||
|
.fallbacks
|
||||||
|
.extend(font_system.dyn_fallback.as_ref(), scripts);
|
||||||
|
font_system.monospace_fallbacks_buffer.clear();
|
||||||
|
Self {
|
||||||
|
font_system,
|
||||||
|
font_match_keys,
|
||||||
|
skip_font_id,
|
||||||
|
default_families,
|
||||||
|
default_i: 0,
|
||||||
|
scripts,
|
||||||
|
word,
|
||||||
|
script_i: (0, 0),
|
||||||
|
common_i: 0,
|
||||||
|
other_i: 0,
|
||||||
|
end: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_missing(&mut self, word: &str) {
|
||||||
|
if self.end {
|
||||||
|
missing_warn!(
|
||||||
|
"Failed to find any fallback for {:?} locale '{}': '{}'",
|
||||||
|
self.scripts,
|
||||||
|
self.font_system.locale(),
|
||||||
|
word
|
||||||
|
);
|
||||||
|
} else if self.other_i > 0 {
|
||||||
|
missing_warn!(
|
||||||
|
"Failed to find preset fallback for {:?} locale '{}', used '{}': '{}'",
|
||||||
|
self.scripts,
|
||||||
|
self.font_system.locale(),
|
||||||
|
self.face_name(self.font_match_keys[self.other_i - 1].id),
|
||||||
|
word
|
||||||
|
);
|
||||||
|
} else if !self.scripts.is_empty() && self.common_i > 0 {
|
||||||
|
let family = self.font_system.fallbacks.common_fallback()[self.common_i - 1];
|
||||||
|
missing_warn!(
|
||||||
|
"Failed to find script fallback for {:?} locale '{}', used '{}': '{}'",
|
||||||
|
self.scripts,
|
||||||
|
self.font_system.locale(),
|
||||||
|
family,
|
||||||
|
word
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn face_name(&self, id: fontdb::ID) -> &str {
|
||||||
|
if let Some(face) = self.font_system.db().face(id) {
|
||||||
|
if let Some((name, _)) = face.families.first() {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
&face.post_script_name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"invalid font id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shape_caches(&mut self) -> &mut ShapeBuffer {
|
||||||
|
&mut self.font_system.shape_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face_contains_family(&self, id: fontdb::ID, family_name: &str) -> bool {
|
||||||
|
if let Some(face) = self.font_system.db().face(id) {
|
||||||
|
face.families.iter().any(|(name, _)| name == family_name)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_font_match_key(&self) -> Option<&FontMatchKey> {
|
||||||
|
let default_family = self.default_families[self.default_i - 1];
|
||||||
|
let default_family_name = self.font_system.db().family_name(default_family);
|
||||||
|
|
||||||
|
self.font_match_keys
|
||||||
|
.iter()
|
||||||
|
.filter(|m_key| m_key.font_weight_diff == 0)
|
||||||
|
.filter(|m_key| Some(m_key.id) != self.skip_font_id)
|
||||||
|
.find(|m_key| self.face_contains_family(m_key.id, default_family_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_item(&mut self, fallbacks: &Fallbacks) -> Option<<Self as Iterator>::Item> {
|
||||||
|
if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
|
||||||
|
if let Some(font) = self.font_system.get_font(fallback_info.id) {
|
||||||
|
return Some(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let font_match_keys_iter = |is_mono| {
|
||||||
|
self.font_match_keys
|
||||||
|
.iter()
|
||||||
|
.filter(move |m_key| m_key.font_weight_diff == 0 || is_mono)
|
||||||
|
.filter(|m_key| Some(m_key.id) != self.skip_font_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
'DEF_FAM: while self.default_i < self.default_families.len() {
|
||||||
|
self.default_i += 1;
|
||||||
|
let is_mono = self.default_families[self.default_i - 1] == &Family::Monospace;
|
||||||
|
let default_font_match_key = self.default_font_match_key().cloned();
|
||||||
|
let word_chars_count = self.word.chars().count();
|
||||||
|
|
||||||
|
macro_rules! mk_mono_fallback_info {
|
||||||
|
($m_key:expr) => {{
|
||||||
|
let supported_cp_count_opt = self
|
||||||
|
.font_system
|
||||||
|
.get_font_supported_codepoints_in_word($m_key.id, self.word);
|
||||||
|
|
||||||
|
supported_cp_count_opt.map(|supported_cp_count| {
|
||||||
|
let codepoint_non_matches = word_chars_count - supported_cp_count;
|
||||||
|
|
||||||
|
MonospaceFallbackInfo {
|
||||||
|
font_weight_diff: Some($m_key.font_weight_diff),
|
||||||
|
codepoint_non_matches: Some(codepoint_non_matches),
|
||||||
|
font_weight: $m_key.font_weight,
|
||||||
|
id: $m_key.id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
match (is_mono, default_font_match_key.as_ref()) {
|
||||||
|
(false, None) => break 'DEF_FAM,
|
||||||
|
(false, Some(m_key)) => {
|
||||||
|
if let Some(font) = self.font_system.get_font(m_key.id) {
|
||||||
|
return Some(font);
|
||||||
|
} else {
|
||||||
|
break 'DEF_FAM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(true, None) => (),
|
||||||
|
(true, Some(m_key)) => {
|
||||||
|
// Default Monospace font
|
||||||
|
if let Some(mut fallback_info) = mk_mono_fallback_info!(m_key) {
|
||||||
|
fallback_info.font_weight_diff = None;
|
||||||
|
|
||||||
|
// Return early if default Monospace font supports all word codepoints.
|
||||||
|
// Otherewise, add to fallbacks set
|
||||||
|
if fallback_info.codepoint_non_matches == Some(0) {
|
||||||
|
if let Some(font) = self.font_system.get_font(m_key.id) {
|
||||||
|
return Some(font);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert!(self
|
||||||
|
.font_system
|
||||||
|
.monospace_fallbacks_buffer
|
||||||
|
.insert(fallback_info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mono_ids_for_scripts = if is_mono && !self.scripts.is_empty() {
|
||||||
|
let scripts = self.scripts.iter().filter_map(|script| {
|
||||||
|
let script_as_lower = script.short_name().to_lowercase();
|
||||||
|
<[u8; 4]>::try_from(script_as_lower.as_bytes()).ok()
|
||||||
|
});
|
||||||
|
self.font_system.get_monospace_ids_for_scripts(scripts)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
for m_key in font_match_keys_iter(is_mono) {
|
||||||
|
if Some(m_key.id) != default_font_match_key.as_ref().map(|m_key| m_key.id) {
|
||||||
|
let is_mono_id = if mono_ids_for_scripts.is_empty() {
|
||||||
|
self.font_system.is_monospace(m_key.id)
|
||||||
|
} else {
|
||||||
|
mono_ids_for_scripts.binary_search(&m_key.id).is_ok()
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_mono_id {
|
||||||
|
let supported_cp_count_opt = self
|
||||||
|
.font_system
|
||||||
|
.get_font_supported_codepoints_in_word(m_key.id, self.word);
|
||||||
|
if let Some(supported_cp_count) = supported_cp_count_opt {
|
||||||
|
let codepoint_non_matches =
|
||||||
|
self.word.chars().count() - supported_cp_count;
|
||||||
|
|
||||||
|
let fallback_info = MonospaceFallbackInfo {
|
||||||
|
font_weight_diff: Some(m_key.font_weight_diff),
|
||||||
|
codepoint_non_matches: Some(codepoint_non_matches),
|
||||||
|
font_weight: m_key.font_weight,
|
||||||
|
id: m_key.id,
|
||||||
|
};
|
||||||
|
assert!(self
|
||||||
|
.font_system
|
||||||
|
.monospace_fallbacks_buffer
|
||||||
|
.insert(fallback_info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If default family is Monospace fallback to first monospaced font
|
||||||
|
if let Some(fallback_info) = self.font_system.monospace_fallbacks_buffer.pop_first() {
|
||||||
|
if let Some(font) = self.font_system.get_font(fallback_info.id) {
|
||||||
|
return Some(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while self.script_i.0 < self.scripts.len() {
|
||||||
|
let script = self.scripts[self.script_i.0];
|
||||||
|
|
||||||
|
let script_families = fallbacks.script_fallback(script);
|
||||||
|
|
||||||
|
while self.script_i.1 < script_families.len() {
|
||||||
|
let script_family = script_families[self.script_i.1];
|
||||||
|
self.script_i.1 += 1;
|
||||||
|
for m_key in font_match_keys_iter(false) {
|
||||||
|
if self.face_contains_family(m_key.id, script_family) {
|
||||||
|
if let Some(font) = self.font_system.get_font(m_key.id) {
|
||||||
|
return Some(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!(
|
||||||
|
"failed to find family '{}' for script {:?} and locale '{}'",
|
||||||
|
script_family,
|
||||||
|
script,
|
||||||
|
self.font_system.locale(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.script_i.0 += 1;
|
||||||
|
self.script_i.1 = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let common_families = fallbacks.common_fallback();
|
||||||
|
while self.common_i < common_families.len() {
|
||||||
|
let common_family = common_families[self.common_i];
|
||||||
|
self.common_i += 1;
|
||||||
|
for m_key in font_match_keys_iter(false) {
|
||||||
|
if self.face_contains_family(m_key.id, common_family) {
|
||||||
|
if let Some(font) = self.font_system.get_font(m_key.id) {
|
||||||
|
return Some(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!("failed to find family '{}'", common_family);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: do we need to do this?
|
||||||
|
//TODO: do not evaluate fonts more than once!
|
||||||
|
let forbidden_families = fallbacks.forbidden_fallback();
|
||||||
|
while self.other_i < self.font_match_keys.len() {
|
||||||
|
let id = self.font_match_keys[self.other_i].id;
|
||||||
|
self.other_i += 1;
|
||||||
|
if Some(id) == self.skip_font_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if forbidden_families
|
||||||
|
.iter()
|
||||||
|
.all(|family_name| !self.face_contains_family(id, family_name))
|
||||||
|
{
|
||||||
|
if let Some(font) = self.font_system.get_font(id) {
|
||||||
|
return Some(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.end = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for FontFallbackIter<'_> {
|
||||||
|
type Item = Arc<Font>;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let mut fallbacks = mem::take(&mut self.font_system.fallbacks);
|
||||||
|
let item = self.next_item(&fallbacks);
|
||||||
|
mem::swap(&mut fallbacks, &mut self.font_system.fallbacks);
|
||||||
|
item
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use unicode_script::Script;
|
||||||
|
|
||||||
|
use super::Fallback;
|
||||||
|
|
||||||
|
/// An empty platform-specific font fallback list.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PlatformFallback;
|
||||||
|
|
||||||
|
impl Fallback for PlatformFallback {
|
||||||
|
fn common_fallback(&self) -> &'static [&'static str] {
|
||||||
|
common_fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forbidden_fallback(&self) -> &'static [&'static str] {
|
||||||
|
forbidden_fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_fallback(
|
||||||
|
&self,
|
||||||
|
script: unicode_script::Script,
|
||||||
|
locale: &str,
|
||||||
|
) -> &'static [&'static str] {
|
||||||
|
script_fallback(script, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to use after any script specific fallbacks
|
||||||
|
fn common_fallback() -> &'static [&'static str] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to never use
|
||||||
|
fn forbidden_fallback() -> &'static [&'static str] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to use per script
|
||||||
|
fn script_fallback(_script: Script, _locale: &str) -> &'static [&'static str] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use unicode_script::Script;
|
||||||
|
|
||||||
|
use super::Fallback;
|
||||||
|
|
||||||
|
/// A platform-specific font fallback list, for Unix.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PlatformFallback;
|
||||||
|
|
||||||
|
impl Fallback for PlatformFallback {
|
||||||
|
fn common_fallback(&self) -> &'static [&'static str] {
|
||||||
|
common_fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forbidden_fallback(&self) -> &'static [&'static str] {
|
||||||
|
forbidden_fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_fallback(
|
||||||
|
&self,
|
||||||
|
script: unicode_script::Script,
|
||||||
|
locale: &str,
|
||||||
|
) -> &'static [&'static str] {
|
||||||
|
script_fallback(script, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to use after any script specific fallbacks
|
||||||
|
fn common_fallback() -> &'static [&'static str] {
|
||||||
|
//TODO: abstract style (sans/serif/monospaced)
|
||||||
|
&[
|
||||||
|
/* Sans-serif fallbacks */
|
||||||
|
"Noto Sans",
|
||||||
|
/* More sans-serif fallbacks */
|
||||||
|
"DejaVu Sans",
|
||||||
|
"FreeSans",
|
||||||
|
/* Mono fallbacks */
|
||||||
|
"Noto Sans Mono",
|
||||||
|
"DejaVu Sans Mono",
|
||||||
|
"FreeMono",
|
||||||
|
/* Symbols fallbacks */
|
||||||
|
"Noto Sans Symbols",
|
||||||
|
"Noto Sans Symbols2",
|
||||||
|
/* Emoji fallbacks*/
|
||||||
|
"Noto Color Emoji",
|
||||||
|
//TODO: Add CJK script here for doublewides?
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to never use
|
||||||
|
fn forbidden_fallback() -> &'static [&'static str] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||||
|
match locale {
|
||||||
|
// Japan
|
||||||
|
"ja" => &["Noto Sans CJK JP"],
|
||||||
|
// Korea
|
||||||
|
"ko" => &["Noto Sans CJK KR"],
|
||||||
|
// Hong Kong
|
||||||
|
"zh-HK" => &["Noto Sans CJK HK"],
|
||||||
|
// Taiwan
|
||||||
|
"zh-TW" => &["Noto Sans CJK TC"],
|
||||||
|
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||||
|
_ => &["Noto Sans CJK SC"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to use per script
|
||||||
|
fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||||
|
//TODO: abstract style (sans/serif/monospaced)
|
||||||
|
match script {
|
||||||
|
Script::Adlam => &["Noto Sans Adlam", "Noto Sans Adlam Unjoined"],
|
||||||
|
Script::Arabic => &["Noto Sans Arabic"],
|
||||||
|
Script::Armenian => &["Noto Sans Armenian"],
|
||||||
|
Script::Bengali => &["Noto Sans Bengali"],
|
||||||
|
Script::Bopomofo => han_unification(locale),
|
||||||
|
//TODO: DejaVu Sans would typically be selected for braille characters,
|
||||||
|
// but this breaks alignment when used alongside monospaced text.
|
||||||
|
// By requesting the use of FreeMono first, this issue can be avoided.
|
||||||
|
Script::Braille => &["FreeMono"],
|
||||||
|
Script::Buhid => &["Noto Sans Buhid"],
|
||||||
|
Script::Chakma => &["Noto Sans Chakma"],
|
||||||
|
Script::Cherokee => &["Noto Sans Cherokee"],
|
||||||
|
Script::Deseret => &["Noto Sans Deseret"],
|
||||||
|
Script::Devanagari => &["Noto Sans Devanagari"],
|
||||||
|
Script::Ethiopic => &["Noto Sans Ethiopic"],
|
||||||
|
Script::Georgian => &["Noto Sans Georgian"],
|
||||||
|
Script::Gothic => &["Noto Sans Gothic"],
|
||||||
|
Script::Grantha => &["Noto Sans Grantha"],
|
||||||
|
Script::Gujarati => &["Noto Sans Gujarati"],
|
||||||
|
Script::Gurmukhi => &["Noto Sans Gurmukhi"],
|
||||||
|
Script::Han => han_unification(locale),
|
||||||
|
Script::Hangul => han_unification("ko"),
|
||||||
|
Script::Hanunoo => &["Noto Sans Hanunoo"],
|
||||||
|
Script::Hebrew => &["Noto Sans Hebrew"],
|
||||||
|
Script::Hiragana => han_unification("ja"),
|
||||||
|
Script::Javanese => &["Noto Sans Javanese"],
|
||||||
|
Script::Kannada => &["Noto Sans Kannada"],
|
||||||
|
Script::Katakana => han_unification("ja"),
|
||||||
|
Script::Khmer => &["Noto Sans Khmer"],
|
||||||
|
Script::Lao => &["Noto Sans Lao"],
|
||||||
|
Script::Malayalam => &["Noto Sans Malayalam"],
|
||||||
|
Script::Mongolian => &["Noto Sans Mongolian"],
|
||||||
|
Script::Myanmar => &["Noto Sans Myanmar"],
|
||||||
|
Script::Oriya => &["Noto Sans Oriya"],
|
||||||
|
Script::Runic => &["Noto Sans Runic"],
|
||||||
|
Script::Sinhala => &["Noto Sans Sinhala"],
|
||||||
|
Script::Syriac => &["Noto Sans Syriac"],
|
||||||
|
Script::Tagalog => &["Noto Sans Tagalog"],
|
||||||
|
Script::Tagbanwa => &["Noto Sans Tagbanwa"],
|
||||||
|
Script::Tai_Le => &["Noto Sans Tai Le"],
|
||||||
|
Script::Tai_Tham => &["Noto Sans Tai Tham"],
|
||||||
|
Script::Tai_Viet => &["Noto Sans Tai Viet"],
|
||||||
|
Script::Tamil => &["Noto Sans Tamil"],
|
||||||
|
Script::Telugu => &["Noto Sans Telugu"],
|
||||||
|
Script::Thaana => &["Noto Sans Thaana"],
|
||||||
|
Script::Thai => &["Noto Sans Thai"],
|
||||||
|
//TODO: no sans script?
|
||||||
|
Script::Tibetan => &["Noto Serif Tibetan"],
|
||||||
|
Script::Tifinagh => &["Noto Sans Tifinagh"],
|
||||||
|
Script::Vai => &["Noto Sans Vai"],
|
||||||
|
//TODO: Use han_unification?
|
||||||
|
Script::Yi => &["Noto Sans Yi", "Noto Sans CJK SC"],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use unicode_script::Script;
|
||||||
|
|
||||||
|
use super::Fallback;
|
||||||
|
|
||||||
|
/// A platform-specific font fallback list, for Windows.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PlatformFallback;
|
||||||
|
|
||||||
|
impl Fallback for PlatformFallback {
|
||||||
|
fn common_fallback(&self) -> &'static [&'static str] {
|
||||||
|
common_fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forbidden_fallback(&self) -> &'static [&'static str] {
|
||||||
|
forbidden_fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script_fallback(
|
||||||
|
&self,
|
||||||
|
script: unicode_script::Script,
|
||||||
|
locale: &str,
|
||||||
|
) -> &'static [&'static str] {
|
||||||
|
script_fallback(script, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to use after any script specific fallbacks
|
||||||
|
fn common_fallback() -> &'static [&'static str] {
|
||||||
|
//TODO: abstract style (sans/serif/monospaced)
|
||||||
|
&[
|
||||||
|
"Segoe UI",
|
||||||
|
"Segoe UI Emoji",
|
||||||
|
"Segoe UI Symbol",
|
||||||
|
"Segoe UI Historic",
|
||||||
|
//TODO: Add CJK script here for doublewides?
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to never use
|
||||||
|
fn forbidden_fallback() -> &'static [&'static str] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||||
|
//TODO!
|
||||||
|
match locale {
|
||||||
|
// Japan
|
||||||
|
"ja" => &["Yu Gothic"],
|
||||||
|
// Korea
|
||||||
|
"ko" => &["Malgun Gothic"],
|
||||||
|
// Hong Kong"
|
||||||
|
"zh-HK" => &["MingLiU_HKSCS"],
|
||||||
|
// Taiwan
|
||||||
|
"zh-TW" => &["Microsoft JhengHei UI"],
|
||||||
|
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||||
|
_ => &["Microsoft YaHei UI"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks to use per script
|
||||||
|
fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
|
||||||
|
//TODO: better match https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc#L99
|
||||||
|
match script {
|
||||||
|
Script::Adlam => &["Ebrima"],
|
||||||
|
Script::Bengali => &["Nirmala UI"],
|
||||||
|
Script::Canadian_Aboriginal => &["Gadugi"],
|
||||||
|
Script::Chakma => &["Nirmala UI"],
|
||||||
|
Script::Cherokee => &["Gadugi"],
|
||||||
|
Script::Devanagari => &["Nirmala UI"],
|
||||||
|
Script::Ethiopic => &["Ebrima"],
|
||||||
|
Script::Gujarati => &["Nirmala UI"],
|
||||||
|
Script::Gurmukhi => &["Nirmala UI"],
|
||||||
|
Script::Han => han_unification(locale),
|
||||||
|
Script::Hangul => han_unification("ko"),
|
||||||
|
Script::Hiragana => han_unification("ja"),
|
||||||
|
Script::Javanese => &["Javanese Text"],
|
||||||
|
Script::Kannada => &["Nirmala UI"],
|
||||||
|
Script::Katakana => han_unification("ja"),
|
||||||
|
Script::Khmer => &["Leelawadee UI"],
|
||||||
|
Script::Lao => &["Leelawadee UI"],
|
||||||
|
Script::Malayalam => &["Nirmala UI"],
|
||||||
|
Script::Mongolian => &["Mongolian Baiti"],
|
||||||
|
Script::Myanmar => &["Myanmar Text"],
|
||||||
|
Script::Oriya => &["Nirmala UI"],
|
||||||
|
Script::Sinhala => &["Nirmala UI"],
|
||||||
|
Script::Tamil => &["Nirmala UI"],
|
||||||
|
Script::Telugu => &["Nirmala UI"],
|
||||||
|
Script::Thaana => &["MV Boli"],
|
||||||
|
Script::Thai => &["Leelawadee UI"],
|
||||||
|
Script::Tibetan => &["Microsoft Himalaya"],
|
||||||
|
Script::Tifinagh => &["Ebrima"],
|
||||||
|
Script::Vai => &["Ebrima"],
|
||||||
|
Script::Yi => &["Microsoft Yi Baiti"],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+218
@@ -0,0 +1,218 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
// re-export ttf_parser
|
||||||
|
pub use ttf_parser;
|
||||||
|
// re-export peniko::Font;
|
||||||
|
#[cfg(feature = "peniko")]
|
||||||
|
pub use peniko::Font as PenikoFont;
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
use rustybuzz::Face as RustybuzzFace;
|
||||||
|
use self_cell::self_cell;
|
||||||
|
|
||||||
|
pub(crate) mod fallback;
|
||||||
|
pub use fallback::{Fallback, PlatformFallback};
|
||||||
|
|
||||||
|
pub use self::system::*;
|
||||||
|
mod system;
|
||||||
|
|
||||||
|
self_cell!(
|
||||||
|
struct OwnedFace {
|
||||||
|
owner: Arc<dyn AsRef<[u8]> + Send + Sync>,
|
||||||
|
|
||||||
|
#[covariant]
|
||||||
|
dependent: RustybuzzFace,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
struct FontMonospaceFallback {
|
||||||
|
monospace_em_width: Option<f32>,
|
||||||
|
scripts: Vec<[u8; 4]>,
|
||||||
|
unicode_codepoints: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A font
|
||||||
|
pub struct Font {
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
swash: (u32, swash::CacheKey),
|
||||||
|
rustybuzz: OwnedFace,
|
||||||
|
#[cfg(not(feature = "peniko"))]
|
||||||
|
data: Arc<dyn AsRef<[u8]> + Send + Sync>,
|
||||||
|
#[cfg(feature = "peniko")]
|
||||||
|
data: peniko::Font,
|
||||||
|
id: fontdb::ID,
|
||||||
|
monospace_fallback: Option<FontMonospaceFallback>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Font {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("Font")
|
||||||
|
.field("id", &self.id)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Font {
|
||||||
|
pub fn id(&self) -> fontdb::ID {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn monospace_em_width(&self) -> Option<f32> {
|
||||||
|
self.monospace_fallback
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|x| x.monospace_em_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scripts(&self) -> &[[u8; 4]] {
|
||||||
|
self.monospace_fallback.as_ref().map_or(&[], |x| &x.scripts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unicode_codepoints(&self) -> &[u32] {
|
||||||
|
self.monospace_fallback
|
||||||
|
.as_ref()
|
||||||
|
.map_or(&[], |x| &x.unicode_codepoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data(&self) -> &[u8] {
|
||||||
|
#[cfg(not(feature = "peniko"))]
|
||||||
|
{
|
||||||
|
(*self.data).as_ref()
|
||||||
|
}
|
||||||
|
#[cfg(feature = "peniko")]
|
||||||
|
{
|
||||||
|
self.data.data.data()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rustybuzz(&self) -> &RustybuzzFace<'_> {
|
||||||
|
self.rustybuzz.borrow_dependent()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "peniko")]
|
||||||
|
pub fn as_peniko(&self) -> PenikoFont {
|
||||||
|
self.data.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
pub fn as_swash(&self) -> swash::FontRef<'_> {
|
||||||
|
let swash = &self.swash;
|
||||||
|
swash::FontRef {
|
||||||
|
data: self.data(),
|
||||||
|
offset: swash.0,
|
||||||
|
key: swash.1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Font {
|
||||||
|
pub fn new(db: &fontdb::Database, id: fontdb::ID) -> Option<Self> {
|
||||||
|
let info = db.face(id)?;
|
||||||
|
|
||||||
|
let monospace_fallback = if cfg!(feature = "monospace_fallback") {
|
||||||
|
db.with_face_data(id, |font_data, face_index| {
|
||||||
|
let face = ttf_parser::Face::parse(font_data, face_index).ok()?;
|
||||||
|
let monospace_em_width = info
|
||||||
|
.monospaced
|
||||||
|
.then(|| {
|
||||||
|
let hor_advance = face.glyph_hor_advance(face.glyph_index(' ')?)? as f32;
|
||||||
|
let upem = face.units_per_em() as f32;
|
||||||
|
Some(hor_advance / upem)
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
if info.monospaced && monospace_em_width.is_none() {
|
||||||
|
None?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scripts = face
|
||||||
|
.tables()
|
||||||
|
.gpos
|
||||||
|
.into_iter()
|
||||||
|
.chain(face.tables().gsub)
|
||||||
|
.flat_map(|table| table.scripts)
|
||||||
|
.map(|script| script.tag.to_bytes())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut unicode_codepoints = Vec::new();
|
||||||
|
|
||||||
|
face.tables()
|
||||||
|
.cmap?
|
||||||
|
.subtables
|
||||||
|
.into_iter()
|
||||||
|
.filter(|subtable| subtable.is_unicode())
|
||||||
|
.for_each(|subtable| {
|
||||||
|
unicode_codepoints.reserve(1024);
|
||||||
|
subtable.codepoints(|code_point| {
|
||||||
|
if subtable.glyph_index(code_point).is_some() {
|
||||||
|
unicode_codepoints.push(code_point);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
unicode_codepoints.shrink_to_fit();
|
||||||
|
|
||||||
|
Some(FontMonospaceFallback {
|
||||||
|
monospace_em_width,
|
||||||
|
scripts,
|
||||||
|
unicode_codepoints,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = match &info.source {
|
||||||
|
fontdb::Source::Binary(data) => Arc::clone(data),
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
fontdb::Source::File(path) => {
|
||||||
|
log::warn!("Unsupported fontdb Source::File('{}')", path.display());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
fontdb::Source::SharedFile(_path, data) => Arc::clone(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
id: info.id,
|
||||||
|
monospace_fallback,
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
swash: {
|
||||||
|
let swash = swash::FontRef::from_index((*data).as_ref(), info.index as usize)?;
|
||||||
|
(swash.offset, swash.key)
|
||||||
|
},
|
||||||
|
rustybuzz: OwnedFace::try_new(Arc::clone(&data), |data| {
|
||||||
|
RustybuzzFace::from_slice((**data).as_ref(), info.index).ok_or(())
|
||||||
|
})
|
||||||
|
.ok()?,
|
||||||
|
#[cfg(not(feature = "peniko"))]
|
||||||
|
data,
|
||||||
|
#[cfg(feature = "peniko")]
|
||||||
|
data: peniko::Font::new(peniko::Blob::new(data), info.index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
#[test]
|
||||||
|
fn test_fonts_load_time() {
|
||||||
|
use crate::FontSystem;
|
||||||
|
use sys_locale::get_locale;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
let mut db = fontdb::Database::new();
|
||||||
|
let locale = get_locale().expect("Local available");
|
||||||
|
db.load_system_fonts();
|
||||||
|
FontSystem::new_with_locale_and_db(locale, db);
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
println!("Fonts load time {}ms.", now.elapsed().as_millis());
|
||||||
|
}
|
||||||
|
}
|
||||||
+407
@@ -0,0 +1,407 @@
|
|||||||
|
use crate::{Attrs, Font, FontMatchAttrs, HashMap, ShapeBuffer};
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use alloc::collections::BTreeSet;
|
||||||
|
use alloc::string::String;
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use core::fmt;
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
// re-export fontdb and rustybuzz
|
||||||
|
pub use fontdb;
|
||||||
|
pub use rustybuzz;
|
||||||
|
|
||||||
|
use super::fallback::{Fallback, Fallbacks, MonospaceFallbackInfo, PlatformFallback};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct FontMatchKey {
|
||||||
|
pub(crate) font_weight_diff: u16,
|
||||||
|
pub(crate) font_weight: u16,
|
||||||
|
pub(crate) id: fontdb::ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FontCachedCodepointSupportInfo {
|
||||||
|
supported: Vec<u32>,
|
||||||
|
not_supported: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontCachedCodepointSupportInfo {
|
||||||
|
const SUPPORTED_MAX_SZ: usize = 512;
|
||||||
|
const NOT_SUPPORTED_MAX_SZ: usize = 1024;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
supported: Vec::with_capacity(Self::SUPPORTED_MAX_SZ),
|
||||||
|
not_supported: Vec::with_capacity(Self::NOT_SUPPORTED_MAX_SZ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn unknown_has_codepoint(
|
||||||
|
&mut self,
|
||||||
|
font_codepoints: &[u32],
|
||||||
|
codepoint: u32,
|
||||||
|
supported_insert_pos: usize,
|
||||||
|
not_supported_insert_pos: usize,
|
||||||
|
) -> bool {
|
||||||
|
let ret = font_codepoints.contains(&codepoint);
|
||||||
|
if ret {
|
||||||
|
// don't bother inserting if we are going to truncate the entry away
|
||||||
|
if supported_insert_pos != Self::SUPPORTED_MAX_SZ {
|
||||||
|
self.supported.insert(supported_insert_pos, codepoint);
|
||||||
|
self.supported.truncate(Self::SUPPORTED_MAX_SZ);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// don't bother inserting if we are going to truncate the entry away
|
||||||
|
if not_supported_insert_pos != Self::NOT_SUPPORTED_MAX_SZ {
|
||||||
|
self.not_supported
|
||||||
|
.insert(not_supported_insert_pos, codepoint);
|
||||||
|
self.not_supported.truncate(Self::NOT_SUPPORTED_MAX_SZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn has_codepoint(&mut self, font_codepoints: &[u32], codepoint: u32) -> bool {
|
||||||
|
match self.supported.binary_search(&codepoint) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(supported_insert_pos) => match self.not_supported.binary_search(&codepoint) {
|
||||||
|
Ok(_) => false,
|
||||||
|
Err(not_supported_insert_pos) => self.unknown_has_codepoint(
|
||||||
|
font_codepoints,
|
||||||
|
codepoint,
|
||||||
|
supported_insert_pos,
|
||||||
|
not_supported_insert_pos,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to the system fonts.
|
||||||
|
pub struct FontSystem {
|
||||||
|
/// The locale of the system.
|
||||||
|
locale: String,
|
||||||
|
|
||||||
|
/// The underlying font database.
|
||||||
|
db: fontdb::Database,
|
||||||
|
|
||||||
|
/// Cache for loaded fonts from the database.
|
||||||
|
font_cache: HashMap<fontdb::ID, Option<Arc<Font>>>,
|
||||||
|
|
||||||
|
/// Sorted unique ID's of all Monospace fonts in DB
|
||||||
|
monospace_font_ids: Vec<fontdb::ID>,
|
||||||
|
|
||||||
|
/// Sorted unique ID's of all Monospace fonts in DB per script.
|
||||||
|
/// A font may support multiple scripts of course, so the same ID
|
||||||
|
/// may appear in multiple map value vecs.
|
||||||
|
per_script_monospace_font_ids: HashMap<[u8; 4], Vec<fontdb::ID>>,
|
||||||
|
|
||||||
|
/// Cache for font codepoint support info
|
||||||
|
font_codepoint_support_info_cache: HashMap<fontdb::ID, FontCachedCodepointSupportInfo>,
|
||||||
|
|
||||||
|
/// Cache for font matches.
|
||||||
|
font_matches_cache: HashMap<FontMatchAttrs, Arc<Vec<FontMatchKey>>>,
|
||||||
|
|
||||||
|
/// Scratch buffer for shaping and laying out.
|
||||||
|
pub(crate) shape_buffer: ShapeBuffer,
|
||||||
|
|
||||||
|
/// Buffer for use in `FontFallbackIter`.
|
||||||
|
pub(crate) monospace_fallbacks_buffer: BTreeSet<MonospaceFallbackInfo>,
|
||||||
|
|
||||||
|
/// Cache for shaped runs
|
||||||
|
#[cfg(feature = "shape-run-cache")]
|
||||||
|
pub shape_run_cache: crate::ShapeRunCache,
|
||||||
|
|
||||||
|
/// List of fallbacks
|
||||||
|
pub(crate) dyn_fallback: Box<dyn Fallback>,
|
||||||
|
|
||||||
|
/// List of fallbacks
|
||||||
|
pub(crate) fallbacks: Fallbacks,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for FontSystem {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("FontSystem")
|
||||||
|
.field("locale", &self.locale)
|
||||||
|
.field("db", &self.db)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontSystem {
|
||||||
|
const FONT_MATCHES_CACHE_SIZE_LIMIT: usize = 256;
|
||||||
|
/// Create a new [`FontSystem`], that allows access to any installed system fonts
|
||||||
|
///
|
||||||
|
/// # Timing
|
||||||
|
///
|
||||||
|
/// This function takes some time to run. On the release build, it can take up to a second,
|
||||||
|
/// while debug builds can take up to ten times longer. For this reason, it should only be
|
||||||
|
/// called once, and the resulting [`FontSystem`] should be shared.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::new_with_fonts(core::iter::empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`FontSystem`] with a pre-specified set of fonts.
|
||||||
|
pub fn new_with_fonts(fonts: impl IntoIterator<Item = fontdb::Source>) -> Self {
|
||||||
|
let locale = Self::get_locale();
|
||||||
|
log::debug!("Locale: {}", locale);
|
||||||
|
|
||||||
|
let mut db = fontdb::Database::new();
|
||||||
|
|
||||||
|
Self::load_fonts(&mut db, fonts.into_iter());
|
||||||
|
|
||||||
|
//TODO: configurable default fonts
|
||||||
|
db.set_monospace_family("Noto Sans Mono");
|
||||||
|
db.set_sans_serif_family("Open Sans");
|
||||||
|
db.set_serif_family("DejaVu Serif");
|
||||||
|
|
||||||
|
Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`FontSystem`] with a pre-specified locale, font database and font fallback list.
|
||||||
|
pub fn new_with_locale_and_db_and_fallback(
|
||||||
|
locale: String,
|
||||||
|
db: fontdb::Database,
|
||||||
|
impl_fallback: impl Fallback + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let mut monospace_font_ids = db
|
||||||
|
.faces()
|
||||||
|
.filter(|face_info| {
|
||||||
|
face_info.monospaced && !face_info.post_script_name.contains("Emoji")
|
||||||
|
})
|
||||||
|
.map(|face_info| face_info.id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
monospace_font_ids.sort();
|
||||||
|
|
||||||
|
let mut per_script_monospace_font_ids: HashMap<[u8; 4], BTreeSet<fontdb::ID>> =
|
||||||
|
HashMap::default();
|
||||||
|
|
||||||
|
if cfg!(feature = "monospace_fallback") {
|
||||||
|
monospace_font_ids.iter().for_each(|&id| {
|
||||||
|
db.with_face_data(id, |font_data, face_index| {
|
||||||
|
let _ = ttf_parser::Face::parse(font_data, face_index).map(|face| {
|
||||||
|
face.tables()
|
||||||
|
.gpos
|
||||||
|
.into_iter()
|
||||||
|
.chain(face.tables().gsub)
|
||||||
|
.flat_map(|table| table.scripts)
|
||||||
|
.inspect(|script| {
|
||||||
|
per_script_monospace_font_ids
|
||||||
|
.entry(script.tag.to_bytes())
|
||||||
|
.or_default()
|
||||||
|
.insert(id);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let per_script_monospace_font_ids = per_script_monospace_font_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, Vec::from_iter(v)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let fallbacks = Fallbacks::new(&impl_fallback, &[], &locale);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
locale,
|
||||||
|
db,
|
||||||
|
monospace_font_ids,
|
||||||
|
per_script_monospace_font_ids,
|
||||||
|
font_cache: Default::default(),
|
||||||
|
font_matches_cache: Default::default(),
|
||||||
|
font_codepoint_support_info_cache: Default::default(),
|
||||||
|
monospace_fallbacks_buffer: BTreeSet::default(),
|
||||||
|
#[cfg(feature = "shape-run-cache")]
|
||||||
|
shape_run_cache: crate::ShapeRunCache::default(),
|
||||||
|
shape_buffer: ShapeBuffer::default(),
|
||||||
|
dyn_fallback: Box::new(impl_fallback),
|
||||||
|
fallbacks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`FontSystem`] with a pre-specified locale and font database.
|
||||||
|
pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self {
|
||||||
|
Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the locale.
|
||||||
|
pub fn locale(&self) -> &str {
|
||||||
|
&self.locale
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the database.
|
||||||
|
pub fn db(&self) -> &fontdb::Database {
|
||||||
|
&self.db
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to the database.
|
||||||
|
pub fn db_mut(&mut self) -> &mut fontdb::Database {
|
||||||
|
self.font_matches_cache.clear();
|
||||||
|
&mut self.db
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume this [`FontSystem`] and return the locale and database.
|
||||||
|
pub fn into_locale_and_db(self) -> (String, fontdb::Database) {
|
||||||
|
(self.locale, self.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a font by its ID.
|
||||||
|
pub fn get_font(&mut self, id: fontdb::ID) -> Option<Arc<Font>> {
|
||||||
|
self.font_cache
|
||||||
|
.entry(id)
|
||||||
|
.or_insert_with(|| {
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
unsafe {
|
||||||
|
self.db.make_shared_face_data(id);
|
||||||
|
}
|
||||||
|
match Font::new(&self.db, id) {
|
||||||
|
Some(font) => Some(Arc::new(font)),
|
||||||
|
None => {
|
||||||
|
log::warn!(
|
||||||
|
"failed to load font '{}'",
|
||||||
|
self.db.face(id)?.post_script_name
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_monospace(&self, id: fontdb::ID) -> bool {
|
||||||
|
self.monospace_font_ids.binary_search(&id).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_monospace_ids_for_scripts(
|
||||||
|
&self,
|
||||||
|
scripts: impl Iterator<Item = [u8; 4]>,
|
||||||
|
) -> Vec<fontdb::ID> {
|
||||||
|
let mut ret = scripts
|
||||||
|
.filter_map(|script| self.per_script_monospace_font_ids.get(&script))
|
||||||
|
.flat_map(|ids| ids.iter().copied())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ret.sort();
|
||||||
|
ret.dedup();
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_font_supported_codepoints_in_word(
|
||||||
|
&mut self,
|
||||||
|
id: fontdb::ID,
|
||||||
|
word: &str,
|
||||||
|
) -> Option<usize> {
|
||||||
|
self.get_font(id).map(|font| {
|
||||||
|
let code_points = font.unicode_codepoints();
|
||||||
|
let cache = self
|
||||||
|
.font_codepoint_support_info_cache
|
||||||
|
.entry(id)
|
||||||
|
.or_insert_with(FontCachedCodepointSupportInfo::new);
|
||||||
|
word.chars()
|
||||||
|
.filter(|ch| cache.has_codepoint(code_points, u32::from(*ch)))
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_font_matches(&mut self, attrs: &Attrs<'_>) -> Arc<Vec<FontMatchKey>> {
|
||||||
|
// Clear the cache first if it reached the size limit
|
||||||
|
if self.font_matches_cache.len() >= Self::FONT_MATCHES_CACHE_SIZE_LIMIT {
|
||||||
|
log::trace!("clear font mache cache");
|
||||||
|
self.font_matches_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.font_matches_cache
|
||||||
|
//TODO: do not create AttrsOwned unless entry does not already exist
|
||||||
|
.entry(attrs.into())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
let mut font_match_keys = self
|
||||||
|
.db
|
||||||
|
.faces()
|
||||||
|
.filter(|face| attrs.matches(face))
|
||||||
|
.map(|face| FontMatchKey {
|
||||||
|
font_weight_diff: attrs.weight.0.abs_diff(face.weight.0),
|
||||||
|
font_weight: face.weight.0,
|
||||||
|
id: face.id,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Sort so we get the keys with weight_offset=0 first
|
||||||
|
font_match_keys.sort();
|
||||||
|
|
||||||
|
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
||||||
|
{
|
||||||
|
let elapsed = now.elapsed();
|
||||||
|
log::debug!("font matches for {:?} in {:?}", attrs, elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Arc::new(font_match_keys)
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
fn get_locale() -> String {
|
||||||
|
sys_locale::get_locale().unwrap_or_else(|| {
|
||||||
|
log::warn!("failed to get system locale, falling back to en-US");
|
||||||
|
String::from("en-US")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
fn get_locale() -> String {
|
||||||
|
String::from("en-US")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
db.load_system_fonts();
|
||||||
|
|
||||||
|
for source in fonts {
|
||||||
|
db.load_font_source(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
log::debug!(
|
||||||
|
"Parsed {} font faces in {}ms.",
|
||||||
|
db.len(),
|
||||||
|
now.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
|
||||||
|
for source in fonts {
|
||||||
|
db.load_font_source(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value borrowed together with an [`FontSystem`]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BorrowedWithFontSystem<'a, T> {
|
||||||
|
pub(crate) inner: &'a mut T,
|
||||||
|
pub(crate) font_system: &'a mut FontSystem,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for BorrowedWithFontSystem<'_, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for BorrowedWithFontSystem<'_, T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
bitflags::bitflags! {
|
||||||
|
/// Flags that change rendering
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct CacheKeyFlags: u32 {
|
||||||
|
/// Skew by 14 degrees to synthesize italic
|
||||||
|
const FAKE_ITALIC = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Key for building a glyph cache
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct CacheKey {
|
||||||
|
/// Font ID
|
||||||
|
pub font_id: fontdb::ID,
|
||||||
|
/// Glyph ID
|
||||||
|
pub glyph_id: u16,
|
||||||
|
/// `f32` bits of font size
|
||||||
|
pub font_size_bits: u32,
|
||||||
|
/// Binning of fractional X offset
|
||||||
|
pub x_bin: SubpixelBin,
|
||||||
|
/// Binning of fractional Y offset
|
||||||
|
pub y_bin: SubpixelBin,
|
||||||
|
/// [`CacheKeyFlags`]
|
||||||
|
pub flags: CacheKeyFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheKey {
|
||||||
|
pub fn new(
|
||||||
|
font_id: fontdb::ID,
|
||||||
|
glyph_id: u16,
|
||||||
|
font_size: f32,
|
||||||
|
pos: (f32, f32),
|
||||||
|
flags: CacheKeyFlags,
|
||||||
|
) -> (Self, i32, i32) {
|
||||||
|
let (x, x_bin) = SubpixelBin::new(pos.0);
|
||||||
|
let (y, y_bin) = SubpixelBin::new(pos.1);
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
font_id,
|
||||||
|
glyph_id,
|
||||||
|
font_size_bits: font_size.to_bits(),
|
||||||
|
x_bin,
|
||||||
|
y_bin,
|
||||||
|
flags,
|
||||||
|
},
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Binning of subpixel position for cache optimization
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub enum SubpixelBin {
|
||||||
|
Zero,
|
||||||
|
One,
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubpixelBin {
|
||||||
|
pub fn new(pos: f32) -> (i32, Self) {
|
||||||
|
let trunc = pos as i32;
|
||||||
|
let fract = pos - trunc as f32;
|
||||||
|
|
||||||
|
if pos.is_sign_negative() {
|
||||||
|
if fract > -0.125 {
|
||||||
|
(trunc, Self::Zero)
|
||||||
|
} else if fract > -0.375 {
|
||||||
|
(trunc - 1, Self::Three)
|
||||||
|
} else if fract > -0.625 {
|
||||||
|
(trunc - 1, Self::Two)
|
||||||
|
} else if fract > -0.875 {
|
||||||
|
(trunc - 1, Self::One)
|
||||||
|
} else {
|
||||||
|
(trunc - 1, Self::Zero)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#[allow(clippy::collapsible_else_if)]
|
||||||
|
if fract < 0.125 {
|
||||||
|
(trunc, Self::Zero)
|
||||||
|
} else if fract < 0.375 {
|
||||||
|
(trunc, Self::One)
|
||||||
|
} else if fract < 0.625 {
|
||||||
|
(trunc, Self::Two)
|
||||||
|
} else if fract < 0.875 {
|
||||||
|
(trunc, Self::Three)
|
||||||
|
} else {
|
||||||
|
(trunc + 1, Self::Zero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_float(&self) -> f32 {
|
||||||
|
match self {
|
||||||
|
Self::Zero => 0.0,
|
||||||
|
Self::One => 0.25,
|
||||||
|
Self::Two => 0.5,
|
||||||
|
Self::Three => 0.75,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_subpixel_bins() {
|
||||||
|
// POSITIVE TESTS
|
||||||
|
|
||||||
|
// Maps to 0.0
|
||||||
|
assert_eq!(SubpixelBin::new(0.0), (0, SubpixelBin::Zero));
|
||||||
|
assert_eq!(SubpixelBin::new(0.124), (0, SubpixelBin::Zero));
|
||||||
|
|
||||||
|
// Maps to 0.25
|
||||||
|
assert_eq!(SubpixelBin::new(0.125), (0, SubpixelBin::One));
|
||||||
|
assert_eq!(SubpixelBin::new(0.25), (0, SubpixelBin::One));
|
||||||
|
assert_eq!(SubpixelBin::new(0.374), (0, SubpixelBin::One));
|
||||||
|
|
||||||
|
// Maps to 0.5
|
||||||
|
assert_eq!(SubpixelBin::new(0.375), (0, SubpixelBin::Two));
|
||||||
|
assert_eq!(SubpixelBin::new(0.5), (0, SubpixelBin::Two));
|
||||||
|
assert_eq!(SubpixelBin::new(0.624), (0, SubpixelBin::Two));
|
||||||
|
|
||||||
|
// Maps to 0.75
|
||||||
|
assert_eq!(SubpixelBin::new(0.625), (0, SubpixelBin::Three));
|
||||||
|
assert_eq!(SubpixelBin::new(0.75), (0, SubpixelBin::Three));
|
||||||
|
assert_eq!(SubpixelBin::new(0.874), (0, SubpixelBin::Three));
|
||||||
|
|
||||||
|
// Maps to 1.0
|
||||||
|
assert_eq!(SubpixelBin::new(0.875), (1, SubpixelBin::Zero));
|
||||||
|
assert_eq!(SubpixelBin::new(0.999), (1, SubpixelBin::Zero));
|
||||||
|
assert_eq!(SubpixelBin::new(1.0), (1, SubpixelBin::Zero));
|
||||||
|
assert_eq!(SubpixelBin::new(1.124), (1, SubpixelBin::Zero));
|
||||||
|
|
||||||
|
// NEGATIVE TESTS
|
||||||
|
|
||||||
|
// Maps to 0.0
|
||||||
|
assert_eq!(SubpixelBin::new(-0.0), (0, SubpixelBin::Zero));
|
||||||
|
assert_eq!(SubpixelBin::new(-0.124), (0, SubpixelBin::Zero));
|
||||||
|
|
||||||
|
// Maps to 0.25
|
||||||
|
assert_eq!(SubpixelBin::new(-0.125), (-1, SubpixelBin::Three));
|
||||||
|
assert_eq!(SubpixelBin::new(-0.25), (-1, SubpixelBin::Three));
|
||||||
|
assert_eq!(SubpixelBin::new(-0.374), (-1, SubpixelBin::Three));
|
||||||
|
|
||||||
|
// Maps to 0.5
|
||||||
|
assert_eq!(SubpixelBin::new(-0.375), (-1, SubpixelBin::Two));
|
||||||
|
assert_eq!(SubpixelBin::new(-0.5), (-1, SubpixelBin::Two));
|
||||||
|
assert_eq!(SubpixelBin::new(-0.624), (-1, SubpixelBin::Two));
|
||||||
|
|
||||||
|
// Maps to 0.75
|
||||||
|
assert_eq!(SubpixelBin::new(-0.625), (-1, SubpixelBin::One));
|
||||||
|
assert_eq!(SubpixelBin::new(-0.75), (-1, SubpixelBin::One));
|
||||||
|
assert_eq!(SubpixelBin::new(-0.874), (-1, SubpixelBin::One));
|
||||||
|
|
||||||
|
// Maps to 1.0
|
||||||
|
assert_eq!(SubpixelBin::new(-0.875), (-1, SubpixelBin::Zero));
|
||||||
|
assert_eq!(SubpixelBin::new(-0.999), (-1, SubpixelBin::Zero));
|
||||||
|
assert_eq!(SubpixelBin::new(-1.0), (-1, SubpixelBin::Zero));
|
||||||
|
assert_eq!(SubpixelBin::new(-1.124), (-1, SubpixelBin::Zero));
|
||||||
|
}
|
||||||
Vendored
+148
@@ -0,0 +1,148 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use core::fmt::Display;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
use crate::{math, CacheKey, CacheKeyFlags, Color};
|
||||||
|
|
||||||
|
/// A laid out glyph
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LayoutGlyph {
|
||||||
|
/// Start index of cluster in original line
|
||||||
|
pub start: usize,
|
||||||
|
/// End index of cluster in original line
|
||||||
|
pub end: usize,
|
||||||
|
/// Font size of the glyph
|
||||||
|
pub font_size: f32,
|
||||||
|
/// Line height of the glyph, will override buffer setting
|
||||||
|
pub line_height_opt: Option<f32>,
|
||||||
|
/// Font id of the glyph
|
||||||
|
pub font_id: fontdb::ID,
|
||||||
|
/// Font id of the glyph
|
||||||
|
pub glyph_id: u16,
|
||||||
|
/// X offset of hitbox
|
||||||
|
pub x: f32,
|
||||||
|
/// Y offset of hitbox
|
||||||
|
pub y: f32,
|
||||||
|
/// Width of hitbox
|
||||||
|
pub w: f32,
|
||||||
|
/// Unicode `BiDi` embedding level, character is left-to-right if `level` is divisible by 2
|
||||||
|
pub level: unicode_bidi::Level,
|
||||||
|
/// X offset in line
|
||||||
|
///
|
||||||
|
/// If you are dealing with physical coordinates, use [`Self::physical`] to obtain a
|
||||||
|
/// [`PhysicalGlyph`] for rendering.
|
||||||
|
///
|
||||||
|
/// This offset is useful when you are dealing with logical units and you do not care or
|
||||||
|
/// cannot guarantee pixel grid alignment. For instance, when you want to use the glyphs
|
||||||
|
/// for vectorial text, apply linear transformations to the layout, etc.
|
||||||
|
pub x_offset: f32,
|
||||||
|
/// Y offset in line
|
||||||
|
///
|
||||||
|
/// If you are dealing with physical coordinates, use [`Self::physical`] to obtain a
|
||||||
|
/// [`PhysicalGlyph`] for rendering.
|
||||||
|
///
|
||||||
|
/// This offset is useful when you are dealing with logical units and you do not care or
|
||||||
|
/// cannot guarantee pixel grid alignment. For instance, when you want to use the glyphs
|
||||||
|
/// for vectorial text, apply linear transformations to the layout, etc.
|
||||||
|
pub y_offset: f32,
|
||||||
|
/// Optional color override
|
||||||
|
pub color_opt: Option<Color>,
|
||||||
|
/// Metadata from `Attrs`
|
||||||
|
pub metadata: usize,
|
||||||
|
/// [`CacheKeyFlags`]
|
||||||
|
pub cache_key_flags: CacheKeyFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PhysicalGlyph {
|
||||||
|
/// Cache key, see [`CacheKey`]
|
||||||
|
pub cache_key: CacheKey,
|
||||||
|
/// Integer component of X offset in line
|
||||||
|
pub x: i32,
|
||||||
|
/// Integer component of Y offset in line
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutGlyph {
|
||||||
|
pub fn physical(&self, offset: (f32, f32), scale: f32) -> PhysicalGlyph {
|
||||||
|
let x_offset = self.font_size * self.x_offset;
|
||||||
|
let y_offset = self.font_size * self.y_offset;
|
||||||
|
|
||||||
|
let (cache_key, x, y) = CacheKey::new(
|
||||||
|
self.font_id,
|
||||||
|
self.glyph_id,
|
||||||
|
self.font_size * scale,
|
||||||
|
(
|
||||||
|
(self.x + x_offset) * scale + offset.0,
|
||||||
|
math::truncf((self.y - y_offset) * scale + offset.1), // Hinting in Y axis
|
||||||
|
),
|
||||||
|
self.cache_key_flags,
|
||||||
|
);
|
||||||
|
|
||||||
|
PhysicalGlyph { cache_key, x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A line of laid out glyphs
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LayoutLine {
|
||||||
|
/// Width of the line
|
||||||
|
pub w: f32,
|
||||||
|
/// Maximum ascent of the glyphs in line
|
||||||
|
pub max_ascent: f32,
|
||||||
|
/// Maximum descent of the glyphs in line
|
||||||
|
pub max_descent: f32,
|
||||||
|
/// Maximum line height of any spans in line
|
||||||
|
pub line_height_opt: Option<f32>,
|
||||||
|
/// Glyphs in line
|
||||||
|
pub glyphs: Vec<LayoutGlyph>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapping mode
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||||
|
pub enum Wrap {
|
||||||
|
/// No wrapping
|
||||||
|
None,
|
||||||
|
/// Wraps at a glyph level
|
||||||
|
Glyph,
|
||||||
|
/// Wraps at the word level
|
||||||
|
Word,
|
||||||
|
/// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself
|
||||||
|
WordOrGlyph,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Wrap {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::None => write!(f, "No Wrap"),
|
||||||
|
Self::Word => write!(f, "Word Wrap"),
|
||||||
|
Self::WordOrGlyph => write!(f, "Word Wrap or Character"),
|
||||||
|
Self::Glyph => write!(f, "Character"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Align or justify
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||||
|
pub enum Align {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Center,
|
||||||
|
Justified,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Align {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Left => write!(f, "Left"),
|
||||||
|
Self::Right => write!(f, "Right"),
|
||||||
|
Self::Center => write!(f, "Center"),
|
||||||
|
Self::Justified => write!(f, "Justified"),
|
||||||
|
Self::End => write!(f, "End"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+150
@@ -0,0 +1,150 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
//! # COSMIC Text
|
||||||
|
//!
|
||||||
|
//! This library provides advanced text handling in a generic way. It provides abstractions for
|
||||||
|
//! shaping, font discovery, font fallback, layout, rasterization, and editing. Shaping utilizes
|
||||||
|
//! rustybuzz, font discovery utilizes fontdb, and the rasterization is optional and utilizes
|
||||||
|
//! swash. The other features are developed internal to this library.
|
||||||
|
//!
|
||||||
|
//! It is recommended that you start by creating a [`FontSystem`], after which you can create a
|
||||||
|
//! [`Buffer`], provide it with some text, and then inspect the layout it produces. At this
|
||||||
|
//! point, you can use the `SwashCache` to rasterize glyphs into either images or pixels.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics, Shaping};
|
||||||
|
//!
|
||||||
|
//! // A FontSystem provides access to detected system fonts, create one per application
|
||||||
|
//! let mut font_system = FontSystem::new();
|
||||||
|
//!
|
||||||
|
//! // A SwashCache stores rasterized glyphs, create one per application
|
||||||
|
//! let mut swash_cache = SwashCache::new();
|
||||||
|
//!
|
||||||
|
//! // Text metrics indicate the font size and line height of a buffer
|
||||||
|
//! let metrics = Metrics::new(14.0, 20.0);
|
||||||
|
//!
|
||||||
|
//! // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget
|
||||||
|
//! let mut buffer = Buffer::new(&mut font_system, metrics);
|
||||||
|
//!
|
||||||
|
//! // Borrow buffer together with the font system for more convenient method calls
|
||||||
|
//! let mut buffer = buffer.borrow_with(&mut font_system);
|
||||||
|
//!
|
||||||
|
//! // Set a size for the text buffer, in pixels
|
||||||
|
//! buffer.set_size(Some(80.0), Some(25.0));
|
||||||
|
//!
|
||||||
|
//! // Attributes indicate what font to choose
|
||||||
|
//! let attrs = Attrs::new();
|
||||||
|
//!
|
||||||
|
//! // Add some text!
|
||||||
|
//! buffer.set_text("Hello, Rust! 🦀\n", &attrs, Shaping::Advanced);
|
||||||
|
//!
|
||||||
|
//! // Perform shaping as desired
|
||||||
|
//! buffer.shape_until_scroll(true);
|
||||||
|
//!
|
||||||
|
//! // Inspect the output runs
|
||||||
|
//! for run in buffer.layout_runs() {
|
||||||
|
//! for glyph in run.glyphs.iter() {
|
||||||
|
//! println!("{:#?}", glyph);
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Create a default text color
|
||||||
|
//! let text_color = Color::rgb(0xFF, 0xFF, 0xFF);
|
||||||
|
//!
|
||||||
|
//! // Draw the buffer (for performance, instead use SwashCache directly)
|
||||||
|
//! buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| {
|
||||||
|
//! // Fill in your code here for drawing rectangles
|
||||||
|
//! });
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
// Not interested in these lints
|
||||||
|
#![allow(clippy::new_without_default)]
|
||||||
|
// TODO: address occurrences and then deny
|
||||||
|
//
|
||||||
|
// Overflows can produce unpredictable results and are only checked in debug builds
|
||||||
|
#![allow(clippy::arithmetic_side_effects)]
|
||||||
|
// Indexing a slice can cause panics and that is something we always want to avoid
|
||||||
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
// Soundness issues
|
||||||
|
//
|
||||||
|
// Dereferencing unaligned pointers may be undefined behavior
|
||||||
|
#![deny(clippy::cast_ptr_alignment)]
|
||||||
|
// Avoid panicking in without information about the panic. Use expect
|
||||||
|
#![deny(clippy::unwrap_used)]
|
||||||
|
// Ensure all types have a debug impl
|
||||||
|
#![deny(missing_debug_implementations)]
|
||||||
|
// This is usually a serious issue - a missing import of a define where it is interpreted
|
||||||
|
// as a catch-all variable in a match, for example
|
||||||
|
#![deny(unreachable_patterns)]
|
||||||
|
// Ensure that all must_use results are used
|
||||||
|
#![deny(unused_must_use)]
|
||||||
|
// Style issues
|
||||||
|
//
|
||||||
|
// Documentation not ideal
|
||||||
|
#![warn(clippy::doc_markdown)]
|
||||||
|
// Document possible errors
|
||||||
|
#![warn(clippy::missing_errors_doc)]
|
||||||
|
// Document possible panics
|
||||||
|
#![warn(clippy::missing_panics_doc)]
|
||||||
|
// Ensure semicolons are present
|
||||||
|
#![warn(clippy::semicolon_if_nothing_returned)]
|
||||||
|
// Ensure numbers are readable
|
||||||
|
#![warn(clippy::unreadable_literal)]
|
||||||
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "std", feature = "no_std")))]
|
||||||
|
compile_error!("Either the `std` or `no_std` feature must be enabled");
|
||||||
|
|
||||||
|
pub use self::attrs::*;
|
||||||
|
mod attrs;
|
||||||
|
|
||||||
|
pub use self::bidi_para::*;
|
||||||
|
mod bidi_para;
|
||||||
|
|
||||||
|
pub use self::buffer::*;
|
||||||
|
mod buffer;
|
||||||
|
|
||||||
|
pub use self::buffer_line::*;
|
||||||
|
mod buffer_line;
|
||||||
|
|
||||||
|
pub use self::cached::*;
|
||||||
|
mod cached;
|
||||||
|
|
||||||
|
pub use self::glyph_cache::*;
|
||||||
|
mod glyph_cache;
|
||||||
|
|
||||||
|
pub use self::cursor::*;
|
||||||
|
mod cursor;
|
||||||
|
|
||||||
|
pub use self::edit::*;
|
||||||
|
mod edit;
|
||||||
|
|
||||||
|
pub use self::font::*;
|
||||||
|
mod font;
|
||||||
|
|
||||||
|
pub use self::layout::*;
|
||||||
|
mod layout;
|
||||||
|
|
||||||
|
pub use self::line_ending::*;
|
||||||
|
mod line_ending;
|
||||||
|
|
||||||
|
pub use self::shape::*;
|
||||||
|
mod shape;
|
||||||
|
|
||||||
|
pub use self::shape_run_cache::*;
|
||||||
|
mod shape_run_cache;
|
||||||
|
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
pub use self::swash::*;
|
||||||
|
#[cfg(feature = "swash")]
|
||||||
|
mod swash;
|
||||||
|
|
||||||
|
mod math;
|
||||||
|
|
||||||
|
type BuildHasher = core::hash::BuildHasherDefault<rustc_hash::FxHasher>;
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
type HashMap<K, V> = std::collections::HashMap<K, V, BuildHasher>;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasher>;
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
use core::ops::Range;
|
||||||
|
|
||||||
|
/// Line ending
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum LineEnding {
|
||||||
|
/// Use `\n` for line ending (POSIX-style)
|
||||||
|
#[default]
|
||||||
|
Lf,
|
||||||
|
/// Use `\r\n` for line ending (Windows-style)
|
||||||
|
CrLf,
|
||||||
|
/// Use `\r` for line ending (many legacy systems)
|
||||||
|
Cr,
|
||||||
|
/// Use `\n\r` for line ending (some legacy systems)
|
||||||
|
LfCr,
|
||||||
|
/// No line ending
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineEnding {
|
||||||
|
/// Get the line ending as a str
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Lf => "\n",
|
||||||
|
Self::CrLf => "\r\n",
|
||||||
|
Self::Cr => "\r",
|
||||||
|
Self::LfCr => "\n\r",
|
||||||
|
Self::None => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over lines terminated by [`LineEnding`]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LineIter<'a> {
|
||||||
|
string: &'a str,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LineIter<'a> {
|
||||||
|
/// Create an iterator of lines in a string slice
|
||||||
|
pub fn new(string: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
string,
|
||||||
|
start: 0,
|
||||||
|
end: string.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for LineIter<'_> {
|
||||||
|
type Item = (Range<usize>, LineEnding);
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let start = self.start;
|
||||||
|
match self.string[start..self.end].find(['\r', '\n']) {
|
||||||
|
Some(i) => {
|
||||||
|
let end = start + i;
|
||||||
|
self.start = end;
|
||||||
|
let after = &self.string[end..];
|
||||||
|
let ending = if after.starts_with("\r\n") {
|
||||||
|
LineEnding::CrLf
|
||||||
|
} else if after.starts_with("\n\r") {
|
||||||
|
LineEnding::LfCr
|
||||||
|
} else if after.starts_with("\n") {
|
||||||
|
LineEnding::Lf
|
||||||
|
} else if after.starts_with("\r") {
|
||||||
|
LineEnding::Cr
|
||||||
|
} else {
|
||||||
|
//TODO: this should not be possible
|
||||||
|
LineEnding::None
|
||||||
|
};
|
||||||
|
self.start += ending.as_str().len();
|
||||||
|
Some((start..end, ending))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if self.start < self.end {
|
||||||
|
self.start = self.end;
|
||||||
|
Some((start..self.end, LineEnding::None))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: DoubleEndedIterator
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line_iter() {
|
||||||
|
let string = "LF\nCRLF\r\nCR\rLFCR\n\rNONE";
|
||||||
|
let mut iter = LineIter::new(string);
|
||||||
|
assert_eq!(iter.next(), Some((0..2, LineEnding::Lf)));
|
||||||
|
assert_eq!(iter.next(), Some((3..7, LineEnding::CrLf)));
|
||||||
|
assert_eq!(iter.next(), Some((9..11, LineEnding::Cr)));
|
||||||
|
assert_eq!(iter.next(), Some((12..16, LineEnding::LfCr)));
|
||||||
|
assert_eq!(iter.next(), Some((18..22, LineEnding::None)));
|
||||||
|
}
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
pub use libm::{floorf, roundf, truncf};
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[inline]
|
||||||
|
pub fn floorf(x: f32) -> f32 {
|
||||||
|
x.floor()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[inline]
|
||||||
|
pub fn roundf(x: f32) -> f32 {
|
||||||
|
x.round()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[inline]
|
||||||
|
pub fn truncf(x: f32) -> f32 {
|
||||||
|
x.trunc()
|
||||||
|
}
|
||||||
Vendored
+1953
File diff suppressed because it is too large.
Load diff
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
use core::ops::Range;
|
||||||
|
|
||||||
|
use crate::{AttrsOwned, HashMap, ShapeGlyph};
|
||||||
|
|
||||||
|
/// Key for caching shape runs.
|
||||||
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||||
|
pub struct ShapeRunKey {
|
||||||
|
pub text: String,
|
||||||
|
pub default_attrs: AttrsOwned,
|
||||||
|
pub attrs_spans: Vec<(Range<usize>, AttrsOwned)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper structure for caching shape runs.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct ShapeRunCache {
|
||||||
|
age: u64,
|
||||||
|
cache: HashMap<ShapeRunKey, (u64, Vec<ShapeGlyph>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShapeRunCache {
|
||||||
|
/// Get cache item, updating age if found
|
||||||
|
pub fn get(&mut self, key: &ShapeRunKey) -> Option<&Vec<ShapeGlyph>> {
|
||||||
|
self.cache.get_mut(key).map(|(age, glyphs)| {
|
||||||
|
*age = self.age;
|
||||||
|
&*glyphs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert cache item with current age
|
||||||
|
pub fn insert(&mut self, key: ShapeRunKey, glyphs: Vec<ShapeGlyph>) {
|
||||||
|
self.cache.insert(key, (self.age, glyphs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove anything in the cache with an age older than `keep_ages`
|
||||||
|
pub fn trim(&mut self, keep_ages: u64) {
|
||||||
|
self.cache
|
||||||
|
.retain(|_key, (age, _glyphs)| *age + keep_ages >= self.age);
|
||||||
|
// Increase age
|
||||||
|
self.age += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for ShapeRunCache {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.debug_tuple("ShapeRunCache").finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+214
@@ -0,0 +1,214 @@
|
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use core::fmt;
|
||||||
|
use swash::scale::{image::Content, ScaleContext};
|
||||||
|
use swash::scale::{Render, Source, StrikeWith};
|
||||||
|
use swash::zeno::{Format, Vector};
|
||||||
|
|
||||||
|
use crate::{CacheKey, CacheKeyFlags, Color, FontSystem, HashMap};
|
||||||
|
|
||||||
|
pub use swash::scale::image::{Content as SwashContent, Image as SwashImage};
|
||||||
|
pub use swash::zeno::{Angle, Command, Placement, Transform};
|
||||||
|
|
||||||
|
fn swash_image(
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
context: &mut ScaleContext,
|
||||||
|
cache_key: CacheKey,
|
||||||
|
) -> Option<SwashImage> {
|
||||||
|
let font = match font_system.get_font(cache_key.font_id) {
|
||||||
|
Some(some) => some,
|
||||||
|
None => {
|
||||||
|
log::warn!("did not find font {:?}", cache_key.font_id);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the scaler
|
||||||
|
let mut scaler = context
|
||||||
|
.builder(font.as_swash())
|
||||||
|
.size(f32::from_bits(cache_key.font_size_bits))
|
||||||
|
.hint(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Compute the fractional offset-- you'll likely want to quantize this
|
||||||
|
// in a real renderer
|
||||||
|
let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float());
|
||||||
|
|
||||||
|
// Select our source order
|
||||||
|
Render::new(&[
|
||||||
|
// Color outline with the first palette
|
||||||
|
Source::ColorOutline(0),
|
||||||
|
// Color bitmap with best fit selection mode
|
||||||
|
Source::ColorBitmap(StrikeWith::BestFit),
|
||||||
|
// Standard scalable outline
|
||||||
|
Source::Outline,
|
||||||
|
])
|
||||||
|
// Select a subpixel format
|
||||||
|
.format(Format::Alpha)
|
||||||
|
// Apply the fractional offset
|
||||||
|
.offset(offset)
|
||||||
|
.transform(if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) {
|
||||||
|
Some(Transform::skew(
|
||||||
|
Angle::from_degrees(14.0),
|
||||||
|
Angle::from_degrees(0.0),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
// Render the image
|
||||||
|
.render(&mut scaler, cache_key.glyph_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn swash_outline_commands(
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
context: &mut ScaleContext,
|
||||||
|
cache_key: CacheKey,
|
||||||
|
) -> Option<Box<[swash::zeno::Command]>> {
|
||||||
|
use swash::zeno::PathData as _;
|
||||||
|
|
||||||
|
let font = match font_system.get_font(cache_key.font_id) {
|
||||||
|
Some(some) => some,
|
||||||
|
None => {
|
||||||
|
log::warn!("did not find font {:?}", cache_key.font_id);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the scaler
|
||||||
|
let mut scaler = context
|
||||||
|
.builder(font.as_swash())
|
||||||
|
.size(f32::from_bits(cache_key.font_size_bits))
|
||||||
|
.hint(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Scale the outline
|
||||||
|
let outline = scaler
|
||||||
|
.scale_outline(cache_key.glyph_id)
|
||||||
|
.or_else(|| scaler.scale_color_outline(cache_key.glyph_id))?;
|
||||||
|
|
||||||
|
// Get the path information of the outline
|
||||||
|
let path = outline.path();
|
||||||
|
|
||||||
|
// Return the commands
|
||||||
|
Some(path.commands().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cache for rasterizing with the swash scaler
|
||||||
|
pub struct SwashCache {
|
||||||
|
context: ScaleContext,
|
||||||
|
pub image_cache: HashMap<CacheKey, Option<SwashImage>>,
|
||||||
|
pub outline_command_cache: HashMap<CacheKey, Option<Box<[swash::zeno::Command]>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for SwashCache {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.pad("SwashCache { .. }")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwashCache {
|
||||||
|
/// Create a new swash cache
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
context: ScaleContext::new(),
|
||||||
|
image_cache: HashMap::default(),
|
||||||
|
outline_command_cache: HashMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a swash Image from a cache key, without caching results
|
||||||
|
pub fn get_image_uncached(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
cache_key: CacheKey,
|
||||||
|
) -> Option<SwashImage> {
|
||||||
|
swash_image(font_system, &mut self.context, cache_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a swash Image from a cache key, caching results
|
||||||
|
pub fn get_image(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
cache_key: CacheKey,
|
||||||
|
) -> &Option<SwashImage> {
|
||||||
|
self.image_cache
|
||||||
|
.entry(cache_key)
|
||||||
|
.or_insert_with(|| swash_image(font_system, &mut self.context, cache_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates outline commands
|
||||||
|
pub fn get_outline_commands(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
cache_key: CacheKey,
|
||||||
|
) -> Option<&[swash::zeno::Command]> {
|
||||||
|
self.outline_command_cache
|
||||||
|
.entry(cache_key)
|
||||||
|
.or_insert_with(|| swash_outline_commands(font_system, &mut self.context, cache_key))
|
||||||
|
.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates outline commands, without caching results
|
||||||
|
pub fn get_outline_commands_uncached(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
cache_key: CacheKey,
|
||||||
|
) -> Option<Box<[swash::zeno::Command]>> {
|
||||||
|
swash_outline_commands(font_system, &mut self.context, cache_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumerate pixels in an Image, use `with_image` for better performance
|
||||||
|
pub fn with_pixels<F: FnMut(i32, i32, Color)>(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
cache_key: CacheKey,
|
||||||
|
base: Color,
|
||||||
|
mut f: F,
|
||||||
|
) {
|
||||||
|
if let Some(image) = self.get_image(font_system, cache_key) {
|
||||||
|
let x = image.placement.left;
|
||||||
|
let y = -image.placement.top;
|
||||||
|
|
||||||
|
match image.content {
|
||||||
|
Content::Mask => {
|
||||||
|
let mut i = 0;
|
||||||
|
for off_y in 0..image.placement.height as i32 {
|
||||||
|
for off_x in 0..image.placement.width as i32 {
|
||||||
|
//TODO: blend base alpha?
|
||||||
|
f(
|
||||||
|
x + off_x,
|
||||||
|
y + off_y,
|
||||||
|
Color(((image.data[i] as u32) << 24) | base.0 & 0xFF_FF_FF),
|
||||||
|
);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Content::Color => {
|
||||||
|
let mut i = 0;
|
||||||
|
for off_y in 0..image.placement.height as i32 {
|
||||||
|
for off_x in 0..image.placement.width as i32 {
|
||||||
|
//TODO: blend base alpha?
|
||||||
|
f(
|
||||||
|
x + off_x,
|
||||||
|
y + off_y,
|
||||||
|
Color::rgba(
|
||||||
|
image.data[i],
|
||||||
|
image.data[i + 1],
|
||||||
|
image.data[i + 2],
|
||||||
|
image.data[i + 3],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Content::SubpixelMask => {
|
||||||
|
log::warn!("TODO: SubpixelMask");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user