diff --git a/Cargo.lock b/Cargo.lock index 91ebe82..8dbaac1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,8 +584,6 @@ dependencies = [ [[package]] name = "cosmic-text" version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ "bitflags", "fontdb 0.16.2", diff --git a/Cargo.toml b/Cargo.toml index 3cfad81..b3d903e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,9 @@ base64 = "0.22" [patch.crates-io] # Keep the grid shrink-to-fit correction local until it lands upstream. 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 # their bodies in std::panic::catch_unwind so a panic degrades to an error return diff --git a/crates/obscura-render/Cargo.toml b/crates/obscura-render/Cargo.toml index 21ffbca..77d66b1 100644 --- a/crates/obscura-render/Cargo.toml +++ b/crates/obscura-render/Cargo.toml @@ -23,8 +23,8 @@ ab_glyph = { workspace = true, optional = true } # 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 } # Keep this exactly aligned with cosmic-text 0.14's raster backend. Cosmic -# does not expose variation coordinates, so variable font instances are -# rasterized through Swash directly while Cosmic remains the shaper. +# and the renderer consume the same canonical variation coordinates for +# shaping and rasterization. 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 } ureq = { version = "2.9", default-features = false, features = ["tls"], optional = true } diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index 0394078..6cbc723 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -1076,6 +1076,8 @@ pub(crate) fn layout_dom_with_web_fonts( font_size: Option, font_weight: u16, font_family: Option, + font_optical_sizing: crate::FontOpticalSizing, + font_variation_settings: Vec, letter_spacing: f32, letter_spacing_non_normal: bool, container_type: crate::ContainerType, @@ -1110,6 +1112,8 @@ pub(crate) fn layout_dom_with_web_fonts( font_size: None, font_weight: 400, font_family: None, + font_optical_sizing: crate::FontOpticalSizing::Auto, + font_variation_settings: Vec::new(), letter_spacing: 0.0, letter_spacing_non_normal: false, 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()); 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_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 .get_node(id) .map_or(false, |node| { @@ -1566,6 +1581,8 @@ pub(crate) fn layout_dom_with_web_fonts( style.letter_spacing_non_normal.unwrap_or(false); let host_weight = crate::style::used_font_weight(style); 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_transform = style.text_transform; 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() { 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(resolved) = crate::style::resolve_contextual_length( expression, @@ -7334,6 +7358,79 @@ mod tests { 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#" +
+ + +
"#, + ); + 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] fn native_select_sizes_for_its_widest_option() { let tree = parse_html( diff --git a/crates/obscura-render/src/inline.rs b/crates/obscura-render/src/inline.rs index d2c0bd5..4d47cee 100644 --- a/crates/obscura-render/src/inline.rs +++ b/crates/obscura-render/src/inline.rs @@ -16,7 +16,8 @@ use std::{collections::HashMap, sync::Arc}; use cosmic_text::{ 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::zeno::{Angle, Format, Transform, Vector}; @@ -91,19 +92,23 @@ struct LoadedFamily { #[derive(Clone)] struct LoadedFace { name: Arc, + font_id: Option, min_weight: u16, max_weight: u16, shape_weight: u16, italic: bool, + has_optical_size: bool, } #[derive(Clone)] struct ResolvedFont { family: Arc, + font_id: Option, shape_weight: u16, - /// Authored `wght` coordinate for a ranged @font-face. Cosmic Text 0.12 - /// cannot carry this into shaping, so it is kept separately for raster. + /// Authored `wght` coordinate for a ranged `@font-face`, retained while + /// descriptor selection is converted into the canonical axis tuple. variation_weight: Option, + has_optical_size: bool, } #[derive(Clone)] @@ -142,19 +147,18 @@ fn resolve_loaded_font( .copied() .find(|face| (face.min_weight..=face.max_weight).contains(&requested_weight)) { - // cosmic-text 0.12 only accepts the named family when the - // shaping weight exactly matches fontdb's weight for that - // file. A variable face commonly advertises `100 900` in - // CSS while fontdb records its default instance as 400. - // Passing the authored weight makes cosmic-text reject the - // 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. + // The named-family matcher uses fontdb's default weight for + // this resource, while a variable face commonly advertises + // `100 900` in CSS. Preserve the descriptor-selected file + // and its database weight; the authored coordinate enters + // the canonical axis tuple below. return ResolvedFont { family: Arc::clone(&face.name), + font_id: face.font_id, shape_weight: face.shape_weight, variation_weight: (face.min_weight != face.max_weight) .then_some(requested_weight), + has_optical_size: face.has_optical_size, }; } let available: Vec<_> = @@ -166,8 +170,10 @@ fn resolve_loaded_font( { return ResolvedFont { family: Arc::clone(&face.name), + font_id: face.font_id, shape_weight: face.shape_weight, variation_weight: None, + has_optical_size: face.has_optical_size, }; } } @@ -177,8 +183,10 @@ fn resolve_loaded_font( let weights: &[u16] = &[400, 700]; ResolvedFont { family: Arc::from(fallback), + font_id: None, shape_weight: match_font_weight(requested_weight, weights), 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 -/// the underline flag; the remaining bits encode an optional one-based index -/// into an [`InlineItem`]'s clip-text fills. +type ClipTextFill = (f32, Vec<([u8; 4], Option)>); + +/// 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_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_MASK: usize = ((1usize << META_VARIATION_BITS) - 1) << META_VARIATION_SHIFT; @@ -277,13 +288,10 @@ fn metadata_fill(metadata: usize) -> Option { ((metadata & META_FILL_MASK) >> META_FILL_SHIFT).checked_sub(1) } -fn metadata_variation_weight(metadata: usize) -> Option { - let weight = ((metadata & META_VARIATION_MASK) >> META_VARIATION_SHIFT) as u16; - (weight != 0).then_some(weight) +fn metadata_variation(metadata: usize) -> Option { + ((metadata & META_VARIATION_MASK) >> META_VARIATION_SHIFT).checked_sub(1) } -type ClipTextFill = (f32, Vec<([u8; 4], Option)>); - /// One inline formatting context: a shaped cosmic-text buffer plus where to /// paint it (filled in after layout). pub struct InlineItem { @@ -300,6 +308,9 @@ pub struct InlineItem { /// one entry, allowing an inline accent span to own a gradient without /// recoloring the rest of its heading. clip_fills: Vec, + /// Canonical axis tuples, populated only when this IFC actually contains + /// variable text. Glyph metadata stores a one-based index. + variation_sets: Vec>, } /// Owns the font set and shaping caches for one render pass, plus every @@ -314,15 +325,15 @@ pub struct TextEngine { replaced: Vec, } -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] struct VariableCacheKey { glyph: CacheKey, - weight: u16, + variations: Arc, } /// Swash's ordinary cache key intentionally has no variation coordinates. -/// Keep variable instances in a separate cache so a 400 glyph can never be -/// reused for 700 and repeated paint remains O(1) after the first raster. +/// Keep variable instances in a separate cache so different axis tuples can +/// never share an outline and repeated paint remains O(1) after first raster. struct VariableSwashCache { context: ScaleContext, images: HashMap>, @@ -340,22 +351,29 @@ impl VariableSwashCache { &mut self, font_system: &mut FontSystem, cache_key: CacheKey, - weight: u16, + variations: Arc, base: Color, mut f: F, ) { + let render_variations = Arc::clone(&variations); let key = VariableCacheKey { glyph: cache_key, - weight, + variations, }; let image = self.images.entry(key).or_insert_with(|| { 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 .context .builder(font.as_swash()) .size(f32::from_bits(cache_key.font_size_bits)) .hint(true) - .variations([("wght", weight as f32)]) + .variations(settings) .build(); let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float()); Render::new(&[ @@ -623,6 +641,14 @@ impl TextEngine { let mut loaded_families = HashMap::new(); for (id, declared_family, declared_weight, declared_italic) in declarations { 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 internal_name = names .first() @@ -641,10 +667,12 @@ impl TextEngine { .or_insert_with(|| LoadedFamily { faces: Vec::new() }); family.faces.push(LoadedFace { name: Arc::clone(&internal_name), + font_id: Some(id), min_weight: weight.0, max_weight: weight.1, shape_weight, italic, + has_optical_size, }); } } @@ -785,7 +813,8 @@ impl TextEngine { letter_spacing: context.letter_spacing, letter_spacing_non_normal: context.letter_spacing_non_normal, weight: context.weight, - variation_weight: context.variation_weight, + font_id: context.font_id, + variations: context.variations.clone(), italic: context.italic, underline: context.underline, color: context.color, @@ -859,10 +888,34 @@ impl TextEngine { // column overflows instead, matching normal `overflow-wrap: normal`. buffer.set_wrap(&mut self.font_system, Wrap::Word); // 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 // 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::>::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)); buffer.set_rich_text( &mut self.font_system, @@ -890,6 +943,7 @@ impl TextEngine { origin: (0.0, 0.0), clip: None, clip_fills, + variation_sets, }); Some(idx) } @@ -976,7 +1030,8 @@ struct SpanAttrs { letter_spacing: f32, letter_spacing_non_normal: bool, weight: u16, - variation_weight: Option, + font_id: Option, + variations: Option>, italic: bool, underline: bool, color: [u8; 4], @@ -985,7 +1040,7 @@ struct 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())); // Inline descendants keep their own computed font metrics inside the // enclosing line box. Without per-span metrics, an ``/`` @@ -996,6 +1051,9 @@ impl SpanAttrs { self.line_height.max(1.0), )); 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 }); if self.letter_spacing.is_finite() && self.letter_spacing != 0.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. 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])); - // Underline, the optional fill index, and a variable `wght` coordinate - // share the per-glyph metadata. - let fill = self.clip_fill.map_or(0, |index| (index + 1) << META_FILL_SHIFT); + let fill = self + .clip_fill + .map_or(0, |index| (index + 1) << META_FILL_SHIFT); debug_assert_eq!(fill & !META_FILL_MASK, 0); - let variation = self - .variation_weight - .map_or(0, |weight| usize::from(weight.clamp(1, 1000)) << META_VARIATION_SHIFT); + let variation = variation_index << META_VARIATION_SHIFT; + assert_eq!(variation & !META_VARIATION_MASK, 0); + if let Some(variations) = self.variations.as_ref() { + a = a.font_variations((**variations).clone()); + } a = a.metadata(fill | variation | usize::from(self.underline)); a } @@ -1035,7 +1095,10 @@ struct SpanCtx { letter_spacing_non_normal: bool, color: [u8; 4], weight: u16, - variation_weight: Option, + font_id: Option, + variations: Option>, + variable_weight: bool, + has_optical_size: bool, italic: bool, underline: bool, transform: TextTransform, @@ -1061,6 +1124,32 @@ struct Collector { /// shape the glyphs in opaque white so their coverage renders, then recolor /// them from the background at paint time; otherwise transparent text stays /// transparent (and invisible), unchanged. +fn resolved_font_variations( + style: &LayoutStyle, + font: &ResolvedFont, +) -> Option> { + 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( base: &LayoutStyle, font: ResolvedFont, @@ -1071,6 +1160,7 @@ fn base_span_ctx( collector.clip_fills.push(fill); index }); + let variations = resolved_font_variations(base, &font); SpanCtx { font_size: base.font_size.unwrap_or(16.0), 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), color: base.color.unwrap_or([0, 0, 0, 255]), 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), underline: base.underline.unwrap_or(false), transform: base.text_transform.unwrap_or(TextTransform::None), @@ -1131,7 +1224,8 @@ fn collect_node_spans( letter_spacing: ctx.letter_spacing, letter_spacing_non_normal: ctx.letter_spacing_non_normal, weight: ctx.weight, - variation_weight: ctx.variation_weight, + font_id: ctx.font_id, + variations: ctx.variations.clone(), italic: ctx.italic, underline: ctx.underline, color: ctx.color, @@ -1153,7 +1247,8 @@ fn collect_node_spans( letter_spacing: ctx.letter_spacing, letter_spacing_non_normal: ctx.letter_spacing_non_normal, weight: ctx.weight, - variation_weight: ctx.variation_weight, + font_id: ctx.font_id, + variations: ctx.variations.clone(), italic: ctx.italic, underline: ctx.underline, color: ctx.color, @@ -1192,15 +1287,20 @@ fn collect_node_spans( }) .unwrap_or_else(|| ResolvedFont { 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 } else { requested_weight }, variation_weight: ctx - .variation_weight - .map(|_| requested_weight.clamp(1, 1000)), + .variable_weight + .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 { font_size: style .and_then(|style| style.font_size) @@ -1216,7 +1316,10 @@ fn collect_node_spans( .unwrap_or(ctx.letter_spacing_non_normal), color, 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), // Underline propagates in: an ancestor's underline covers // 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) .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( font_system, physical.cache_key, - weight, + variations, glyph_color, &mut draw_pixel, ); @@ -1791,24 +1897,30 @@ mod tests { faces: vec![ LoadedFace { name: Arc::from("Poppins"), + font_id: None, min_weight: 400, max_weight: 400, shape_weight: 400, italic: false, + has_optical_size: false, }, LoadedFace { name: Arc::from("Poppins Medium"), + font_id: None, min_weight: 500, max_weight: 500, shape_weight: 500, italic: false, + has_optical_size: false, }, LoadedFace { name: Arc::from("Poppins"), + font_id: None, min_weight: 700, max_weight: 700, shape_weight: 700, italic: false, + has_optical_size: false, }, ], }; @@ -1840,10 +1952,12 @@ mod tests { LoadedFamily { faces: vec![LoadedFace { name: Arc::from(FAMILY), + font_id: None, min_weight: 100, max_weight: 900, shape_weight: 400, italic: false, + has_optical_size: false, }], }, ); @@ -1865,6 +1979,79 @@ mod tests { 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( + "

Build whatever you want, without touching your CSS file.

", + ); + 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] fn ranged_face_preserves_requested_weight_for_variable_raster() { let loaded = HashMap::from([( @@ -1872,10 +2059,12 @@ mod tests { LoadedFamily { faces: vec![LoadedFace { name: Arc::from("Inter"), + font_id: None, min_weight: 100, max_weight: 900, shape_weight: 400, italic: false, + has_optical_size: true, }], }, )]); @@ -1886,24 +2075,82 @@ mod tests { } #[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 { font_size: 16.0, line_height: 18.0, letter_spacing: 0.0, letter_spacing_non_normal: false, weight: 400, - variation_weight: Some(725), + font_id: None, + variations: Some(Arc::new(variations)), italic: false, underline: true, color: [1, 2, 3, 255], family: Arc::from(FAMILY), clip_fill: Some(37), }; - let metadata = attrs.to_attrs().metadata; - assert_ne!(metadata & META_UNDERLINE, 0); - assert_eq!(metadata_fill(metadata), Some(37)); - assert_eq!(metadata_variation_weight(metadata), Some(725)); + let shaped = attrs.to_attrs(42); + assert_ne!(shaped.metadata & META_UNDERLINE, 0); + assert_eq!(metadata_fill(shaped.metadata), Some(37)); + 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("

ordinary text

"); + 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] @@ -1914,14 +2161,15 @@ mod tests { letter_spacing: 2.0, letter_spacing_non_normal: true, weight: 400, - variation_weight: None, + font_id: None, + variations: None, italic: false, underline: false, color: [0, 0, 0, 255], family: Arc::from(FAMILY), 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!(attrs.font_features.features.iter().any(|feature| { feature.tag == FeatureTag::STANDARD_LIGATURES && feature.value == 0 @@ -1932,7 +2180,7 @@ mod tests { let mut zero = span; 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 { @@ -1976,7 +2224,12 @@ mod tests { .variable_swash .images .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(); cached_weights.sort_unstable(); cached_weights.dedup(); @@ -1984,10 +2237,10 @@ mod tests { } #[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 (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!( black_ink > regular_ink * 13 / 10, "wght=900 should carry substantially more raster ink: {regular_ink} vs {black_ink}" @@ -2034,11 +2287,105 @@ mod tests { .variable_swash .images .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(); assert_eq!(weights, std::collections::HashSet::from([400, 900])); } + fn render_tailwind_optical_heading( + optical_sizing: crate::FontOpticalSizing, + explicit_opsz: Option, + width: f32, + ) -> ((f32, f32), usize, std::collections::HashSet) { + 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( + "

Build whatever you want, without touching your CSS file.

", + ); + 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] fn text_only_inline_block_keeps_an_internal_shaping_context() { let tree = obscura_dom::parse_html( diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index c579261..c7f3ff9 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -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. #[derive(Debug, Clone, Default)] pub struct LayoutStyle { @@ -374,6 +395,12 @@ pub struct LayoutStyle { /// resolves it to a bundled face (Liberation Sans/Serif/Mono) the way /// Chromium picks a generic family on this host. pub font_family: Option, + /// Computed inherited `font-optical-sizing`. `None` during cascade means + /// inherit; the top-down pass resolves every element to `Some`. + pub font_optical_sizing: Option, + /// Computed inherited `font-variation-settings`. `None` during cascade + /// means inherit, while `Some(Vec::new())` is the `normal` initial value. + pub font_variation_settings: Option>, /// Inherited `text-align`, represented with the matching horizontal /// alignment keywords. Kept separate from flex/grid `align-items`: using /// one field for both made `text-align:left` shrink-wrap flex children. diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index 39ba5e0..24b010a 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -9,6 +9,7 @@ //! the crate dependency-light. use crate::{Display, Edges, LayoutStyle}; +use std::collections::BTreeMap; /// Compute the layout-relevant style for an element: UA defaults for its tag, /// overridden by its inline `style="..."` declarations. @@ -431,6 +432,106 @@ fn parse_container_shorthand(value: &str) -> Option<(Vec, crate::Contain } } +fn parse_font_variation_settings(value: &str) -> Option> { + 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 { + 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) { match name { "display" => { @@ -735,6 +836,30 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { 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 // deliberately separate from flex/grid `align-items`, which positions // child boxes rather than text inside them. @@ -1242,6 +1367,8 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool { | "font-weight" | "font-family" | "font-style" + | "font-optical-sizing" + | "font-variation-settings" | "text-align" | "text-transform" | "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-name" => parse_container_names(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!( value.to_ascii_lowercase().as_str(), "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. style.font_style_italic = Some(false); 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_expression = None; for token in &tokens[..size_index] { @@ -4508,6 +4645,168 @@ mod tests { 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] fn letter_spacing_preserves_units_resets_and_invalid_cascade_values() { let relative = compute_style("span", Some("letter-spacing:-.05em")); diff --git a/vendor/cosmic-text/Cargo.toml b/vendor/cosmic-text/Cargo.toml new file mode 100644 index 0000000..fd7f5f7 --- /dev/null +++ b/vendor/cosmic-text/Cargo.toml @@ -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 "] +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 diff --git a/vendor/cosmic-text/Cargo.toml.orig b/vendor/cosmic-text/Cargo.toml.orig new file mode 100644 index 0000000..ffa1eae --- /dev/null +++ b/vendor/cosmic-text/Cargo.toml.orig @@ -0,0 +1,84 @@ +[package] +name = "cosmic-text" +description = "Pure Rust multi-line text handling" +version = "0.14.2" +authors = ["Jeremy Soller "] +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"] diff --git a/vendor/cosmic-text/LICENSE-APACHE b/vendor/cosmic-text/LICENSE-APACHE new file mode 100644 index 0000000..6f75635 --- /dev/null +++ b/vendor/cosmic-text/LICENSE-APACHE @@ -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. diff --git a/vendor/cosmic-text/LICENSE-MIT b/vendor/cosmic-text/LICENSE-MIT new file mode 100644 index 0000000..db6aab1 --- /dev/null +++ b/vendor/cosmic-text/LICENSE-MIT @@ -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. diff --git a/vendor/cosmic-text/README.md b/vendor/cosmic-text/README.md new file mode 100644 index 0000000..979a333 --- /dev/null +++ b/vendor/cosmic-text/README.md @@ -0,0 +1,94 @@ +# COSMIC Text + +[![crates.io](https://img.shields.io/crates/v/cosmic-text.svg)](https://crates.io/crates/cosmic-text) +[![docs.rs](https://docs.rs/cosmic-text/badge.svg)](https://docs.rs/cosmic-text) +![license](https://img.shields.io/crates/l/cosmic-text.svg) +[![Rust workflow](https://github.com/pop-os/cosmic-text/workflows/Rust/badge.svg?event=push)](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 +[![Arabic screenshot](screenshots/arabic.png)](screenshots/arabic.png) + +Hindi translation of Universal Declaration of Human Rights +[![Hindi screenshot](screenshots/hindi.png)](screenshots/hindi.png) + +Simplified Chinese translation of Universal Declaration of Human Rights +[![Simplified Chinses screenshot](screenshots/chinese-simplified.png)](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. diff --git a/vendor/cosmic-text/fonts/NotoSans-LICENSE b/vendor/cosmic-text/fonts/NotoSans-LICENSE new file mode 100644 index 0000000..cb2a761 --- /dev/null +++ b/vendor/cosmic-text/fonts/NotoSans-LICENSE @@ -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. diff --git a/vendor/cosmic-text/fonts/NotoSans-Regular.ttf b/vendor/cosmic-text/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000..7552fbe Binary files /dev/null and b/vendor/cosmic-text/fonts/NotoSans-Regular.ttf differ diff --git a/vendor/cosmic-text/fonts/NotoSansArabic.ttf b/vendor/cosmic-text/fonts/NotoSansArabic.ttf new file mode 100644 index 0000000..0356020 Binary files /dev/null and b/vendor/cosmic-text/fonts/NotoSansArabic.ttf differ diff --git a/vendor/cosmic-text/fonts/NotoSansHebrew.ttf b/vendor/cosmic-text/fonts/NotoSansHebrew.ttf new file mode 100644 index 0000000..7ea039e Binary files /dev/null and b/vendor/cosmic-text/fonts/NotoSansHebrew.ttf differ diff --git a/vendor/cosmic-text/src/attrs.rs b/vendor/cosmic-text/src/attrs.rs new file mode 100644 index 0000000..20c697a --- /dev/null +++ b/vendor/cosmic-text/src/attrs.rs @@ -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 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 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, +} + +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(&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, +} + +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 { + 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(&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, + 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, + pub stretch: Stretch, + pub style: Style, + pub weight: Weight, + pub metadata: usize, + pub cache_key_flags: CacheKeyFlags, + pub metrics_opt: Option, + /// Letter spacing (tracking) in EM + pub letter_spacing_opt: Option, + 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, + pub family_owned: FamilyOwned, + pub font_id_opt: Option, + pub stretch: Stretch, + pub style: Style, + pub weight: Weight, + pub metadata: usize, + pub cache_key_flags: CacheKeyFlags, + pub metrics_opt: Option, + /// Letter spacing (tracking) in EM + pub letter_spacing_opt: Option, + 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, +} + +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, &AttrsOwned)> { + self.spans_iter().collect() + } + + /// Get an iterator over the current attribute spans + pub fn spans_iter(&self) -> impl Iterator, &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, 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))); + } +} diff --git a/vendor/cosmic-text/src/bidi_para.rs b/vendor/cosmic-text/src/bidi_para.rs new file mode 100644 index 0000000..5a7e595 --- /dev/null +++ b/vendor/cosmic-text/src/bidi_para.rs @@ -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, +} + +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 { + 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) + } + } +} diff --git a/vendor/cosmic-text/src/buffer.rs b/vendor/cosmic-text/src/buffer.rs new file mode 100644 index 0000000..0292e6b --- /dev/null +++ b/vendor/cosmic-text/src/buffer.rs @@ -0,0 +1,1485 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; +use core::{cmp, fmt}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + Affinity, Align, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, + Cursor, FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LineEnding, LineIter, Motion, + Scroll, ShapeLine, Shaping, Wrap, +}; + +/// A line of visible text for rendering +#[derive(Debug)] +pub struct LayoutRun<'a> { + /// The index of the original text line + pub line_i: usize, + /// The original text line + pub text: &'a str, + /// True if the original paragraph direction is RTL + pub rtl: bool, + /// The array of layout glyphs to draw + pub glyphs: &'a [LayoutGlyph], + /// Y offset to baseline of line + pub line_y: f32, + /// Y offset to top of line + pub line_top: f32, + /// Y offset to next line + pub line_height: f32, + /// Width of line + pub line_w: f32, +} + +impl LayoutRun<'_> { + /// Return the pixel span `Some((x_left, x_width))` of the highlighted area between `cursor_start` + /// and `cursor_end` within this run, or None if the cursor range does not intersect this run. + /// This may return widths of zero if `cursor_start == cursor_end`, if the run is empty, or if the + /// region's left start boundary is the same as the cursor's end boundary or vice versa. + #[allow(clippy::missing_panics_doc)] + pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> { + let mut x_start = None; + let mut x_end = None; + let rtl_factor = if self.rtl { 1. } else { 0. }; + let ltr_factor = 1. - rtl_factor; + for glyph in self.glyphs.iter() { + let cursor = self.cursor_from_glyph_left(glyph); + if cursor >= cursor_start && cursor <= cursor_end { + if x_start.is_none() { + x_start = Some(glyph.x + glyph.w * rtl_factor); + } + x_end = Some(glyph.x + glyph.w * rtl_factor); + } + let cursor = self.cursor_from_glyph_right(glyph); + if cursor >= cursor_start && cursor <= cursor_end { + if x_start.is_none() { + x_start = Some(glyph.x + glyph.w * ltr_factor); + } + x_end = Some(glyph.x + glyph.w * ltr_factor); + } + } + if let Some(x_start) = x_start { + let x_end = x_end.expect("end of cursor not found"); + let (x_start, x_end) = if x_start < x_end { + (x_start, x_end) + } else { + (x_end, x_start) + }; + Some((x_start, x_end - x_start)) + } else { + None + } + } + + fn cursor_from_glyph_left(&self, glyph: &LayoutGlyph) -> Cursor { + if self.rtl { + Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before) + } else { + Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After) + } + } + + fn cursor_from_glyph_right(&self, glyph: &LayoutGlyph) -> Cursor { + if self.rtl { + Cursor::new_with_affinity(self.line_i, glyph.start, Affinity::After) + } else { + Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before) + } + } +} + +/// An iterator of visible text lines, see [`LayoutRun`] +#[derive(Debug)] +pub struct LayoutRunIter<'b> { + buffer: &'b Buffer, + line_i: usize, + layout_i: usize, + total_height: f32, + line_top: f32, +} + +impl<'b> LayoutRunIter<'b> { + pub fn new(buffer: &'b Buffer) -> Self { + Self { + buffer, + line_i: buffer.scroll.line, + layout_i: 0, + total_height: 0.0, + line_top: 0.0, + } + } +} + +impl<'b> Iterator for LayoutRunIter<'b> { + type Item = LayoutRun<'b>; + + fn next(&mut self) -> Option { + while let Some(line) = self.buffer.lines.get(self.line_i) { + let shape = line.shape_opt()?; + let layout = line.layout_opt()?; + while let Some(layout_line) = layout.get(self.layout_i) { + self.layout_i += 1; + + let line_height = layout_line + .line_height_opt + .unwrap_or(self.buffer.metrics.line_height); + self.total_height += line_height; + + let line_top = self.line_top - self.buffer.scroll.vertical; + let glyph_height = layout_line.max_ascent + layout_line.max_descent; + let centering_offset = (line_height - glyph_height) / 2.0; + let line_y = line_top + centering_offset + layout_line.max_ascent; + if let Some(height) = self.buffer.height_opt { + if line_y > height { + return None; + } + } + self.line_top += line_height; + if line_y < 0.0 { + continue; + } + + return Some(LayoutRun { + line_i: self.line_i, + text: line.text(), + rtl: shape.rtl, + glyphs: &layout_line.glyphs, + line_y, + line_top, + line_height, + line_w: layout_line.w, + }); + } + self.line_i += 1; + self.layout_i = 0; + } + + None + } +} + +/// Metrics of text +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Metrics { + /// Font size in pixels + pub font_size: f32, + /// Line height in pixels + pub line_height: f32, +} + +impl Metrics { + /// Create metrics with given font size and line height + pub const fn new(font_size: f32, line_height: f32) -> Self { + Self { + font_size, + line_height, + } + } + + /// Create metrics with given font size and calculate line height using relative scale + pub fn relative(font_size: f32, line_height_scale: f32) -> Self { + Self { + font_size, + line_height: font_size * line_height_scale, + } + } + + /// Scale font size and line height + pub fn scale(self, scale: f32) -> Self { + Self { + font_size: self.font_size * scale, + line_height: self.line_height * scale, + } + } +} + +impl fmt::Display for Metrics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}px / {}px", self.font_size, self.line_height) + } +} + +/// A buffer of text that is shaped and laid out +#[derive(Debug)] +pub struct Buffer { + /// [`BufferLine`]s (or paragraphs) of text in the buffer + pub lines: Vec, + metrics: Metrics, + width_opt: Option, + height_opt: Option, + scroll: Scroll, + /// True if a redraw is requires. Set to false after processing + redraw: bool, + wrap: Wrap, + monospace_width: Option, + tab_width: u16, +} + +impl Clone for Buffer { + fn clone(&self) -> Self { + Self { + lines: self.lines.clone(), + metrics: self.metrics, + width_opt: self.width_opt, + height_opt: self.height_opt, + scroll: self.scroll, + redraw: self.redraw, + wrap: self.wrap, + monospace_width: self.monospace_width, + tab_width: self.tab_width, + } + } +} + +impl Buffer { + /// Create an empty [`Buffer`] with the provided [`Metrics`]. + /// This is useful for initializing a [`Buffer`] without a [`FontSystem`]. + /// + /// You must populate the [`Buffer`] with at least one [`BufferLine`] before shaping and layout, + /// for example by calling [`Buffer::set_text`]. + /// + /// If you have a [`FontSystem`] in scope, you should use [`Buffer::new`] instead. + /// + /// # Panics + /// + /// Will panic if `metrics.line_height` is zero. + pub fn new_empty(metrics: Metrics) -> Self { + assert_ne!(metrics.line_height, 0.0, "line height cannot be 0"); + Self { + lines: Vec::new(), + metrics, + width_opt: None, + height_opt: None, + scroll: Scroll::default(), + redraw: false, + wrap: Wrap::WordOrGlyph, + monospace_width: None, + tab_width: 8, + } + } + + /// Create a new [`Buffer`] with the provided [`FontSystem`] and [`Metrics`] + /// + /// # Panics + /// + /// Will panic if `metrics.line_height` is zero. + pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self { + let mut buffer = Self::new_empty(metrics); + buffer.set_text(font_system, "", &Attrs::new(), Shaping::Advanced); + buffer + } + + /// Mutably borrows the buffer together with an [`FontSystem`] for more convenient methods + pub fn borrow_with<'a>( + &'a mut self, + font_system: &'a mut FontSystem, + ) -> BorrowedWithFontSystem<'a, Buffer> { + BorrowedWithFontSystem { + inner: self, + font_system, + } + } + + fn relayout(&mut self, font_system: &mut FontSystem) { + #[cfg(all(feature = "std", not(target_arch = "wasm32")))] + let instant = std::time::Instant::now(); + + for line in &mut self.lines { + if line.shape_opt().is_some() { + line.reset_layout(); + line.layout( + font_system, + self.metrics.font_size, + self.width_opt, + self.wrap, + self.monospace_width, + self.tab_width, + ); + } + } + + self.redraw = true; + + #[cfg(all(feature = "std", not(target_arch = "wasm32")))] + log::debug!("relayout: {:?}", instant.elapsed()); + } + + /// Shape lines until cursor, also scrolling to include cursor in view + #[allow(clippy::missing_panics_doc)] + pub fn shape_until_cursor( + &mut self, + font_system: &mut FontSystem, + cursor: Cursor, + prune: bool, + ) { + let metrics = self.metrics; + let old_scroll = self.scroll; + + let layout_cursor = self + .layout_cursor(font_system, cursor) + .expect("shape_until_cursor invalid cursor"); + + let mut layout_y = 0.0; + let mut total_height = { + let layout = self + .line_layout(font_system, layout_cursor.line) + .expect("shape_until_cursor failed to scroll forwards"); + (0..layout_cursor.layout).for_each(|layout_i| { + layout_y += layout[layout_i] + .line_height_opt + .unwrap_or(metrics.line_height); + }); + layout_y + + layout[layout_cursor.layout] + .line_height_opt + .unwrap_or(metrics.line_height) + }; + + if self.scroll.line > layout_cursor.line + || (self.scroll.line == layout_cursor.line && self.scroll.vertical > layout_y) + { + // Adjust scroll backwards if cursor is before it + self.scroll.line = layout_cursor.line; + self.scroll.vertical = layout_y; + } else if let Some(height) = self.height_opt { + // Adjust scroll forwards if cursor is after it + let mut line_i = layout_cursor.line; + if line_i <= self.scroll.line { + // This is a single line that may wrap + if total_height > height + self.scroll.vertical { + self.scroll.vertical = total_height - height; + } + } else { + while line_i > self.scroll.line { + line_i -= 1; + let layout = self + .line_layout(font_system, line_i) + .expect("shape_until_cursor failed to scroll forwards"); + for layout_line in layout.iter() { + total_height += layout_line.line_height_opt.unwrap_or(metrics.line_height); + } + if total_height > height + self.scroll.vertical { + self.scroll.line = line_i; + self.scroll.vertical = total_height - height; + } + } + } + } + + if old_scroll != self.scroll { + self.redraw = true; + } + + self.shape_until_scroll(font_system, prune); + + // Adjust horizontal scroll to include cursor + if let Some(layout_cursor) = self.layout_cursor(font_system, cursor) { + if let Some(layout_lines) = self.line_layout(font_system, layout_cursor.line) { + if let Some(layout_line) = layout_lines.get(layout_cursor.layout) { + let (x_min, x_max) = if let Some(glyph) = layout_line + .glyphs + .get(layout_cursor.glyph) + .or_else(|| layout_line.glyphs.last()) + { + //TODO: use code from cursor_glyph_opt? + let x_a = glyph.x; + let x_b = glyph.x + glyph.w; + (x_a.min(x_b), x_a.max(x_b)) + } else { + (0.0, 0.0) + }; + if x_min < self.scroll.horizontal { + self.scroll.horizontal = x_min; + self.redraw = true; + } + if let Some(width) = self.width_opt { + if x_max > self.scroll.horizontal + width { + self.scroll.horizontal = x_max - width; + self.redraw = true; + } + } + } + } + } + } + + /// Shape lines until scroll + #[allow(clippy::missing_panics_doc)] + pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) { + let metrics = self.metrics; + let old_scroll = self.scroll; + + loop { + // Adjust scroll.layout to be positive by moving scroll.line backwards + while self.scroll.vertical < 0.0 { + if self.scroll.line > 0 { + let line_i = self.scroll.line - 1; + if let Some(layout) = self.line_layout(font_system, line_i) { + let mut layout_height = 0.0; + for layout_line in layout.iter() { + layout_height += + layout_line.line_height_opt.unwrap_or(metrics.line_height); + } + self.scroll.line = line_i; + self.scroll.vertical += layout_height; + } else { + // If layout is missing, just assume line height + self.scroll.line = line_i; + self.scroll.vertical += metrics.line_height; + } + } else { + self.scroll.vertical = 0.0; + break; + } + } + + let scroll_start = self.scroll.vertical; + let scroll_end = scroll_start + self.height_opt.unwrap_or(f32::INFINITY); + + let mut total_height = 0.0; + for line_i in 0..self.lines.len() { + if line_i < self.scroll.line { + if prune { + self.lines[line_i].reset_shaping(); + } + continue; + } + if total_height > scroll_end { + if prune { + self.lines[line_i].reset_shaping(); + continue; + } else { + break; + } + } + + let mut layout_height = 0.0; + let layout = self + .line_layout(font_system, line_i) + .expect("shape_until_scroll invalid line"); + for layout_line in layout.iter() { + let line_height = layout_line.line_height_opt.unwrap_or(metrics.line_height); + layout_height += line_height; + total_height += line_height; + } + + // Adjust scroll.vertical to be smaller by moving scroll.line forwards + //TODO: do we want to adjust it exactly to a layout line? + if line_i == self.scroll.line && layout_height < self.scroll.vertical { + self.scroll.line += 1; + self.scroll.vertical -= layout_height; + } + } + + if total_height < scroll_end && self.scroll.line > 0 { + // Need to scroll up to stay inside of buffer + self.scroll.vertical -= scroll_end - total_height; + } else { + // Done adjusting scroll + break; + } + } + + if old_scroll != self.scroll { + self.redraw = true; + } + } + + /// Convert a [`Cursor`] to a [`LayoutCursor`] + pub fn layout_cursor( + &mut self, + font_system: &mut FontSystem, + cursor: Cursor, + ) -> Option { + let layout = self.line_layout(font_system, cursor.line)?; + for (layout_i, layout_line) in layout.iter().enumerate() { + for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() { + let cursor_end = + Cursor::new_with_affinity(cursor.line, glyph.end, Affinity::Before); + let cursor_start = + Cursor::new_with_affinity(cursor.line, glyph.start, Affinity::After); + let (cursor_left, cursor_right) = if glyph.level.is_ltr() { + (cursor_start, cursor_end) + } else { + (cursor_end, cursor_start) + }; + if cursor == cursor_left { + return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i)); + } + if cursor == cursor_right { + return Some(LayoutCursor::new(cursor.line, layout_i, glyph_i + 1)); + } + } + } + + // Fall back to start of line + //TODO: should this be the end of the line? + Some(LayoutCursor::new(cursor.line, 0, 0)) + } + + /// Shape the provided line index and return the result + pub fn line_shape( + &mut self, + font_system: &mut FontSystem, + line_i: usize, + ) -> Option<&ShapeLine> { + let line = self.lines.get_mut(line_i)?; + Some(line.shape(font_system, self.tab_width)) + } + + /// Lay out the provided line index and return the result + pub fn line_layout( + &mut self, + font_system: &mut FontSystem, + line_i: usize, + ) -> Option<&[LayoutLine]> { + let line = self.lines.get_mut(line_i)?; + Some(line.layout( + font_system, + self.metrics.font_size, + self.width_opt, + self.wrap, + self.monospace_width, + self.tab_width, + )) + } + + /// Get the current [`Metrics`] + pub fn metrics(&self) -> Metrics { + self.metrics + } + + /// Set the current [`Metrics`] + /// + /// # Panics + /// + /// Will panic if `metrics.font_size` is zero. + pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) { + self.set_metrics_and_size(font_system, metrics, self.width_opt, self.height_opt); + } + + /// Get the current [`Wrap`] + pub fn wrap(&self) -> Wrap { + self.wrap + } + + /// Set the current [`Wrap`] + pub fn set_wrap(&mut self, font_system: &mut FontSystem, wrap: Wrap) { + if wrap != self.wrap { + self.wrap = wrap; + self.relayout(font_system); + self.shape_until_scroll(font_system, false); + } + } + + /// Get the current `monospace_width` + pub fn monospace_width(&self) -> Option { + self.monospace_width + } + + /// Set monospace width monospace glyphs should be resized to match. `None` means don't resize + pub fn set_monospace_width( + &mut self, + font_system: &mut FontSystem, + monospace_width: Option, + ) { + if monospace_width != self.monospace_width { + self.monospace_width = monospace_width; + self.relayout(font_system); + self.shape_until_scroll(font_system, false); + } + } + + /// Get the current `tab_width` + pub fn tab_width(&self) -> u16 { + self.tab_width + } + + /// Set tab width (number of spaces between tab stops) + pub fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { + // A tab width of 0 is not allowed + if tab_width == 0 { + return; + } + if tab_width != self.tab_width { + self.tab_width = tab_width; + // Shaping must be reset when tab width is changed + for line in self.lines.iter_mut() { + if line.shape_opt().is_some() && line.text().contains('\t') { + line.reset_shaping(); + } + } + self.redraw = true; + self.shape_until_scroll(font_system, false); + } + } + + /// Get the current buffer dimensions (width, height) + pub fn size(&self) -> (Option, Option) { + (self.width_opt, self.height_opt) + } + + /// Set the current buffer dimensions + pub fn set_size( + &mut self, + font_system: &mut FontSystem, + width_opt: Option, + height_opt: Option, + ) { + self.set_metrics_and_size(font_system, self.metrics, width_opt, height_opt); + } + + /// Set the current [`Metrics`] and buffer dimensions at the same time + /// + /// # Panics + /// + /// Will panic if `metrics.font_size` is zero. + pub fn set_metrics_and_size( + &mut self, + font_system: &mut FontSystem, + metrics: Metrics, + width_opt: Option, + height_opt: Option, + ) { + let clamped_width_opt = width_opt.map(|width| width.max(0.0)); + let clamped_height_opt = height_opt.map(|height| height.max(0.0)); + + if metrics != self.metrics + || clamped_width_opt != self.width_opt + || clamped_height_opt != self.height_opt + { + assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); + self.metrics = metrics; + self.width_opt = clamped_width_opt; + self.height_opt = clamped_height_opt; + self.relayout(font_system); + self.shape_until_scroll(font_system, false); + } + } + + /// Get the current scroll location + pub fn scroll(&self) -> Scroll { + self.scroll + } + + /// Set the current scroll location + pub fn set_scroll(&mut self, scroll: Scroll) { + if scroll != self.scroll { + self.scroll = scroll; + self.redraw = true; + } + } + + /// Set text of buffer, using provided attributes for each line by default + pub fn set_text( + &mut self, + font_system: &mut FontSystem, + text: &str, + attrs: &Attrs, + shaping: Shaping, + ) { + self.lines.clear(); + for (range, ending) in LineIter::new(text) { + self.lines.push(BufferLine::new( + &text[range], + ending, + AttrsList::new(attrs), + shaping, + )); + } + if self.lines.is_empty() { + self.lines.push(BufferLine::new( + "", + LineEnding::default(), + AttrsList::new(attrs), + shaping, + )); + } + self.scroll = Scroll::default(); + self.shape_until_scroll(font_system, false); + } + + /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) + /// + /// ``` + /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; + /// # let mut font_system = FontSystem::new(); + /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); + /// let attrs = Attrs::new().family(Family::Serif); + /// buffer.set_rich_text( + /// &mut font_system, + /// [ + /// ("hello, ", attrs.clone()), + /// ("cosmic\ntext", attrs.clone().family(Family::Monospace)), + /// ], + /// &attrs, + /// Shaping::Advanced, + /// None, + /// ); + /// ``` + pub fn set_rich_text<'r, 's, I>( + &mut self, + font_system: &mut FontSystem, + spans: I, + default_attrs: &Attrs, + shaping: Shaping, + alignment: Option, + ) where + I: IntoIterator)>, + { + let mut end = 0; + // TODO: find a way to cache this string and vec for reuse + let (string, spans_data): (String, Vec<_>) = spans + .into_iter() + .map(|(s, attrs)| { + let start = end; + end += s.len(); + (s, (attrs, start..end)) + }) + .unzip(); + + let mut spans_iter = spans_data.into_iter(); + let mut maybe_span = spans_iter.next(); + + // split the string into lines, as ranges + let string_start = string.as_ptr() as usize; + let mut lines_iter = BidiParagraphs::new(&string).map(|line: &str| { + let start = line.as_ptr() as usize - string_start; + let end = start + line.len(); + start..end + }); + let mut maybe_line = lines_iter.next(); + //TODO: set this based on information from spans + let line_ending = LineEnding::default(); + + let mut line_count = 0; + let mut attrs_list = self + .lines + .get_mut(line_count) + .map(BufferLine::reclaim_attrs) + .unwrap_or_else(|| AttrsList::new(&Attrs::new())) + .reset(default_attrs); + let mut line_string = self + .lines + .get_mut(line_count) + .map(BufferLine::reclaim_text) + .unwrap_or_default(); + + loop { + let (Some(line_range), Some((attrs, span_range))) = (&maybe_line, &maybe_span) else { + // this is reached only if this text is empty + if self.lines.len() == line_count { + self.lines.push(BufferLine::empty()); + } + self.lines[line_count].reset_new( + String::new(), + line_ending, + AttrsList::new(default_attrs), + shaping, + ); + line_count += 1; + break; + }; + + // start..end is the intersection of this line and this span + let start = line_range.start.max(span_range.start); + let end = line_range.end.min(span_range.end); + if start < end { + let text = &string[start..end]; + let text_start = line_string.len(); + line_string.push_str(text); + let text_end = line_string.len(); + // Only add attrs if they don't match the defaults + if *attrs != attrs_list.defaults() { + attrs_list.add_span(text_start..text_end, attrs); + } + } + + // we know that at the end of a line, + // span text's end index is always >= line text's end index + // so if this span ends before this line ends, + // there is another span in this line. + // otherwise, we move on to the next line. + if span_range.end < line_range.end { + maybe_span = spans_iter.next(); + } else { + maybe_line = lines_iter.next(); + if maybe_line.is_some() { + // finalize this line and start a new line + let next_attrs_list = self + .lines + .get_mut(line_count + 1) + .map(BufferLine::reclaim_attrs) + .unwrap_or_else(|| AttrsList::new(&Attrs::new())) + .reset(default_attrs); + let next_line_string = self + .lines + .get_mut(line_count + 1) + .map(BufferLine::reclaim_text) + .unwrap_or_default(); + let prev_attrs_list = core::mem::replace(&mut attrs_list, next_attrs_list); + let prev_line_string = core::mem::replace(&mut line_string, next_line_string); + if self.lines.len() == line_count { + self.lines.push(BufferLine::empty()); + } + self.lines[line_count].reset_new( + prev_line_string, + line_ending, + prev_attrs_list, + shaping, + ); + line_count += 1; + } else { + // finalize the final line + if self.lines.len() == line_count { + self.lines.push(BufferLine::empty()); + } + self.lines[line_count].reset_new(line_string, line_ending, attrs_list, shaping); + line_count += 1; + break; + } + } + } + + // Discard excess lines now that we have reused as much of the existing allocations as possible. + self.lines.truncate(line_count); + + self.lines.iter_mut().for_each(|line| { + line.set_align(alignment); + }); + + self.scroll = Scroll::default(); + + self.shape_until_scroll(font_system, false); + } + + /// True if a redraw is needed + pub fn redraw(&self) -> bool { + self.redraw + } + + /// Set redraw needed flag + pub fn set_redraw(&mut self, redraw: bool) { + self.redraw = redraw; + } + + /// Get the visible layout runs for rendering and other tasks + pub fn layout_runs(&self) -> LayoutRunIter { + LayoutRunIter::new(self) + } + + /// Convert x, y position to Cursor (hit detection) + pub fn hit(&self, x: f32, y: f32) -> Option { + #[cfg(all(feature = "std", not(target_arch = "wasm32")))] + let instant = std::time::Instant::now(); + + let mut new_cursor_opt = None; + + let mut runs = self.layout_runs().peekable(); + let mut first_run = true; + while let Some(run) = runs.next() { + let line_top = run.line_top; + let line_height = run.line_height; + + if first_run && y < line_top { + first_run = false; + let new_cursor = Cursor::new(run.line_i, 0); + new_cursor_opt = Some(new_cursor); + } else if y >= line_top && y < line_top + line_height { + let mut new_cursor_glyph = run.glyphs.len(); + let mut new_cursor_char = 0; + let mut new_cursor_affinity = Affinity::After; + + let mut first_glyph = true; + + 'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() { + if first_glyph { + first_glyph = false; + if (run.rtl && x > glyph.x) || (!run.rtl && x < 0.0) { + new_cursor_glyph = 0; + new_cursor_char = 0; + } + } + if x >= glyph.x && x <= glyph.x + glyph.w { + new_cursor_glyph = glyph_i; + + let cluster = &run.text[glyph.start..glyph.end]; + let total = cluster.grapheme_indices(true).count(); + let mut egc_x = glyph.x; + let egc_w = glyph.w / (total as f32); + for (egc_i, egc) in cluster.grapheme_indices(true) { + if x >= egc_x && x <= egc_x + egc_w { + new_cursor_char = egc_i; + + let right_half = x >= egc_x + egc_w / 2.0; + if right_half != glyph.level.is_rtl() { + // If clicking on last half of glyph, move cursor past glyph + new_cursor_char += egc.len(); + new_cursor_affinity = Affinity::Before; + } + break 'hit; + } + egc_x += egc_w; + } + + let right_half = x >= glyph.x + glyph.w / 2.0; + if right_half != glyph.level.is_rtl() { + // If clicking on last half of glyph, move cursor past glyph + new_cursor_char = cluster.len(); + new_cursor_affinity = Affinity::Before; + } + break 'hit; + } + } + + let mut new_cursor = Cursor::new(run.line_i, 0); + + match run.glyphs.get(new_cursor_glyph) { + Some(glyph) => { + // Position at glyph + new_cursor.index = glyph.start + new_cursor_char; + new_cursor.affinity = new_cursor_affinity; + } + None => { + if let Some(glyph) = run.glyphs.last() { + // Position at end of line + new_cursor.index = glyph.end; + new_cursor.affinity = Affinity::Before; + } + } + } + + new_cursor_opt = Some(new_cursor); + + break; + } else if runs.peek().is_none() && y > run.line_y { + let mut new_cursor = Cursor::new(run.line_i, 0); + if let Some(glyph) = run.glyphs.last() { + new_cursor = run.cursor_from_glyph_right(glyph); + } + new_cursor_opt = Some(new_cursor); + } + } + + #[cfg(all(feature = "std", not(target_arch = "wasm32")))] + log::trace!("click({}, {}): {:?}", x, y, instant.elapsed()); + + new_cursor_opt + } + + /// Apply a [`Motion`] to a [`Cursor`] + pub fn cursor_motion( + &mut self, + font_system: &mut FontSystem, + mut cursor: Cursor, + mut cursor_x_opt: Option, + motion: Motion, + ) -> Option<(Cursor, Option)> { + match motion { + Motion::LayoutCursor(layout_cursor) => { + let layout = self.line_layout(font_system, layout_cursor.line)?; + + let layout_line = match layout.get(layout_cursor.layout) { + Some(some) => some, + None => match layout.last() { + Some(some) => some, + None => { + return None; + } + }, + }; + + let (new_index, new_affinity) = match layout_line.glyphs.get(layout_cursor.glyph) { + Some(glyph) => (glyph.start, Affinity::After), + None => match layout_line.glyphs.last() { + Some(glyph) => (glyph.end, Affinity::Before), + //TODO: is this correct? + None => (0, Affinity::After), + }, + }; + + if cursor.line != layout_cursor.line + || cursor.index != new_index + || cursor.affinity != new_affinity + { + cursor.line = layout_cursor.line; + cursor.index = new_index; + cursor.affinity = new_affinity; + } + } + Motion::Previous => { + let line = self.lines.get(cursor.line)?; + if cursor.index > 0 { + // Find previous character index + let mut prev_index = 0; + for (i, _) in line.text().grapheme_indices(true) { + if i < cursor.index { + prev_index = i; + } else { + break; + } + } + + cursor.index = prev_index; + cursor.affinity = Affinity::After; + } else if cursor.line > 0 { + cursor.line -= 1; + cursor.index = self.lines.get(cursor.line)?.text().len(); + cursor.affinity = Affinity::After; + } + cursor_x_opt = None; + } + Motion::Next => { + let line = self.lines.get(cursor.line)?; + if cursor.index < line.text().len() { + for (i, c) in line.text().grapheme_indices(true) { + if i == cursor.index { + cursor.index += c.len(); + cursor.affinity = Affinity::Before; + break; + } + } + } else if cursor.line + 1 < self.lines.len() { + cursor.line += 1; + cursor.index = 0; + cursor.affinity = Affinity::Before; + } + cursor_x_opt = None; + } + Motion::Left => { + let rtl_opt = self + .line_shape(font_system, cursor.line) + .map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + (cursor, cursor_x_opt) = + self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Next)?; + } else { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::Previous, + )?; + } + } + } + Motion::Right => { + let rtl_opt = self + .line_shape(font_system, cursor.line) + .map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::Previous, + )?; + } else { + (cursor, cursor_x_opt) = + self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Next)?; + } + } + } + Motion::Up => { + let mut layout_cursor = self.layout_cursor(font_system, cursor)?; + + if cursor_x_opt.is_none() { + cursor_x_opt = Some( + layout_cursor.glyph as i32, //TODO: glyph x position + ); + } + + if layout_cursor.layout > 0 { + layout_cursor.layout -= 1; + } else if layout_cursor.line > 0 { + layout_cursor.line -= 1; + layout_cursor.layout = usize::MAX; + } + + if let Some(cursor_x) = cursor_x_opt { + layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position + } + + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::LayoutCursor(layout_cursor), + )?; + } + Motion::Down => { + let mut layout_cursor = self.layout_cursor(font_system, cursor)?; + + let layout_len = self.line_layout(font_system, layout_cursor.line)?.len(); + + if cursor_x_opt.is_none() { + cursor_x_opt = Some( + layout_cursor.glyph as i32, //TODO: glyph x position + ); + } + + if layout_cursor.layout + 1 < layout_len { + layout_cursor.layout += 1; + } else if layout_cursor.line + 1 < self.lines.len() { + layout_cursor.line += 1; + layout_cursor.layout = 0; + } + + if let Some(cursor_x) = cursor_x_opt { + layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position + } + + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::LayoutCursor(layout_cursor), + )?; + } + Motion::Home => { + let mut layout_cursor = self.layout_cursor(font_system, cursor)?; + layout_cursor.glyph = 0; + #[allow(unused_assignments)] + { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::LayoutCursor(layout_cursor), + )?; + } + cursor_x_opt = None; + } + Motion::SoftHome => { + let line = self.lines.get(cursor.line)?; + cursor.index = line + .text() + .char_indices() + .filter_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) }) + .next() + .unwrap_or(0); + cursor_x_opt = None; + } + Motion::End => { + let mut layout_cursor = self.layout_cursor(font_system, cursor)?; + layout_cursor.glyph = usize::MAX; + #[allow(unused_assignments)] + { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::LayoutCursor(layout_cursor), + )?; + } + cursor_x_opt = None; + } + Motion::ParagraphStart => { + cursor.index = 0; + cursor_x_opt = None; + } + Motion::ParagraphEnd => { + cursor.index = self.lines.get(cursor.line)?.text().len(); + cursor_x_opt = None; + } + Motion::PageUp => { + if let Some(height) = self.height_opt { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::Vertical(-height as i32), + )?; + } + } + Motion::PageDown => { + if let Some(height) = self.height_opt { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::Vertical(height as i32), + )?; + } + } + Motion::Vertical(px) => { + // TODO more efficient, use layout run line height + let lines = px / self.metrics().line_height as i32; + match lines.cmp(&0) { + cmp::Ordering::Less => { + for _ in 0..-lines { + (cursor, cursor_x_opt) = + self.cursor_motion(font_system, cursor, cursor_x_opt, Motion::Up)?; + } + } + cmp::Ordering::Greater => { + for _ in 0..lines { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::Down, + )?; + } + } + cmp::Ordering::Equal => {} + } + } + Motion::PreviousWord => { + let line = self.lines.get(cursor.line)?; + if cursor.index > 0 { + cursor.index = line + .text() + .unicode_word_indices() + .rev() + .map(|(i, _)| i) + .find(|&i| i < cursor.index) + .unwrap_or(0); + } else if cursor.line > 0 { + cursor.line -= 1; + cursor.index = self.lines.get(cursor.line)?.text().len(); + } + cursor_x_opt = None; + } + Motion::NextWord => { + let line = self.lines.get(cursor.line)?; + if cursor.index < line.text().len() { + cursor.index = line + .text() + .unicode_word_indices() + .map(|(i, word)| i + word.len()) + .find(|&i| i > cursor.index) + .unwrap_or(line.text().len()); + } else if cursor.line + 1 < self.lines.len() { + cursor.line += 1; + cursor.index = 0; + } + cursor_x_opt = None; + } + Motion::LeftWord => { + let rtl_opt = self + .line_shape(font_system, cursor.line) + .map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::NextWord, + )?; + } else { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::PreviousWord, + )?; + } + } + } + Motion::RightWord => { + let rtl_opt = self + .line_shape(font_system, cursor.line) + .map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::PreviousWord, + )?; + } else { + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::NextWord, + )?; + } + } + } + Motion::BufferStart => { + cursor.line = 0; + cursor.index = 0; + cursor_x_opt = None; + } + Motion::BufferEnd => { + cursor.line = self.lines.len().saturating_sub(1); + cursor.index = self.lines.get(cursor.line)?.text().len(); + cursor_x_opt = None; + } + Motion::GotoLine(line) => { + let mut layout_cursor = self.layout_cursor(font_system, cursor)?; + layout_cursor.line = line; + (cursor, cursor_x_opt) = self.cursor_motion( + font_system, + cursor, + cursor_x_opt, + Motion::LayoutCursor(layout_cursor), + )?; + } + } + Some((cursor, cursor_x_opt)) + } + + /// Draw the buffer + #[cfg(feature = "swash")] + pub fn draw( + &self, + font_system: &mut FontSystem, + cache: &mut crate::SwashCache, + color: Color, + mut f: F, + ) where + F: FnMut(i32, i32, u32, u32, Color), + { + for run in self.layout_runs() { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((0., 0.), 1.0); + + let glyph_color = match glyph.color_opt { + Some(some) => some, + None => color, + }; + + cache.with_pixels( + font_system, + physical_glyph.cache_key, + glyph_color, + |x, y, color| { + f( + physical_glyph.x + x, + run.line_y as i32 + physical_glyph.y + y, + 1, + 1, + color, + ); + }, + ); + } + } + } +} + +impl BorrowedWithFontSystem<'_, Buffer> { + /// Shape lines until cursor, also scrolling to include cursor in view + pub fn shape_until_cursor(&mut self, cursor: Cursor, prune: bool) { + self.inner + .shape_until_cursor(self.font_system, cursor, prune); + } + + /// Shape lines until scroll + pub fn shape_until_scroll(&mut self, prune: bool) { + self.inner.shape_until_scroll(self.font_system, prune); + } + + /// Shape the provided line index and return the result + pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> { + self.inner.line_shape(self.font_system, line_i) + } + + /// Lay out the provided line index and return the result + pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> { + self.inner.line_layout(self.font_system, line_i) + } + + /// Set the current [`Metrics`] + /// + /// # Panics + /// + /// Will panic if `metrics.font_size` is zero. + pub fn set_metrics(&mut self, metrics: Metrics) { + self.inner.set_metrics(self.font_system, metrics); + } + + /// Set the current [`Wrap`] + pub fn set_wrap(&mut self, wrap: Wrap) { + self.inner.set_wrap(self.font_system, wrap); + } + + /// Set the current buffer dimensions + pub fn set_size(&mut self, width_opt: Option, height_opt: Option) { + self.inner.set_size(self.font_system, width_opt, height_opt); + } + + /// Set the current [`Metrics`] and buffer dimensions at the same time + /// + /// # Panics + /// + /// Will panic if `metrics.font_size` is zero. + pub fn set_metrics_and_size( + &mut self, + metrics: Metrics, + width_opt: Option, + height_opt: Option, + ) { + self.inner + .set_metrics_and_size(self.font_system, metrics, width_opt, height_opt); + } + + /// Set tab width (number of spaces between tab stops) + pub fn set_tab_width(&mut self, tab_width: u16) { + self.inner.set_tab_width(self.font_system, tab_width); + } + + /// Set text of buffer, using provided attributes for each line by default + pub fn set_text(&mut self, text: &str, attrs: &Attrs, shaping: Shaping) { + self.inner.set_text(self.font_system, text, attrs, shaping); + } + + /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) + /// + /// ``` + /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; + /// # let mut font_system = FontSystem::new(); + /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); + /// let attrs = Attrs::new().family(Family::Serif); + /// buffer.set_rich_text( + /// &mut font_system, + /// [ + /// ("hello, ", attrs.clone()), + /// ("cosmic\ntext", attrs.clone().family(Family::Monospace)), + /// ], + /// &attrs, + /// Shaping::Advanced, + /// None, + /// ); + /// ``` + pub fn set_rich_text<'r, 's, I>( + &mut self, + spans: I, + default_attrs: &Attrs, + shaping: Shaping, + alignment: Option, + ) where + I: IntoIterator)>, + { + self.inner + .set_rich_text(self.font_system, spans, default_attrs, shaping, alignment); + } + + /// Apply a [`Motion`] to a [`Cursor`] + pub fn cursor_motion( + &mut self, + cursor: Cursor, + cursor_x_opt: Option, + motion: Motion, + ) -> Option<(Cursor, Option)> { + self.inner + .cursor_motion(self.font_system, cursor, cursor_x_opt, motion) + } + + /// Draw the buffer + #[cfg(feature = "swash")] + pub fn draw(&mut self, cache: &mut crate::SwashCache, color: Color, f: F) + where + F: FnMut(i32, i32, u32, u32, Color), + { + self.inner.draw(self.font_system, cache, color, f); + } +} diff --git a/vendor/cosmic-text/src/buffer_line.rs b/vendor/cosmic-text/src/buffer_line.rs new file mode 100644 index 0000000..6929e03 --- /dev/null +++ b/vendor/cosmic-text/src/buffer_line.rs @@ -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, + shape_opt: Cached, + layout_opt: Cached>, + shaping: Shaping, + metadata: Option, +} + +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>( + 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>( + &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>( + &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 { + 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) -> 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, + wrap: Wrap, + match_mono_width: Option, + 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> { + 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 { + 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 + } +} diff --git a/vendor/cosmic-text/src/cached.rs b/vendor/cosmic-text/src/cached.rs new file mode 100644 index 0000000..cf7e1ac --- /dev/null +++ b/vendor/cosmic-text/src/cached.rs @@ -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 { + Empty, + Unused(T), + Used(T), +} + +impl Cached { + /// 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 { + 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 { + 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); + } +} diff --git a/vendor/cosmic-text/src/cursor.rs b/vendor/cosmic-text/src/cursor.rs new file mode 100644 index 0000000..e6358c6 --- /dev/null +++ b/vendor/cosmic-text/src/cursor.rs @@ -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, + } + } +} diff --git a/vendor/cosmic-text/src/edit/editor.rs b/vendor/cosmic-text/src/edit/editor.rs new file mode 100644 index 0000000..c1b63d8 --- /dev/null +++ b/vendor/cosmic-text/src/edit/editor.rs @@ -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, + selection: Selection, + cursor_moved: bool, + auto_indent: bool, + change: Option, +} + +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>) -> 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( + &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, + ) -> 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 { + 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 { + 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( + &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, + ); + } +} diff --git a/vendor/cosmic-text/src/edit/mod.rs b/vendor/cosmic-text/src/edit/mod.rs new file mode 100644 index 0000000..a27fa41 --- /dev/null +++ b/vendor/cosmic-text/src/edit/mod.rs @@ -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), +} + +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 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> for BufferRef<'_> { + fn from(arc: Arc) -> 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, +} + +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 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 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) -> Cursor; + + /// Copy selection + fn copy_selection(&self) -> Option; + + /// 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) { + 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; + + /// 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) -> 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); + } +} diff --git a/vendor/cosmic-text/src/edit/syntect.rs b/vendor/cosmic-text/src/edit/syntect.rs new file mode 100644 index 0000000..57eb9c7 --- /dev/null +++ b/vendor/cosmic-text/src/edit/syntect.rs @@ -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>, + syntax_system: &'syntax_system SyntaxSystem, + theme_name: &str, + ) -> Option { + 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>( + &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(&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) -> Cursor { + self.editor.insert_at(cursor, data, attrs_list) + } + + fn copy_selection(&self) -> Option { + 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 { + 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>(&mut self, path: P, attrs: crate::Attrs) -> io::Result<()> { + self.inner.load_text(self.font_system, path, attrs) + } + + #[cfg(feature = "swash")] + pub fn draw(&mut self, cache: &mut crate::SwashCache, f: F) + where + F: FnMut(i32, i32, u32, u32, Color), + { + self.inner.draw(self.font_system, cache, f); + } +} diff --git a/vendor/cosmic-text/src/edit/vi.rs b/vendor/cosmic-text/src/edit/vi.rs new file mode 100644 index 0000000..0719e7b --- /dev/null +++ b/vendor/cosmic-text/src/edit/vi.rs @@ -0,0 +1,1173 @@ +use alloc::{collections::BTreeMap, string::String}; +use core::cmp; +use modit::{Event, Key, Parser, TextObject, WordIter}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + Action, AttrsList, BorrowedWithFontSystem, BufferRef, Change, Color, Cursor, Edit, FontSystem, + Motion, Selection, SyntaxEditor, SyntaxTheme, +}; + +pub use modit::{ViMode, ViParser}; + +fn undo_2_action<'buffer, E: Edit<'buffer>>( + editor: &mut E, + action: cosmic_undo_2::Action<&Change>, +) { + match action { + cosmic_undo_2::Action::Do(change) => { + editor.apply_change(change); + } + cosmic_undo_2::Action::Undo(change) => { + //TODO: make this more efficient + let mut reversed = change.clone(); + reversed.reverse(); + editor.apply_change(&reversed); + } + } +} + +fn finish_change<'buffer, E: Edit<'buffer>>( + editor: &mut E, + commands: &mut cosmic_undo_2::Commands, + changed: &mut bool, + pivot: Option, +) -> Option { + //TODO: join changes together + match editor.finish_change() { + Some(change) => { + if !change.items.is_empty() { + commands.push(change.clone()); + *changed = eval_changed(commands, pivot); + } + Some(change) + } + None => None, + } +} + +/// Evaluate if an [`ViEditor`] changed based on its last saved state. +fn eval_changed(commands: &cosmic_undo_2::Commands, pivot: Option) -> bool { + // Editors are considered modified if the current change index is unequal to the last + // saved index or if `pivot` is None. + // The latter case handles a never saved editor with a current command index of None. + // Check the unit tests for an example. + match (commands.current_command_index(), pivot) { + (Some(current), Some(pivot)) => current != pivot, + // Edge case for an editor with neither a save point nor any changes. + // This could be a new editor or an editor without a save point where undo() is called + // until the editor is fresh. + (None, None) => false, + // Default to true because it's safer to assume a buffer has been modified so as to not + // lose changes + _ => true, + } +} + +fn search<'buffer, E: Edit<'buffer>>(editor: &mut E, value: &str, forwards: bool) -> bool { + let mut cursor = editor.cursor(); + let start_line = cursor.line; + if forwards { + while cursor.line < editor.with_buffer(|buffer| buffer.lines.len()) { + if let Some(index) = editor.with_buffer(|buffer| { + buffer.lines[cursor.line] + .text() + .match_indices(value) + .filter_map(|(i, _)| { + if cursor.line != start_line || i > cursor.index { + Some(i) + } else { + None + } + }) + .next() + }) { + cursor.index = index; + editor.set_cursor(cursor); + return true; + } + + cursor.line += 1; + } + } else { + cursor.line += 1; + while cursor.line > 0 { + cursor.line -= 1; + + if let Some(index) = editor.with_buffer(|buffer| { + buffer.lines[cursor.line] + .text() + .rmatch_indices(value) + .filter_map(|(i, _)| { + if cursor.line != start_line || i < cursor.index { + Some(i) + } else { + None + } + }) + .next() + }) { + cursor.index = index; + editor.set_cursor(cursor); + return true; + } + } + } + false +} + +fn select_in<'buffer, E: Edit<'buffer>>(editor: &mut E, start_c: char, end_c: char, include: bool) { + // Find the largest encompasing object, or if there is none, find the next one. + let cursor = editor.cursor(); + let (start, end) = editor.with_buffer(|buffer| { + // Search forwards for isolated end character, counting start and end characters found + let mut end = cursor; + let mut starts = 0; + let mut ends = 0; + 'find_end: loop { + let line = &buffer.lines[end.line]; + let text = line.text(); + for (i, c) in text[end.index..].char_indices() { + if c == end_c { + ends += 1; + } else if c == start_c { + starts += 1; + } + if ends > starts { + end.index += if include { i + c.len_utf8() } else { i }; + break 'find_end; + } + } + if end.line + 1 < buffer.lines.len() { + end.line += 1; + end.index = 0; + } else { + break 'find_end; + } + } + + // Search backwards to resolve starts and ends + let mut start = cursor; + 'find_start: loop { + let line = &buffer.lines[start.line]; + let text = line.text(); + for (i, c) in text[..start.index].char_indices().rev() { + if c == start_c { + starts += 1; + } else if c == end_c { + ends += 1; + } + if starts >= ends { + start.index = if include { i } else { i + c.len_utf8() }; + break 'find_start; + } + } + if start.line > 0 { + start.line -= 1; + start.index = buffer.lines[start.line].text().len(); + } else { + break 'find_start; + } + } + + (start, end) + }); + + editor.set_selection(Selection::Normal(start)); + editor.set_cursor(end); +} + +#[derive(Debug)] +pub struct ViEditor<'syntax_system, 'buffer> { + editor: SyntaxEditor<'syntax_system, 'buffer>, + parser: ViParser, + passthrough: bool, + registers: BTreeMap, + search_opt: Option<(String, bool)>, + commands: cosmic_undo_2::Commands, + changed: bool, + save_pivot: Option, +} + +impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { + pub fn new(editor: SyntaxEditor<'syntax_system, 'buffer>) -> Self { + Self { + editor, + parser: ViParser::new(), + passthrough: false, + registers: BTreeMap::new(), + search_opt: None, + commands: cosmic_undo_2::Commands::new(), + changed: false, + save_pivot: None, + } + } + + /// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing + pub fn update_theme(&mut self, theme_name: &str) -> bool { + self.editor.update_theme(theme_name) + } + + /// 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>( + &mut self, + font_system: &mut FontSystem, + path: P, + attrs: crate::Attrs, + ) -> std::io::Result<()> { + self.editor.load_text(font_system, path, attrs) + } + + /// Get the default background color + pub fn background_color(&self) -> Color { + self.editor.background_color() + } + + /// Get the default foreground (text) color + pub fn foreground_color(&self) -> Color { + self.editor.foreground_color() + } + + /// Get the default cursor color + pub fn cursor_color(&self) -> Color { + self.editor.cursor_color() + } + + /// Get the default selection color + pub fn selection_color(&self) -> Color { + self.editor.selection_color() + } + + /// Get the current syntect theme + pub fn theme(&self) -> &SyntaxTheme { + self.editor.theme() + } + + /// Get changed flag + pub fn changed(&self) -> bool { + self.changed + } + + /// Set changed flag + pub fn set_changed(&mut self, changed: bool) { + self.changed = changed; + } + + /// Set current change as the save (or pivot) point. + /// + /// A pivot point is the last saved index. Anything before or after the pivot indicates that + /// the editor has been changed or is unsaved. + /// + /// Undoing changes down to the pivot point sets the editor as unchanged. + /// Redoing changes up to the pivot point sets the editor as unchanged. + /// + /// Undoing or redoing changes beyond the pivot point sets the editor to changed. + pub fn save_point(&mut self) { + self.save_pivot = Some(self.commands.current_command_index().unwrap_or_default()); + self.changed = false; + } + + /// Set passthrough mode (true will turn off vi features) + pub fn set_passthrough(&mut self, passthrough: bool) { + if passthrough != self.passthrough { + self.passthrough = passthrough; + self.with_buffer_mut(|buffer| buffer.set_redraw(true)); + } + } + + /// Get current vi parser + pub fn parser(&self) -> &ViParser { + &self.parser + } + + /// Redo a change + pub fn redo(&mut self) { + log::debug!("Redo"); + for action in self.commands.redo() { + undo_2_action(&mut self.editor, action); + } + self.changed = eval_changed(&self.commands, self.save_pivot); + } + + /// Undo a change + pub fn undo(&mut self) { + log::debug!("Undo"); + for action in self.commands.undo() { + undo_2_action(&mut self.editor, action); + } + self.changed = eval_changed(&self.commands, self.save_pivot); + } + + #[cfg(feature = "swash")] + pub fn draw(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, mut f: F) + where + F: FnMut(i32, i32, u32, u32, Color), + { + let background_color = self.background_color(); + let foreground_color = self.foreground_color(); + let cursor_color = self.cursor_color(); + let selection_color = self.selection_color(); + self.with_buffer(|buffer| { + let size = buffer.size(); + if let Some(width) = size.0 { + if let Some(height) = size.1 { + f(0, 0, width as u32, height as u32, background_color); + } + } + let font_size = buffer.metrics().font_size; + 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; + + let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32, f32)> { + //TODO: better calculation of width + let default_width = font_size / 2.0; + if cursor.line == line_i { + for (glyph_i, glyph) in run.glyphs.iter().enumerate() { + 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 width = glyph.w / (total as f32); + let offset = (before as f32) * width; + return Some((glyph_i, offset, width)); + } + } + match run.glyphs.last() { + Some(glyph) => { + if cursor.index == glyph.end { + return Some((run.glyphs.len(), 0.0, default_width)); + } + } + None => { + return Some((0, 0.0, default_width)); + } + } + } + None + }; + + // Highlight selection + if let Some((start, end)) = self.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((cursor_glyph, cursor_glyph_offset, cursor_glyph_width)) = + cursor_glyph_opt(&self.cursor()) + { + let block_cursor = if self.passthrough { + false + } else { + match self.parser.mode { + ViMode::Insert | ViMode::Replace => false, + _ => true, /*TODO: determine block cursor in other modes*/ + } + }; + + let (start_x, end_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, + (glyph.x + glyph.w - cursor_glyph_offset - cursor_glyph_width) + as i32, + ) + } else { + ( + (glyph.x + cursor_glyph_offset) as i32, + (glyph.x + cursor_glyph_offset + cursor_glyph_width) as i32, + ) + } + } + None => match run.glyphs.last() { + Some(glyph) => { + // End of last glyph + if glyph.level.is_rtl() { + (glyph.x as i32, (glyph.x - cursor_glyph_width) as i32) + } else { + ( + (glyph.x + glyph.w) as i32, + (glyph.x + glyph.w + cursor_glyph_width) as i32, + ) + } + } + None => { + // Start of empty line + (0, cursor_glyph_width as i32) + } + }, + }; + + if block_cursor { + let left_x = cmp::min(start_x, end_x); + let right_x = cmp::max(start_x, end_x); + f( + left_x, + line_top as i32, + (right_x - left_x) as u32, + line_height as u32, + selection_color, + ); + } else { + f( + start_x, + line_top as i32, + 1, + line_height as u32, + cursor_color, + ); + } + } + + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((0., 0.), 1.0); + + let glyph_color = match glyph.color_opt { + Some(some) => some, + None => foreground_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 ViEditor<'_, '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) { + 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) -> Cursor { + self.editor.insert_at(cursor, data, attrs_list) + } + + fn copy_selection(&self) -> Option { + 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 { + finish_change( + &mut self.editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ) + } + + fn action(&mut self, font_system: &mut FontSystem, action: Action) { + log::debug!("Action {:?}", action); + + let editor = &mut self.editor; + + // Ensure a change is always started + editor.start_change(); + + if self.passthrough { + editor.action(font_system, action); + // Always finish change when passing through (TODO: group changes) + finish_change( + editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ); + return; + } + + let key = match action { + //TODO: this leaves lots of room for issues in translation, should we directly accept Key? + Action::Backspace => Key::Backspace, + Action::Delete => Key::Delete, + Action::Motion(Motion::Down) => Key::Down, + Action::Motion(Motion::End) => Key::End, + Action::Enter => Key::Enter, + Action::Escape => Key::Escape, + Action::Motion(Motion::Home) => Key::Home, + Action::Indent => Key::Tab, + Action::Insert(c) => Key::Char(c), + Action::Motion(Motion::Left) => Key::Left, + Action::Motion(Motion::PageDown) => Key::PageDown, + Action::Motion(Motion::PageUp) => Key::PageUp, + Action::Motion(Motion::Right) => Key::Right, + Action::Unindent => Key::Backtab, + Action::Motion(Motion::Up) => Key::Up, + _ => { + log::debug!("Pass through action {:?}", action); + editor.action(font_system, action); + // Always finish change when passing through (TODO: group changes) + finish_change( + editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ); + return; + } + }; + + let has_selection = !matches!(editor.selection(), Selection::None); + + self.parser.parse(key, has_selection, |event| { + log::debug!(" Event {:?}", event); + let action = match event { + Event::AutoIndent => { + log::info!("TODO: AutoIndent"); + return; + } + Event::Backspace => Action::Backspace, + Event::BackspaceInLine => { + let cursor = editor.cursor(); + if cursor.index > 0 { + Action::Backspace + } else { + return; + } + } + Event::ChangeStart => { + editor.start_change(); + return; + } + Event::ChangeFinish => { + finish_change( + editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ); + return; + } + Event::Delete => Action::Delete, + Event::DeleteInLine => { + let cursor = editor.cursor(); + if cursor.index + < editor.with_buffer(|buffer| buffer.lines[cursor.line].text().len()) + { + Action::Delete + } else { + return; + } + } + Event::Escape => Action::Escape, + Event::Insert(c) => Action::Insert(c), + Event::NewLine => Action::Enter, + Event::Put { register, after } => { + if let Some((selection, data)) = self.registers.get(®ister) { + editor.start_change(); + if editor.delete_selection() { + editor.insert_string(data, None); + } else { + match selection { + Selection::None | Selection::Normal(_) | Selection::Word(_) => { + let mut cursor = editor.cursor(); + if after { + editor.with_buffer(|buffer| { + let text = buffer.lines[cursor.line].text(); + if let Some(c) = text[cursor.index..].chars().next() { + cursor.index += c.len_utf8(); + } + }); + editor.set_cursor(cursor); + } + editor.insert_at(cursor, data, None); + } + Selection::Line(_) => { + let mut cursor = editor.cursor(); + if after { + // Insert at next line + cursor.line += 1; + } else { + // Previous line will be moved down, so set cursor to next line + cursor.line += 1; + editor.set_cursor(cursor); + cursor.line -= 1; + } + // Insert at start of line + cursor.index = 0; + + // Insert text + editor.insert_at(cursor, "\n", None); + editor.insert_at(cursor, data, None); + + //TODO: Hack to allow immediate up/down + editor.shape_as_needed(font_system, false); + + // Move to inserted line, preserving cursor x position + if after { + editor.action(font_system, Action::Motion(Motion::Down)); + } else { + editor.action(font_system, Action::Motion(Motion::Up)); + } + } + } + } + finish_change( + editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ); + } + return; + } + Event::Redraw => { + editor.with_buffer_mut(|buffer| buffer.set_redraw(true)); + return; + } + Event::SelectClear => { + editor.set_selection(Selection::None); + return; + } + Event::SelectStart => { + let cursor = editor.cursor(); + editor.set_selection(Selection::Normal(cursor)); + return; + } + Event::SelectLineStart => { + let cursor = editor.cursor(); + editor.set_selection(Selection::Line(cursor)); + return; + } + Event::SelectTextObject(text_object, include) => { + match text_object { + TextObject::AngleBrackets => select_in(editor, '<', '>', include), + TextObject::CurlyBrackets => select_in(editor, '{', '}', include), + TextObject::DoubleQuotes => select_in(editor, '"', '"', include), + TextObject::Parentheses => select_in(editor, '(', ')', include), + TextObject::Search { forwards } => { + if let Some((value, _)) = &self.search_opt { + if search(editor, value, forwards) { + let mut cursor = editor.cursor(); + editor.set_selection(Selection::Normal(cursor)); + //TODO: traverse lines if necessary + cursor.index += value.len(); + editor.set_cursor(cursor); + } + } + } + TextObject::SingleQuotes => select_in(editor, '\'', '\'', include), + TextObject::SquareBrackets => select_in(editor, '[', ']', include), + TextObject::Ticks => select_in(editor, '`', '`', include), + TextObject::Word(word) => { + let mut cursor = editor.cursor(); + let mut selection = editor.selection(); + editor.with_buffer(|buffer| { + let text = buffer.lines[cursor.line].text(); + match WordIter::new(text, word) + .find(|&(i, w)| i <= cursor.index && i + w.len() > cursor.index) + { + Some((i, w)) => { + cursor.index = i; + selection = Selection::Normal(cursor); + cursor.index += w.len(); + } + None => { + //TODO + } + } + }); + editor.set_selection(selection); + editor.set_cursor(cursor); + } + _ => { + log::info!("TODO: {:?}", text_object); + } + } + return; + } + Event::SetSearch(value, forwards) => { + self.search_opt = Some((value, forwards)); + return; + } + Event::ShiftLeft => Action::Unindent, + Event::ShiftRight => Action::Indent, + Event::SwapCase => { + log::info!("TODO: SwapCase"); + return; + } + Event::Undo => { + for action in self.commands.undo() { + undo_2_action(editor, action); + } + return; + } + Event::Yank { register } => { + if let Some(data) = editor.copy_selection() { + self.registers.insert(register, (editor.selection(), data)); + } + return; + } + Event::Motion(motion) => { + match motion { + modit::Motion::Around => { + //TODO: what to do for this psuedo-motion? + return; + } + modit::Motion::Down => Action::Motion(Motion::Down), + modit::Motion::End => Action::Motion(Motion::End), + modit::Motion::GotoLine(line) => { + Action::Motion(Motion::GotoLine(line.saturating_sub(1))) + } + modit::Motion::GotoEof => Action::Motion(Motion::GotoLine( + editor.with_buffer(|buffer| buffer.lines.len().saturating_sub(1)), + )), + modit::Motion::Home => Action::Motion(Motion::Home), + modit::Motion::Inside => { + //TODO: what to do for this psuedo-motion? + return; + } + modit::Motion::Left => Action::Motion(Motion::Left), + modit::Motion::LeftInLine => { + let cursor = editor.cursor(); + if cursor.index > 0 { + Action::Motion(Motion::Left) + } else { + return; + } + } + modit::Motion::Line => { + //TODO: what to do for this psuedo-motion? + return; + } + modit::Motion::NextChar(find_c) => { + let mut cursor = editor.cursor(); + editor.with_buffer(|buffer| { + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + if let Some((i, _)) = text[cursor.index..] + .char_indices() + .find(|&(i, c)| i > 0 && c == find_c) + { + cursor.index += i; + } + } + }); + editor.set_cursor(cursor); + return; + } + modit::Motion::NextCharTill(find_c) => { + let mut cursor = editor.cursor(); + editor.with_buffer(|buffer| { + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + let mut last_i = 0; + for (i, c) in text[cursor.index..].char_indices() { + if last_i > 0 && c == find_c { + cursor.index += last_i; + break; + } else { + last_i = i; + } + } + } + }); + editor.set_cursor(cursor); + return; + } + modit::Motion::NextSearch => match &self.search_opt { + Some((value, forwards)) => { + search(editor, value, *forwards); + return; + } + None => return, + }, + modit::Motion::NextWordEnd(word) => { + let mut cursor = editor.cursor(); + editor.with_buffer(|buffer| { + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + cursor.index = WordIter::new(text, word) + .map(|(i, w)| { + i + w + .char_indices() + .last() + .map(|(i, _)| i) + .unwrap_or(0) + }) + .find(|&i| i > cursor.index) + .unwrap_or(text.len()); + if cursor.index == text.len() { + // Try again, searching next line + continue; + } + } else if cursor.line + 1 < buffer.lines.len() { + // Go to next line and rerun loop + cursor.line += 1; + cursor.index = 0; + continue; + } + break; + } + }); + editor.set_cursor(cursor); + return; + } + modit::Motion::NextWordStart(word) => { + let mut cursor = editor.cursor(); + editor.with_buffer(|buffer| { + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + cursor.index = WordIter::new(text, word) + .map(|(i, _)| i) + .find(|&i| i > cursor.index) + .unwrap_or(text.len()); + if cursor.index == text.len() { + // Try again, searching next line + continue; + } + } else if cursor.line + 1 < buffer.lines.len() { + // Go to next line and rerun loop + cursor.line += 1; + cursor.index = 0; + continue; + } + break; + } + }); + editor.set_cursor(cursor); + return; + } + modit::Motion::PageDown => Action::Motion(Motion::PageDown), + modit::Motion::PageUp => Action::Motion(Motion::PageUp), + modit::Motion::PreviousChar(find_c) => { + let mut cursor = editor.cursor(); + editor.with_buffer(|buffer| { + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + if let Some((i, _)) = text[..cursor.index] + .char_indices() + .rfind(|&(_, c)| c == find_c) + { + cursor.index = i; + } + } + }); + editor.set_cursor(cursor); + return; + } + modit::Motion::PreviousCharTill(find_c) => { + let mut cursor = editor.cursor(); + editor.with_buffer(|buffer| { + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + if let Some(i) = text[..cursor.index] + .char_indices() + .filter_map(|(i, c)| { + if c == find_c { + let end = i + c.len_utf8(); + if end < cursor.index { + return Some(end); + } + } + None + }) + .last() + { + cursor.index = i; + } + } + }); + editor.set_cursor(cursor); + return; + } + modit::Motion::PreviousSearch => match &self.search_opt { + Some((value, forwards)) => { + search(editor, value, !*forwards); + return; + } + None => return, + }, + modit::Motion::PreviousWordEnd(word) => { + let mut cursor = editor.cursor(); + editor.with_buffer(|buffer| { + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + cursor.index = WordIter::new(text, word) + .map(|(i, w)| { + i + w + .char_indices() + .last() + .map(|(i, _)| i) + .unwrap_or(0) + }) + .filter(|&i| i < cursor.index) + .last() + .unwrap_or(0); + if cursor.index == 0 { + // Try again, searching previous line + continue; + } + } else if cursor.line > 0 { + // Go to previous line and rerun loop + cursor.line -= 1; + cursor.index = buffer.lines[cursor.line].text().len(); + continue; + } + break; + } + }); + editor.set_cursor(cursor); + return; + } + modit::Motion::PreviousWordStart(word) => { + let mut cursor = editor.cursor(); + editor.with_buffer(|buffer| { + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + cursor.index = WordIter::new(text, word) + .map(|(i, _)| i) + .filter(|&i| i < cursor.index) + .last() + .unwrap_or(0); + if cursor.index == 0 { + // Try again, searching previous line + continue; + } + } else if cursor.line > 0 { + // Go to previous line and rerun loop + cursor.line -= 1; + cursor.index = buffer.lines[cursor.line].text().len(); + continue; + } + break; + } + }); + editor.set_cursor(cursor); + return; + } + modit::Motion::Right => Action::Motion(Motion::Right), + modit::Motion::RightInLine => { + let cursor = editor.cursor(); + if cursor.index + < editor + .with_buffer(|buffer| buffer.lines[cursor.line].text().len()) + { + Action::Motion(Motion::Right) + } else { + return; + } + } + modit::Motion::ScreenHigh => { + //TODO: is this efficient? + if let Some(line_i) = editor.with_buffer(|buffer| { + buffer.layout_runs().next().map(|first| first.line_i) + }) { + Action::Motion(Motion::GotoLine(line_i)) + } else { + return; + } + } + modit::Motion::ScreenLow => { + //TODO: is this efficient? + if let Some(line_i) = editor.with_buffer(|buffer| { + buffer.layout_runs().last().map(|last| last.line_i) + }) { + Action::Motion(Motion::GotoLine(line_i)) + } else { + return; + } + } + modit::Motion::ScreenMiddle => { + //TODO: is this efficient? + let action_opt = editor.with_buffer(|buffer| { + let mut layout_runs = buffer.layout_runs(); + + match (layout_runs.next(), layout_runs.last()) { + (Some(first), Some(last)) => Some(Action::Motion( + Motion::GotoLine((last.line_i + first.line_i) / 2), + )), + _ => None, + } + }); + match action_opt { + Some(action) => action, + None => return, + } + } + modit::Motion::Selection => { + //TODO: what to do for this psuedo-motion? + return; + } + modit::Motion::SoftHome => Action::Motion(Motion::SoftHome), + modit::Motion::Up => Action::Motion(Motion::Up), + } + } + }; + editor.action(font_system, action); + }); + } + + fn cursor_position(&self) -> Option<(i32, i32)> { + self.editor.cursor_position() + } +} + +impl BorrowedWithFontSystem<'_, ViEditor<'_, '_>> { + /// 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>( + &mut self, + path: P, + attrs: crate::Attrs, + ) -> std::io::Result<()> { + self.inner.load_text(self.font_system, path, attrs) + } + + #[cfg(feature = "swash")] + pub fn draw(&mut self, cache: &mut crate::SwashCache, f: F) + where + F: FnMut(i32, i32, u32, u32, Color), + { + self.inner.draw(self.font_system, cache, f); + } +} diff --git a/vendor/cosmic-text/src/font/fallback/macos.rs b/vendor/cosmic-text/src/font/fallback/macos.rs new file mode 100644 index 0000000..7eddd48 --- /dev/null +++ b/vendor/cosmic-text/src/font/fallback/macos.rs @@ -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"], + _ => &[], + } +} diff --git a/vendor/cosmic-text/src/font/fallback/mod.rs b/vendor/cosmic-text/src/font/fallback/mod.rs new file mode 100644 index 0000000..032f112 --- /dev/null +++ b/vendor/cosmic-text/src/font/fallback/mod.rs @@ -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, + forbidden_fallback_range: Range, + // PERF: Consider using NoHashHasher since Script is just an integer + script_fallback_ranges: HashMap>, + 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, + codepoint_non_matches: Option, + font_weight: u16, + id: fontdb::ID, +} + +pub struct FontFallbackIter<'a> { + font_system: &'a mut FontSystem, + font_match_keys: &'a [FontMatchKey], + skip_font_id: Option, + 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, + 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<::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; + fn next(&mut self) -> Option { + 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 + } +} diff --git a/vendor/cosmic-text/src/font/fallback/other.rs b/vendor/cosmic-text/src/font/fallback/other.rs new file mode 100644 index 0000000..7659d80 --- /dev/null +++ b/vendor/cosmic-text/src/font/fallback/other.rs @@ -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] { + &[] +} diff --git a/vendor/cosmic-text/src/font/fallback/unix.rs b/vendor/cosmic-text/src/font/fallback/unix.rs new file mode 100644 index 0000000..12c4a7e --- /dev/null +++ b/vendor/cosmic-text/src/font/fallback/unix.rs @@ -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"], + _ => &[], + } +} diff --git a/vendor/cosmic-text/src/font/fallback/windows.rs b/vendor/cosmic-text/src/font/fallback/windows.rs new file mode 100644 index 0000000..0737263 --- /dev/null +++ b/vendor/cosmic-text/src/font/fallback/windows.rs @@ -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"], + _ => &[], + } +} diff --git a/vendor/cosmic-text/src/font/mod.rs b/vendor/cosmic-text/src/font/mod.rs new file mode 100644 index 0000000..cb9182a --- /dev/null +++ b/vendor/cosmic-text/src/font/mod.rs @@ -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 + Send + Sync>, + + #[covariant] + dependent: RustybuzzFace, + } +); + +struct FontMonospaceFallback { + monospace_em_width: Option, + scripts: Vec<[u8; 4]>, + unicode_codepoints: Vec, +} + +/// A font +pub struct Font { + #[cfg(feature = "swash")] + swash: (u32, swash::CacheKey), + rustybuzz: OwnedFace, + #[cfg(not(feature = "peniko"))] + data: Arc + Send + Sync>, + #[cfg(feature = "peniko")] + data: peniko::Font, + id: fontdb::ID, + monospace_fallback: Option, +} + +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 { + 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 { + 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()); + } +} diff --git a/vendor/cosmic-text/src/font/system.rs b/vendor/cosmic-text/src/font/system.rs new file mode 100644 index 0000000..d3a91d9 --- /dev/null +++ b/vendor/cosmic-text/src/font/system.rs @@ -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, + not_supported: Vec, +} + +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>>, + + /// Sorted unique ID's of all Monospace fonts in DB + monospace_font_ids: Vec, + + /// 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>, + + /// Cache for font codepoint support info + font_codepoint_support_info_cache: HashMap, + + /// Cache for font matches. + font_matches_cache: HashMap>>, + + /// Scratch buffer for shaping and laying out. + pub(crate) shape_buffer: ShapeBuffer, + + /// Buffer for use in `FontFallbackIter`. + pub(crate) monospace_fallbacks_buffer: BTreeSet, + + /// Cache for shaped runs + #[cfg(feature = "shape-run-cache")] + pub shape_run_cache: crate::ShapeRunCache, + + /// List of fallbacks + pub(crate) dyn_fallback: Box, + + /// 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) -> 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::>(); + monospace_font_ids.sort(); + + let mut per_script_monospace_font_ids: HashMap<[u8; 4], BTreeSet> = + 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> { + 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, + ) -> Vec { + let mut ret = scripts + .filter_map(|script| self.per_script_monospace_font_ids.get(&script)) + .flat_map(|ids| ids.iter().copied()) + .collect::>(); + ret.sort(); + ret.dedup(); + ret + } + + #[inline(always)] + pub fn get_font_supported_codepoints_in_word( + &mut self, + id: fontdb::ID, + word: &str, + ) -> Option { + 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> { + // 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::>(); + + // 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) { + #[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) { + 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 Deref for BorrowedWithFontSystem<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl DerefMut for BorrowedWithFontSystem<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner + } +} diff --git a/vendor/cosmic-text/src/glyph_cache.rs b/vendor/cosmic-text/src/glyph_cache.rs new file mode 100644 index 0000000..01ac1a4 --- /dev/null +++ b/vendor/cosmic-text/src/glyph_cache.rs @@ -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)); +} diff --git a/vendor/cosmic-text/src/layout.rs b/vendor/cosmic-text/src/layout.rs new file mode 100644 index 0000000..5f7287d --- /dev/null +++ b/vendor/cosmic-text/src/layout.rs @@ -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, + /// 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, + /// 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, + /// Glyphs in line + pub glyphs: Vec, +} + +/// 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"), + } + } +} diff --git a/vendor/cosmic-text/src/lib.rs b/vendor/cosmic-text/src/lib.rs new file mode 100644 index 0000000..53c5ea5 --- /dev/null +++ b/vendor/cosmic-text/src/lib.rs @@ -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; + +#[cfg(feature = "std")] +type HashMap = std::collections::HashMap; +#[cfg(not(feature = "std"))] +type HashMap = hashbrown::HashMap; diff --git a/vendor/cosmic-text/src/line_ending.rs b/vendor/cosmic-text/src/line_ending.rs new file mode 100644 index 0000000..13c1488 --- /dev/null +++ b/vendor/cosmic-text/src/line_ending.rs @@ -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, LineEnding); + fn next(&mut self) -> Option { + 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))); +} diff --git a/vendor/cosmic-text/src/math.rs b/vendor/cosmic-text/src/math.rs new file mode 100644 index 0000000..126cbf7 --- /dev/null +++ b/vendor/cosmic-text/src/math.rs @@ -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() +} diff --git a/vendor/cosmic-text/src/shape.rs b/vendor/cosmic-text/src/shape.rs new file mode 100644 index 0000000..07b6509 --- /dev/null +++ b/vendor/cosmic-text/src/shape.rs @@ -0,0 +1,1953 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#![allow(clippy::too_many_arguments)] + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use core::cmp::{max, min}; +use core::fmt; +use core::mem; +use core::ops::Range; +use unicode_script::{Script, UnicodeScript}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::fallback::FontFallbackIter; +use crate::{ + math, Align, AttrsList, CacheKeyFlags, Color, Font, FontSystem, LayoutGlyph, LayoutLine, + Metrics, Wrap, +}; + +/// The shaping strategy of some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Shaping { + /// Basic shaping with no font fallback. + /// + /// This shaping strategy is very cheap, but it will not display complex + /// scripts properly nor try to find missing glyphs in your system fonts. + /// + /// You should use this strategy when you have complete control of the text + /// and the font you are displaying in your application. + #[cfg(feature = "swash")] + Basic, + /// Advanced text shaping and font fallback. + /// + /// You will need to enable this strategy if the text contains a complex + /// script, the font used needs it, and/or multiple fonts in your system + /// may be needed to display all of the glyphs. + Advanced, +} + +impl Shaping { + fn run( + self, + glyphs: &mut Vec, + font_system: &mut FontSystem, + line: &str, + attrs_list: &AttrsList, + start_run: usize, + end_run: usize, + span_rtl: bool, + ) { + match self { + #[cfg(feature = "swash")] + Self::Basic => shape_skip(font_system, glyphs, line, attrs_list, start_run, end_run), + #[cfg(not(feature = "shape-run-cache"))] + Self::Advanced => shape_run( + glyphs, + font_system, + line, + attrs_list, + start_run, + end_run, + span_rtl, + ), + #[cfg(feature = "shape-run-cache")] + Self::Advanced => shape_run_cached( + glyphs, + font_system, + line, + attrs_list, + start_run, + end_run, + span_rtl, + ), + } + } +} + +/// A set of buffers containing allocations for shaped text. +#[derive(Default)] +pub struct ShapeBuffer { + /// Buffer for holding unicode text. + rustybuzz_buffer: Option, + + /// Temporary buffers for scripts. + scripts: Vec