diff --git a/internal/common/sharedfontique.rs b/internal/common/sharedfontique.rs index ce00d429f65..bf1eca75cf4 100644 --- a/internal/common/sharedfontique.rs +++ b/internal/common/sharedfontique.rs @@ -7,7 +7,7 @@ pub use ttf_parser; use std::collections::HashMap; use std::sync::Arc; -static COLLECTION: std::sync::LazyLock = std::sync::LazyLock::new(|| { +pub static COLLECTION: std::sync::LazyLock = std::sync::LazyLock::new(|| { let mut collection = fontique::Collection::new(fontique::CollectionOptions { shared: true, ..Default::default() @@ -55,8 +55,8 @@ pub fn get_collection() -> Collection { #[derive(Clone)] pub struct Collection { - inner: fontique::Collection, - source_cache: fontique::SourceCache, + pub inner: fontique::Collection, + pub source_cache: fontique::SourceCache, pub default_fonts: Arc>, } diff --git a/internal/compiler/passes/embed_glyphs.rs b/internal/compiler/passes/embed_glyphs.rs index 2576dd53cde..3d4c052d417 100644 --- a/internal/compiler/passes/embed_glyphs.rs +++ b/internal/compiler/passes/embed_glyphs.rs @@ -137,15 +137,11 @@ pub fn embed_glyphs<'a>( let font = { let mut query = collection.query(); - if let Some(ref family) = family { - query.set_families(std::iter::once(fontique::QueryFamily::from( - family.as_str(), - ))); + query.set_families(std::iter::once(if let Some(ref family) = family { + fontique::QueryFamily::from(family.as_str()) } else { - query.set_families(std::iter::once(fontique::QueryFamily::Generic( - fontique::GenericFamily::SansSerif, - ))); - } + fontique::QueryFamily::Generic(fontique::GenericFamily::SansSerif) + })); let mut font = None; diff --git a/internal/core/Cargo.toml b/internal/core/Cargo.toml index e5a337cbcb9..f46e9a9738a 100644 --- a/internal/core/Cargo.toml +++ b/internal/core/Cargo.toml @@ -67,6 +67,8 @@ unstable-wgpu-26 = ["dep:wgpu-26"] default = ["std", "unicode"] +shared-parley = ["shared-fontique", "dep:parley"] + [dependencies] i-slint-common = { workspace = true, features = ["default"] } i-slint-core-macros = { workspace = true, features = ["default"] } @@ -100,6 +102,7 @@ unicode-script = { version = "0.5.7", optional = true } integer-sqrt = { version = "0.1.5" } bytemuck = { workspace = true, optional = true, features = ["derive"] } sys-locale = { version = "0.3.2", optional = true } +parley = { version = "0.5.0", optional = true } image = { workspace = true, optional = true, default-features = false } clru = { workspace = true, optional = true } diff --git a/internal/core/graphics.rs b/internal/core/graphics.rs index 7e47bfb9182..ed29a3a6adf 100644 --- a/internal/core/graphics.rs +++ b/internal/core/graphics.rs @@ -164,9 +164,11 @@ impl FontRequest { let mut collection = sharedfontique::get_collection(); let mut query = collection.query(); - query.set_families( - self.family.as_ref().map(|family| fontique::QueryFamily::from(family.as_str())), - ); + query.set_families(std::iter::once(if let Some(family) = self.family.as_ref() { + fontique::QueryFamily::from(family.as_str()) + } else { + fontique::QueryFamily::Generic(fontique::GenericFamily::SansSerif) + })); query.set_attributes(fontique::Attributes { weight: self diff --git a/internal/core/textlayout.rs b/internal/core/textlayout.rs index 1d1aa4acd80..2a1e2db0b70 100644 --- a/internal/core/textlayout.rs +++ b/internal/core/textlayout.rs @@ -40,6 +40,9 @@ use linebreak_simple::{BreakOpportunity, LineBreakIterator}; mod fragments; mod glyphclusters; mod shaping; +#[cfg(feature = "shared-parley")] +/// cbindgen:ignore +pub mod sharedparley; use shaping::ShapeBuffer; pub use shaping::{AbstractFont, FontMetrics, Glyph, TextShaper}; diff --git a/internal/core/textlayout/sharedparley.rs b/internal/core/textlayout/sharedparley.rs new file mode 100644 index 00000000000..5b8f1c1af95 --- /dev/null +++ b/internal/core/textlayout/sharedparley.rs @@ -0,0 +1,153 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +pub use parley; + +use std::boxed::Box; +use std::cell::RefCell; + +use crate::{ + graphics::FontRequest, + items::TextStrokeStyle, + lengths::{LogicalLength, ScaleFactor}, + textlayout::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap}, +}; +use i_slint_common::sharedfontique; + +pub const DEFAULT_FONT_SIZE: LogicalLength = LogicalLength::new(12.); + +struct Contexts { + layout: parley::LayoutContext, + font: parley::FontContext, +} + +impl Default for Contexts { + fn default() -> Self { + Self { + font: parley::FontContext { + collection: sharedfontique::COLLECTION.inner.clone(), + source_cache: sharedfontique::COLLECTION.source_cache.clone(), + }, + layout: Default::default(), + } + } +} + +std::thread_local! { + static CONTEXTS: RefCell> = Default::default(); +} + +#[derive(Debug, Default, PartialEq, Clone, Copy)] +pub struct Brush { + pub stroke: Option, +} + +pub struct LayoutOptions { + pub max_width: Option, + pub max_height: Option, + pub horizontal_align: TextHorizontalAlignment, + pub vertical_align: TextVerticalAlignment, + pub stroke: Option, + pub font_request: Option, + pub text_wrap: TextWrap, + pub text_overflow: TextOverflow, +} + +impl Default for LayoutOptions { + fn default() -> Self { + Self { + max_width: None, + max_height: None, + horizontal_align: TextHorizontalAlignment::Left, + vertical_align: TextVerticalAlignment::Top, + stroke: None, + font_request: None, + text_wrap: TextWrap::WordWrap, + text_overflow: TextOverflow::Clip, + } + } +} + +pub fn layout(text: &str, scale_factor: ScaleFactor, options: LayoutOptions) -> Layout { + let max_physical_width = options.max_width.map(|max_width| (max_width * scale_factor).get()); + let max_physical_height = options.max_height.map(|max_height| max_height * scale_factor); + let pixel_size = options + .font_request + .as_ref() + .and_then(|font_request| font_request.pixel_size) + .unwrap_or(DEFAULT_FONT_SIZE); + + let mut layout = CONTEXTS.with_borrow_mut(move |contexts| { + let mut builder = + contexts.layout.ranged_builder(&mut contexts.font, text, scale_factor.get(), true); + if let Some(ref font_request) = options.font_request { + if let Some(family) = &font_request.family { + builder.push_default(parley::StyleProperty::FontStack( + parley::style::FontStack::Single(parley::style::FontFamily::Named( + family.as_str().into(), + )), + )); + } + if let Some(weight) = font_request.weight { + builder.push_default(parley::StyleProperty::FontWeight( + parley::style::FontWeight::new(weight as f32), + )); + } + if let Some(letter_spacing) = font_request.letter_spacing { + builder.push_default(parley::StyleProperty::LetterSpacing(letter_spacing.get())); + } + builder.push_default(parley::StyleProperty::FontStyle(if font_request.italic { + parley::style::FontStyle::Italic + } else { + parley::style::FontStyle::Normal + })); + } + builder.push_default(parley::StyleProperty::FontSize(pixel_size.get())); + builder.push_default(parley::StyleProperty::WordBreak(match options.text_wrap { + TextWrap::NoWrap => parley::style::WordBreakStrength::KeepAll, + TextWrap::WordWrap => parley::style::WordBreakStrength::Normal, + TextWrap::CharWrap => parley::style::WordBreakStrength::BreakAll, + })); + if options.text_overflow == TextOverflow::Elide { + todo!(); + } + + builder.push_default(parley::StyleProperty::Brush(Brush { stroke: options.stroke })); + + builder.build(text) + }); + + layout.break_all_lines(max_physical_width); + layout.align( + max_physical_width, + match options.horizontal_align { + TextHorizontalAlignment::Left => parley::Alignment::Left, + TextHorizontalAlignment::Center => parley::Alignment::Middle, + TextHorizontalAlignment::Right => parley::Alignment::Right, + }, + parley::AlignmentOptions::default(), + ); + + let y_offset = match (max_physical_height, options.vertical_align) { + (Some(max_height), TextVerticalAlignment::Center) => { + (max_height.get() - layout.height()) / 2.0 + } + (Some(max_height), TextVerticalAlignment::Bottom) => max_height.get() - layout.height(), + (None, _) | (Some(_), TextVerticalAlignment::Top) => 0.0, + }; + + Layout { inner: layout, y_offset } +} + +pub struct Layout { + inner: parley::Layout, + pub y_offset: f32, +} + +impl std::ops::Deref for Layout { + type Target = parley::Layout; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/internal/renderers/femtovg/Cargo.toml b/internal/renderers/femtovg/Cargo.toml index 3664e5cfef6..c0650cd1802 100644 --- a/internal/renderers/femtovg/Cargo.toml +++ b/internal/renderers/femtovg/Cargo.toml @@ -23,7 +23,7 @@ wgpu-26 = ["dep:wgpu-26", "femtovg/wgpu", "i-slint-core/unstable-wgpu-26"] unstable-wgpu-26 = ["wgpu-26"] [dependencies] -i-slint-core = { workspace = true, features = ["default", "box-shadow-cache", "shared-fontique"] } +i-slint-core = { workspace = true, features = ["default", "box-shadow-cache", "shared-fontique", "shared-parley"] } i-slint-core-macros = { workspace = true, features = ["default"] } i-slint-common = { workspace = true, features = ["default", "shared-fontique"] } @@ -34,7 +34,7 @@ derive_more = { workspace = true } lyon_path = "1.0" pin-weak = "1" scoped-tls-hkt = "0.1" -femtovg = { version = "0.17.0" } +femtovg = { version = "0.18.1" } ttf-parser = { workspace = true } unicode-script = { version = "0.5.4" } # Use the same version was femtovg's rustybuzz, to avoid duplicate crates imgref = { version = "1.6.1" } diff --git a/internal/renderers/femtovg/font_cache.rs b/internal/renderers/femtovg/font_cache.rs new file mode 100644 index 00000000000..3596c8b40b7 --- /dev/null +++ b/internal/renderers/femtovg/font_cache.rs @@ -0,0 +1,39 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// cspell:ignore Noto fontconfig + +use core::num::NonZeroUsize; +use femtovg::TextContext; +use i_slint_core::textlayout::sharedparley::parley; +use std::cell::RefCell; +use std::collections::HashMap; + +pub struct FontCache { + pub(crate) text_context: femtovg::TextContext, + fonts: HashMap<(u64, u32), femtovg::FontId>, +} + +impl Default for FontCache { + fn default() -> Self { + let text_context = TextContext::default(); + text_context.resize_shaped_words_cache(NonZeroUsize::new(10_000_000).unwrap()); + text_context.resize_shaping_run_cache(NonZeroUsize::new(1_000_000).unwrap()); + + Self { text_context, fonts: Default::default() } + } +} + +impl FontCache { + pub fn font(&mut self, font: &parley::Font) -> femtovg::FontId { + let text_context = self.text_context.clone(); + + *self.fonts.entry((font.data.id(), font.index)).or_insert_with(move || { + text_context.add_shared_font_with_index(font.data.clone(), font.index).unwrap() + }) + } +} + +thread_local! { + pub static FONT_CACHE: RefCell = RefCell::new(Default::default()) +} diff --git a/internal/renderers/femtovg/fonts.rs b/internal/renderers/femtovg/fonts.rs deleted file mode 100644 index ece1b1fc150..00000000000 --- a/internal/renderers/femtovg/fonts.rs +++ /dev/null @@ -1,705 +0,0 @@ -// Copyright © SixtyFPS GmbH -// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -// cspell:ignore Noto fontconfig - -use core::num::NonZeroUsize; -use femtovg::TextContext; -use i_slint_common::sharedfontique::{self, fontique}; -use i_slint_core::graphics::euclid; -use i_slint_core::graphics::FontRequest; -use i_slint_core::items::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap}; -use i_slint_core::lengths::PointLengths; -use i_slint_core::lengths::{LogicalLength, LogicalSize, ScaleFactor, SizeLengths}; -use i_slint_core::{SharedString, SharedVector}; -use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; - -use super::{PhysicalLength, PhysicalPoint, PhysicalSize}; - -pub const DEFAULT_FONT_SIZE: LogicalLength = LogicalLength::new(12.); - -#[derive(Clone, PartialEq, Eq, Hash)] -struct FontCacheKey { - family: SharedString, - weight: Option, - italic: bool, -} - -#[derive(Clone)] -pub struct Font { - fonts: SharedVector, - pixel_size: PhysicalLength, - text_context: TextContext, -} - -impl Font { - pub fn init_paint( - &self, - letter_spacing: PhysicalLength, - mut paint: femtovg::Paint, - ) -> femtovg::Paint { - paint.set_font(&self.fonts); - paint.set_font_size(self.pixel_size.get()); - paint.set_text_baseline(femtovg::Baseline::Top); - paint.set_letter_spacing(letter_spacing.get()); - paint - } - - pub fn text_size( - &self, - letter_spacing: PhysicalLength, - text: &str, - max_width: Option, - ) -> PhysicalSize { - let paint = self.init_paint(letter_spacing, femtovg::Paint::default()); - let font_metrics = self.text_context.measure_font(&paint).unwrap(); - let mut lines = 0; - let mut width = 0.; - let mut start = 0; - if let Some(max_width) = max_width { - while start < text.len() { - let max_line_index = text[start..].find('\n').map_or(text.len(), |i| i + 1 + start); - let index = self - .text_context - .break_text(max_width.get(), &text[start..max_line_index], &paint) - .unwrap(); - if index == 0 { - break; - } - let index = start + index; - let measure = - self.text_context.measure_text(0., 0., &text[start..index], &paint).unwrap(); - start = index; - lines += 1; - width = measure.width().max(width); - } - } else { - for line in text.lines() { - let measure = self.text_context.measure_text(0., 0., line, &paint).unwrap(); - lines += 1; - width = measure.width().max(width); - } - } - euclid::size2(width, lines as f32 * font_metrics.height()) - } - - pub fn height(&self) -> PhysicalLength { - let mut paint = femtovg::Paint::default(); - // These are the only two properties measure_font() needs - paint.set_font(&self.fonts); - paint.set_font_size(self.pixel_size.get()); - PhysicalLength::new(self.text_context.measure_font(&paint).unwrap().height()) - } -} - -pub(crate) fn text_size( - font_request: &i_slint_core::graphics::FontRequest, - scale_factor: ScaleFactor, - text: &str, - max_width: Option, -) -> LogicalSize { - let font = - FONT_CACHE.with(|cache| cache.borrow_mut().font(font_request.clone(), scale_factor, text)); - let letter_spacing = font_request.letter_spacing.unwrap_or_default(); - font.text_size(letter_spacing * scale_factor, text, max_width.map(|x| x * scale_factor)) - / scale_factor -} - -pub(crate) fn font_metrics( - font_request: i_slint_core::graphics::FontRequest, -) -> i_slint_core::items::FontMetrics { - let primary_font = FONT_CACHE.with(|cache| { - cache.borrow_mut().load_single_font(font_request.family.as_ref(), font_request.clone()) - }); - - let logical_pixel_size = (font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE)).get(); - - let units_per_em = primary_font.design_font_metrics.units_per_em; - - i_slint_core::items::FontMetrics { - ascent: primary_font.design_font_metrics.ascent * logical_pixel_size / units_per_em, - descent: primary_font.design_font_metrics.descent * logical_pixel_size / units_per_em, - x_height: primary_font.design_font_metrics.x_height * logical_pixel_size / units_per_em, - cap_height: primary_font.design_font_metrics.cap_height * logical_pixel_size / units_per_em, - } -} - -#[derive(Clone)] -struct LoadedFont { - femtovg_font_id: femtovg::FontId, - font: fontique::QueryFont, - design_font_metrics: sharedfontique::DesignFontMetrics, -} - -#[derive(Default)] -struct GlyphCoverage { - // Used to express script support for all scripts except Unknown, Common and Inherited - // For those the detailed glyph_coverage is used instead - supported_scripts: HashMap, - // Especially in characters mapped to the common script, the support varies. For example - // '✓' and the digit '1' map to Common, but not all fonts providing digits also support the - // check mark glyph. - exact_glyph_coverage: HashMap, -} - -enum GlyphCoverageCheckResult { - Incomplete, - Improved, - Complete, -} - -pub struct FontCache { - loaded_fonts: HashMap, - // for a given font family id, this tells us what we've learned about the script - // coverage of the font. - loaded_font_coverage: HashMap, - pub(crate) text_context: TextContext, - available_families: HashSet, -} - -impl Default for FontCache { - fn default() -> Self { - let available_families = - sharedfontique::get_collection().family_names().map(|str| str.into()).collect(); - - let text_context = TextContext::default(); - text_context.resize_shaped_words_cache(NonZeroUsize::new(10_000_000).unwrap()); - text_context.resize_shaping_run_cache(NonZeroUsize::new(1_000_000).unwrap()); - - Self { - loaded_fonts: HashMap::new(), - loaded_font_coverage: HashMap::new(), - text_context, - available_families, - } - } -} - -thread_local! { - pub static FONT_CACHE: RefCell = RefCell::new(Default::default()) -} - -impl FontCache { - fn load_single_font( - &mut self, - family: Option<&SharedString>, - font_request: FontRequest, - ) -> LoadedFont { - let text_context = self.text_context.clone(); - let cache_key = FontCacheKey { - family: family.cloned().unwrap_or_default(), - weight: font_request.weight, - italic: font_request.italic, - }; - - if let Some(loaded_font) = self.loaded_fonts.get(&cache_key) { - return loaded_font.clone(); - } - - //let now = std::time::Instant::now(); - - let font = font_request.query_fontique().unwrap(); - - let design_font_metrics = sharedfontique::DesignFontMetrics::new(&font); - - let femtovg_font_id = - text_context.add_shared_font_with_index(font.blob.clone(), font.index).unwrap(); - - //println!("Loaded {:#?} in {}ms.", request, now.elapsed().as_millis()); - let new_font = LoadedFont { femtovg_font_id, font, design_font_metrics }; - self.loaded_fonts.insert(cache_key, new_font.clone()); - new_font - } - - pub fn font( - &mut self, - font_request: FontRequest, - scale_factor: ScaleFactor, - reference_text: &str, - ) -> Font { - let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE) * scale_factor; - - let primary_font = - self.load_single_font(font_request.family.as_ref(), font_request.clone()); - - use unicode_script::{Script, UnicodeScript}; - // map from required script to sample character - let mut scripts_required: HashMap = Default::default(); - let mut chars_required: HashSet = Default::default(); - for ch in reference_text.chars() { - if ch.is_control() || ch.is_whitespace() { - continue; - } - let script = ch.script(); - if script == Script::Common || script == Script::Inherited || script == Script::Unknown - { - chars_required.insert(ch); - } else { - scripts_required.insert(script, ch); - } - } - - let mut coverage_result = self.check_and_update_script_coverage( - &mut scripts_required, - &mut chars_required, - primary_font.font.clone(), - ); - - //eprintln!( - // "coverage for {} after checking primary font: {:#?}", - // reference_text, scripts_required - //); - - let fallbacks = if !matches!(coverage_result, GlyphCoverageCheckResult::Complete) { - self.font_fallbacks_for_request( - font_request.family.as_ref(), - pixel_size, - &primary_font, - reference_text, - ) - } else { - Vec::new() - }; - - let fonts = core::iter::once(primary_font.femtovg_font_id) - .chain(fallbacks.iter().filter_map(|fallback_family| { - if matches!(coverage_result, GlyphCoverageCheckResult::Complete) { - return None; - } - - let fallback_font = - self.load_single_font(Some(fallback_family), font_request.clone()); - - coverage_result = self.check_and_update_script_coverage( - &mut scripts_required, - &mut chars_required, - fallback_font.font, - ); - - if matches!( - coverage_result, - GlyphCoverageCheckResult::Improved | GlyphCoverageCheckResult::Complete - ) { - Some(fallback_font.femtovg_font_id) - } else { - None - } - })) - .collect::>(); - - Font { fonts, text_context: self.text_context.clone(), pixel_size } - } - - #[cfg(target_vendor = "apple")] - fn font_fallbacks_for_request( - &self, - _family: Option<&SharedString>, - _pixel_size: PhysicalLength, - _primary_font: &LoadedFont, - _reference_text: &str, - ) -> Vec { - let requested_font = match core_text::font::new_from_name( - &_family.as_ref().map_or_else(|| "", |s| s.as_str()), - _pixel_size.get() as f64, - ) { - Ok(f) => f, - Err(_) => return vec![], - }; - - core_text::font::cascade_list_for_languages( - &requested_font, - &core_foundation::array::CFArray::from_CFTypes(&[]), - ) - .iter() - .map(|fallback_descriptor| SharedString::from(fallback_descriptor.family_name())) - .filter(|family| self.is_known_family(&family)) - .collect::>() - } - - #[cfg(target_os = "windows")] - fn font_fallbacks_for_request( - &self, - _family: Option<&SharedString>, - _pixel_size: PhysicalLength, - _primary_font: &LoadedFont, - reference_text: &str, - ) -> Vec { - let system_font_fallback = match dwrote::FontFallback::get_system_fallback() { - Some(fallback) => fallback, - None => return Vec::new(), - }; - let font_collection = dwrote::FontCollection::get_system(false); - let base_family = Some(_family.as_ref().map_or_else(|| "", |s| s.as_str())); - - let reference_text_utf16: Vec = reference_text.encode_utf16().collect(); - - // Hack to implement the minimum interface for direct write. We have yet to provide the correct - // locale (but return an empty string for now). This struct stores the number of utf-16 characters - // so that in get_locale_name it can return that the (empty) locale applies all the characters after - // `text_position`, by returning the count. - struct TextAnalysisHack(u32); - impl dwrote::TextAnalysisSourceMethods for TextAnalysisHack { - fn get_locale_name(&self, text_position: u32) -> (std::borrow::Cow<'_, str>, u32) { - ("".into(), self.0 - text_position) - } - - // We should do better on this one, too... - fn get_paragraph_reading_direction( - &self, - ) -> winapi::um::dwrite::DWRITE_READING_DIRECTION { - winapi::um::dwrite::DWRITE_READING_DIRECTION_LEFT_TO_RIGHT - } - } - - let text_analysis_source = dwrote::TextAnalysisSource::from_text_and_number_subst( - Box::new(TextAnalysisHack(reference_text_utf16.len() as u32)), - std::borrow::Cow::Borrowed(&reference_text_utf16), - dwrote::NumberSubstitution::new( - winapi::um::dwrite::DWRITE_NUMBER_SUBSTITUTION_METHOD_NONE, - "", - true, - ), - ); - - let mut fallback_fonts = Vec::new(); - - let mut utf16_pos = 0; - - while utf16_pos < reference_text_utf16.len() { - let fallback_result = system_font_fallback.map_characters( - &text_analysis_source, - utf16_pos as u32, - (reference_text_utf16.len() - utf16_pos) as u32, - &font_collection, - base_family, - dwrote::FontWeight::Regular, - dwrote::FontStyle::Normal, - dwrote::FontStretch::Normal, - ); - - if let Some(fallback_font) = fallback_result.mapped_font { - let family: SharedString = fallback_font.family_name().into(); - if self.is_known_family(&family) { - fallback_fonts.push(family) - } - } else { - break; - } - - utf16_pos += fallback_result.mapped_length; - } - - fallback_fonts - } - - #[cfg(not(any( - target_family = "windows", - target_vendor = "apple", - target_arch = "wasm32", - target_os = "android", - target_os = "nto", - )))] - fn font_fallbacks_for_request( - &self, - _family: Option<&SharedString>, - _pixel_size: PhysicalLength, - _primary_font: &LoadedFont, - _reference_text: &str, - ) -> Vec { - Vec::new() - } - - #[cfg(any(target_arch = "wasm32", target_os = "android"))] - fn font_fallbacks_for_request( - &self, - _family: Option<&SharedString>, - _pixel_size: PhysicalLength, - _primary_font: &LoadedFont, - _reference_text: &str, - ) -> Vec { - [SharedString::from("DejaVu Sans")] - .iter() - .filter(|family_name| self.is_known_family(&family_name)) - .cloned() - .collect() - } - - #[cfg(target_os = "nto")] - fn font_fallbacks_for_request( - &self, - _family: Option<&SharedString>, - _pixel_size: PhysicalLength, - _primary_font: &LoadedFont, - _reference_text: &str, - ) -> Vec { - [SharedString::from("Noto Sans")] - .iter() - .filter(|family_name| self.is_known_family(&family_name)) - .cloned() - .collect() - } - - #[cfg_attr( - not(any( - target_family = "windows", - target_vendor = "apple", - target_arch = "wasm32", - target_os = "android", - target_os = "nto", - )), - allow(dead_code) - )] - fn is_known_family(&self, family: &str) -> bool { - self.available_families.contains(family) - } - - // From the set of script without coverage, remove all entries that are known to be covered by - // the given face_id. Any yet unknown script coverage for the face_id is updated (hence - // mutable self). - fn check_and_update_script_coverage( - &mut self, - scripts_without_coverage: &mut HashMap, - chars_without_coverage: &mut HashSet, - font: fontique::QueryFont, - ) -> GlyphCoverageCheckResult { - //eprintln!("required scripts {:#?}", required_scripts); - let coverage = self.loaded_font_coverage.entry(font.family.0).or_default(); - - let mut scripts_that_need_checking = Vec::new(); - let mut chars_that_need_checking = Vec::new(); - - let old_uncovered_scripts_count = scripts_without_coverage.len(); - let old_uncovered_chars_count = chars_without_coverage.len(); - - scripts_without_coverage.retain(|script, sample| { - coverage.supported_scripts.get(script).map_or_else( - || { - scripts_that_need_checking.push((*script, *sample)); - true // this may or may not be supported, so keep it in scripts_without_coverage - }, - |has_coverage| !has_coverage, - ) - }); - - chars_without_coverage.retain(|ch| { - coverage.exact_glyph_coverage.get(ch).map_or_else( - || { - chars_that_need_checking.push(*ch); - true // this may or may not be supported, so keep it in chars_without_coverage - }, - |has_coverage| !has_coverage, - ) - }); - - if !scripts_that_need_checking.is_empty() || !chars_that_need_checking.is_empty() { - let face = ttf_parser::Face::parse(font.blob.data(), font.index).unwrap(); - - for (unchecked_script, sample_char) in scripts_that_need_checking { - let glyph_coverage = face.glyph_index(sample_char).is_some(); - coverage.supported_scripts.insert(unchecked_script, glyph_coverage); - - if glyph_coverage { - scripts_without_coverage.remove(&unchecked_script); - } - } - - for unchecked_char in chars_that_need_checking { - let glyph_coverage = face.glyph_index(unchecked_char).is_some(); - coverage.exact_glyph_coverage.insert(unchecked_char, glyph_coverage); - - if glyph_coverage { - chars_without_coverage.remove(&unchecked_char); - } - } - } - - let remaining_required_script_coverage = scripts_without_coverage.len(); - let remaining_required_char_coverage = chars_without_coverage.len(); - - if scripts_without_coverage.is_empty() && chars_without_coverage.is_empty() { - GlyphCoverageCheckResult::Complete - } else if remaining_required_script_coverage < old_uncovered_scripts_count - || remaining_required_char_coverage < old_uncovered_chars_count - { - GlyphCoverageCheckResult::Improved - } else { - GlyphCoverageCheckResult::Incomplete - } - } -} - -/// Layout the given string in lines, and call the `layout_line` callback with the line to draw at position y. -/// The signature of the `layout_line` function is: `(text, pos, start_index, line_metrics)`. -/// start index is the starting byte of the text in the string. -/// Returns the coordinates of the cursor, if a cursor byte offset was provided. -pub(crate) fn layout_text_lines( - string: &str, - font: &Font, - max_size: PhysicalSize, - (horizontal_alignment, vertical_alignment): (TextHorizontalAlignment, TextVerticalAlignment), - wrap: TextWrap, - overflow: TextOverflow, - single_line: bool, - cursor_byte_offset: Option, - paint: &femtovg::Paint, - mut layout_line: impl FnMut(&str, PhysicalPoint, usize, &femtovg::TextMetrics), -) -> Option { - let wrap = wrap != TextWrap::NoWrap; - let elide = overflow == TextOverflow::Elide; - - let max_width = max_size.width_length(); - let max_height = max_size.height_length(); - - let text_context = FONT_CACHE.with(|cache| cache.borrow().text_context.clone()); - let font_metrics = text_context.measure_font(paint).unwrap(); - let font_height = PhysicalLength::new(font_metrics.height()); - - let mut cursor_point: Option = None; - - let text_height = || { - if single_line { - font_height - } else { - // Note: this is kind of doing twice the layout because text_size also does it - let text_height = font - .text_size( - PhysicalLength::new(paint.letter_spacing()), - string, - if wrap { Some(max_width) } else { None }, - ) - .height_length(); - if elide && text_height > max_height { - // The height of the text is used for vertical alignment below. - // If the full text doesn't fit into max_height and eliding is - // enabled, calculate the height of the max number of lines that - // fit to ensure correct vertical alignment when elided. - let max_lines = (max_height.get() / font_height.get()).floor(); - font_height * max_lines - } else { - text_height - } - } - }; - - let mut process_line = - |text_span: &str, y: PhysicalLength, start: usize, line_metrics: &femtovg::TextMetrics| { - let x = match horizontal_alignment { - TextHorizontalAlignment::Left => PhysicalLength::default(), - TextHorizontalAlignment::Center => { - max_width / 2. - max_width.min(PhysicalLength::new(line_metrics.width())) / 2. - } - TextHorizontalAlignment::Right => { - max_width - max_width.min(PhysicalLength::new(line_metrics.width())) - } - }; - let line_pos = PhysicalPoint::from_lengths(x, y); - layout_line(text_span, line_pos, start, line_metrics); - - if let Some(cursor_byte_offset) = cursor_byte_offset { - let text_span_range = start..=(start + text_span.len()); - if text_span_range.contains(&cursor_byte_offset) { - let cursor_x = PhysicalLength::new( - line_metrics - .glyphs - .iter() - .find_map(|glyph| { - if glyph.byte_index == (cursor_byte_offset - start) { - Some(glyph.x) - } else { - None - } - }) - .unwrap_or_else(|| line_metrics.width()), - ); - cursor_point = Some(PhysicalPoint::from_lengths( - line_pos.x_length() + cursor_x, - line_pos.y_length(), - )); - } - } - }; - - let baseline_y = match vertical_alignment { - TextVerticalAlignment::Top => PhysicalLength::default(), - TextVerticalAlignment::Center => max_height / 2. - text_height() / 2., - TextVerticalAlignment::Bottom => max_height - text_height(), - }; - let mut y = baseline_y; - let mut start = 0; - 'lines: while start < string.len() && y + font_height <= max_height { - if wrap && (!elide || y + font_height * 2. <= max_height) { - let max_line_index = string[start..].find('\n').map_or(string.len(), |i| i + 1 + start); - let index = text_context - .break_text(max_width.get(), &string[start..max_line_index], paint) - .unwrap(); - if index == 0 { - // FIXME the word is too big to be shown, but we should still break, ideally - break; - } - let index = start + index; - let line = string[start..index].trim_end_matches('\n'); - let text_metrics = text_context.measure_text(0., 0., line, paint).unwrap(); - process_line(line, y, start, &text_metrics); - y += font_height; - start = index; - } else { - let index = if single_line { - string.len() - } else { - string[start..].find('\n').map_or(string.len(), |i| start + i) - }; - let line = &string[start..index]; - let text_metrics = text_context.measure_text(0., 0., line, paint).unwrap(); - let elide_last_line = - elide && index < string.len() && y + font_height * 2. > max_height; - if text_metrics.width() > max_width.get() || elide_last_line { - let w = max_width - - if elide { - PhysicalLength::new( - text_context.measure_text(0., 0., "…", paint).unwrap().width(), - ) - } else { - PhysicalLength::default() - }; - let mut current_x = 0.; - for glyph in &text_metrics.glyphs { - current_x += glyph.advance_x; - if current_x >= w.get() { - let txt = &line[..glyph.byte_index]; - if elide { - let elided = format!("{txt}…"); - process_line(&elided, y, start, &text_metrics); - } else { - process_line(txt, y, start, &text_metrics); - } - y += font_height; - start = index + 1; - continue 'lines; - } - } - if elide_last_line { - let elided = format!("{}…", line.strip_suffix('\n').unwrap_or(line)); - process_line(&elided, y, start, &text_metrics); - y += font_height; - start = index + 1; - continue 'lines; - } - } - process_line(line, y, start, &text_metrics); - y += font_height; - start = index + 1; - } - } - - cursor_point.or_else(|| { - cursor_byte_offset.map(|_| { - let x = match horizontal_alignment { - TextHorizontalAlignment::Left => PhysicalLength::default(), - TextHorizontalAlignment::Center => max_size.width_length() / 2., - TextHorizontalAlignment::Right => max_size.width_length(), - }; - PhysicalPoint::from_lengths(x, y) - }) - }) -} diff --git a/internal/renderers/femtovg/itemrenderer.rs b/internal/renderers/femtovg/itemrenderer.rs index 82e47f3ee2a..b62a9add13e 100644 --- a/internal/renderers/femtovg/itemrenderer.rs +++ b/internal/renderers/femtovg/itemrenderer.rs @@ -23,13 +23,14 @@ use i_slint_core::lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, RectLengths, ScaleFactor, SizeLengths, }; +use i_slint_core::textlayout::sharedparley::{self, parley}; use i_slint_core::{Brush, Color, ImageInner, SharedString}; use crate::images::TextureImporter; use super::images::{Texture, TextureCacheKey}; use super::PhysicalSize; -use super::{fonts, PhysicalBorderRadius, PhysicalLength, PhysicalPoint, PhysicalRect}; +use super::{font_cache, PhysicalBorderRadius, PhysicalLength, PhysicalPoint, PhysicalRect}; type FemtovgBoxShadowCache = BoxShadowCache>; @@ -194,6 +195,52 @@ impl<'a, R: femtovg::Renderer + TextureImporter> GLItemRenderer<'a, R> { } } +fn draw_glyphs( + layout: &sharedparley::Layout, + canvas: &mut Canvas, + paint: &mut femtovg::Paint, +) { + for line in layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let run = glyph_run.run(); + + let font_id = + font_cache::FONT_CACHE.with(|cache| cache.borrow_mut().font(run.font())); + + let brush = glyph_run.style().brush; + + let glyphs = glyph_run.positioned_glyphs().map(|glyph: parley::Glyph| { + femtovg::PositionedGlyph { + x: glyph.x, + y: glyph.y + layout.y_offset, + glyph_id: glyph.id, + } + }); + + paint.set_font_size(run.font_size()); + + match brush.stroke { + Some(i_slint_core::items::TextStrokeStyle::Outside) => { + canvas.stroke_glyph_run(font_id, glyphs.clone(), paint).unwrap(); + canvas.fill_glyph_run(font_id, glyphs, paint).unwrap(); + } + Some(i_slint_core::items::TextStrokeStyle::Center) => { + canvas.fill_glyph_run(font_id, glyphs.clone(), paint).unwrap(); + canvas.stroke_glyph_run(font_id, glyphs, paint).unwrap(); + } + None => { + canvas.fill_glyph_run(font_id, glyphs, paint).unwrap(); + } + } + } + parley::PositionedLayoutItem::InlineBox(_inline_box) => {} + }; + } + } +} + impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer<'a, R> { fn draw_rectangle( &mut self, @@ -329,8 +376,8 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer size: LogicalSize, _cache: &CachedRenderingData, ) { - let max_width = size.width_length() * self.scale_factor; - let max_height = size.height_length() * self.scale_factor; + let max_width = size.width_length(); + let max_height = size.height_length(); if max_width.get() <= 0. || max_height.get() <= 0. { return; @@ -340,15 +387,13 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer return; } - let string = text.text(); - let string = string.as_str(); - let font = fonts::FONT_CACHE.with(|cache| { - cache.borrow_mut().font(text.font_request(self_rc), self.scale_factor, &text.text()) - }); + let (horizontal_align, vertical_align) = text.alignment(); + let color = text.color(); + let font_request = text.font_request(self_rc); let text_path = rect_to_path((size * self.scale_factor).into()); - let paint = match self.brush_to_paint(text.color(), &text_path) { - Some(paint) => font.init_paint(text.letter_spacing() * self.scale_factor, paint), + let mut paint = match self.brush_to_paint(color, &text_path) { + Some(paint) => paint, None => return, }; @@ -369,39 +414,31 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer None } else { paint.set_line_width(stroke_width); - Some(font.init_paint(text.letter_spacing() * self.scale_factor, paint)) + Some(paint) } } None => None, }; - let mut canvas = self.canvas.borrow_mut(); - fonts::layout_text_lines( - string, - &font, - PhysicalSize::from_lengths(max_width, max_height), - text.alignment(), - text.wrap(), - text.overflow(), - false, - None, - &paint, - |to_draw, pos, _, _| { - match (stroke_style, &stroke_paint) { - (TextStrokeStyle::Outside, Some(stroke_paint)) => { - canvas.stroke_text(pos.x, pos.y, to_draw.trim_end(), stroke_paint).unwrap(); - canvas.fill_text(pos.x, pos.y, to_draw.trim_end(), &paint).unwrap(); - } - (TextStrokeStyle::Center, Some(stroke_paint)) => { - canvas.fill_text(pos.x, pos.y, to_draw.trim_end(), &paint).unwrap(); - canvas.stroke_text(pos.x, pos.y, to_draw.trim_end(), stroke_paint).unwrap(); - } - _ => { - canvas.fill_text(pos.x, pos.y, to_draw.trim_end(), &paint).unwrap(); - } - }; + let layout = sharedparley::layout( + text.text().as_str(), + self.scale_factor, + sharedparley::LayoutOptions { + horizontal_align, + vertical_align, + max_height: Some(max_height), + max_width: Some(max_width), + stroke: stroke_paint.is_some().then_some(stroke_style), + font_request: Some(font_request), + text_wrap: text.wrap(), + text_overflow: text.overflow(), + ..Default::default() }, ); + + let mut canvas = self.canvas.borrow_mut(); + + draw_glyphs(&layout, &mut canvas, &mut paint); } fn draw_text_input( @@ -410,8 +447,8 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer self_rc: &ItemRc, size: LogicalSize, ) { - let width = size.width_length() * self.scale_factor; - let height = size.height_length() * self.scale_factor; + let width = size.width_length(); + let height = size.height_length(); if width.get() <= 0. || height.get() <= 0. { return; } @@ -420,21 +457,15 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer return; } - let font = fonts::FONT_CACHE.with(|cache| { - cache.borrow_mut().font( - text_input.font_request(self_rc), - self.scale_factor, - &text_input.text(), - ) - }); + let font_request = text_input.font_request(self_rc); let visual_representation = text_input.visual_representation(None); - let paint = match self.brush_to_paint( + let mut paint = match self.brush_to_paint( visual_representation.text_color, &rect_to_path((size * self.scale_factor).into()), ) { - Some(paint) => font.init_paint(text_input.letter_spacing() * self.scale_factor, paint), + Some(paint) => paint, None => return, }; @@ -452,113 +483,66 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer }; let mut canvas = self.canvas.borrow_mut(); - let font_height = font.height(); let text: SharedString = visual_representation.text.into(); - let cursor_point = fonts::layout_text_lines( - text.as_str(), - &font, - PhysicalSize::from_lengths(width, height), - (text_input.horizontal_alignment(), text_input.vertical_alignment()), - text_input.wrap(), - items::TextOverflow::Clip, - text_input.single_line(), - cursor_visible.then_some(cursor_pos), - &paint, - |to_draw: &str, pos: PhysicalPoint, start, metrics: &femtovg::TextMetrics| { - let range = start..(start + to_draw.len()); - if min_select != max_select - && (range.contains(&min_select) - || range.contains(&max_select) - || (min_select..max_select).contains(&start)) - { - let mut selection_start_x = PhysicalLength::default(); - let mut selection_end_x = PhysicalLength::default(); - let mut after_selection_x = PhysicalLength::default(); - // Determine the first and last (inclusive) glyph of the selection. The anchor - // will always be at the start of a grapheme boundary, so there's at ShapedGlyph - // that has a matching byte index. For the selection end we have to look for the - // visual end of glyph before the cursor, because due to for example ligatures - // (or generally glyph substitution) there may not be a dedicated glyph. - // FIXME: in the case of ligature, there is currently no way to know the exact - // position of the split. When we know it, we might need to draw in two - // steps with clip to draw each part of the ligature in a different color - for glyph in &metrics.glyphs { - if glyph.byte_index == min_select.saturating_sub(start) { - selection_start_x = PhysicalLength::new(glyph.x - glyph.bearing_x); - } - if glyph.byte_index == max_select - start - || glyph.byte_index >= to_draw.len() - { - after_selection_x = PhysicalLength::new(glyph.x - glyph.bearing_x); - break; - } - selection_end_x = PhysicalLength::new(glyph.x + glyph.advance_x); - } - - let selection_rect = PhysicalRect::new( - pos + PhysicalPoint::from_lengths( - selection_start_x, - PhysicalLength::default(), - ) - .to_vector(), - PhysicalSize::from_lengths( - selection_end_x - selection_start_x, - font_height, - ), - ); - canvas.fill_path( - &rect_to_path(selection_rect), - &femtovg::Paint::color(to_femtovg_color( - &text_input.selection_background_color(), - )), - ); - let mut selected_paint = paint.clone(); - selected_paint - .set_color(to_femtovg_color(&text_input.selection_foreground_color())); - canvas - .fill_text( - pos.x, - pos.y, - to_draw[..min_select.saturating_sub(start)].trim_end(), - &paint, - ) - .unwrap(); - canvas - .fill_text( - pos.x + selection_start_x.get(), - pos.y, - to_draw[min_select.saturating_sub(start) - ..(max_select - start).min(to_draw.len())] - .trim_end(), - &selected_paint, - ) - .unwrap(); - canvas - .fill_text( - pos.x + after_selection_x.get(), - pos.y, - to_draw[(max_select - start).min(to_draw.len())..].trim_end(), - &paint, - ) - .unwrap(); - } else { - // no selection on this line - canvas.fill_text(pos.x, pos.y, to_draw.trim_end(), &paint).unwrap(); - }; + let layout = sharedparley::layout( + &text, + self.scale_factor, + sharedparley::LayoutOptions { + max_width: Some(width), + max_height: Some(height), + vertical_align: text_input.vertical_alignment(), + font_request: Some(font_request), + ..Default::default() }, ); - if let Some(cursor_point) = cursor_point { - let mut cursor_rect = femtovg::Path::new(); - cursor_rect.rect( - cursor_point.x, - cursor_point.y, - (text_input.text_cursor_width() * self.scale_factor).get(), - font_height.get(), + let selection = parley::layout::cursor::Selection::new( + parley::layout::cursor::Cursor::from_byte_index( + &layout, + min_select, + Default::default(), + ), + parley::layout::cursor::Cursor::from_byte_index( + &layout, + max_select, + Default::default(), + ), + ); + + selection.geometry_with(&layout, |rect, _| { + let mut selection_path = femtovg::Path::new(); + selection_path.rect( + rect.min_x() as _, + rect.min_y() as f32 + layout.y_offset, + rect.width() as _, + rect.height() as _, + ); + canvas.fill_path( + &selection_path, + &femtovg::Paint::color(to_femtovg_color(&text_input.selection_background_color())), + ); + }); + + draw_glyphs(&layout, &mut canvas, &mut paint); + + if cursor_visible { + let cursor = parley::layout::cursor::Cursor::from_byte_index( + &layout, + cursor_pos, + Default::default(), + ); + let rect = cursor.geometry(&layout, (text_input.text_cursor_width()).get()); + + let mut cursor_path = femtovg::Path::new(); + cursor_path.rect( + rect.min_x() as _, + rect.min_y() as f32 + layout.y_offset, + rect.width() as _, + rect.height() as _, ); canvas.fill_path( - &cursor_rect, + &cursor_path, &femtovg::Paint::color(to_femtovg_color(&visual_representation.cursor_color)), ); } @@ -989,12 +973,10 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer } fn draw_string(&mut self, string: &str, color: Color) { - let font = fonts::FONT_CACHE - .with(|cache| cache.borrow_mut().font(Default::default(), self.scale_factor, string)); - let paint = font - .init_paint(PhysicalLength::default(), femtovg::Paint::color(to_femtovg_color(&color))); + let layout = sharedparley::layout(string, self.scale_factor, Default::default()); + let mut paint = femtovg::Paint::color(to_femtovg_color(&color)); let mut canvas = self.canvas.borrow_mut(); - canvas.fill_text(0., 0., string, &paint).unwrap(); + draw_glyphs(&layout, &mut canvas, &mut paint); } fn draw_image_direct(&mut self, image: i_slint_core::graphics::Image) { diff --git a/internal/renderers/femtovg/lib.rs b/internal/renderers/femtovg/lib.rs index 8f7ecd01481..4af56fa614d 100644 --- a/internal/renderers/femtovg/lib.rs +++ b/internal/renderers/femtovg/lib.rs @@ -21,6 +21,7 @@ use i_slint_core::lengths::{ }; use i_slint_core::platform::PlatformError; use i_slint_core::renderer::RendererSealed; +use i_slint_core::textlayout::sharedparley::{self, parley}; use i_slint_core::window::{WindowAdapter, WindowInner}; use i_slint_core::Brush; use images::TextureImporter; @@ -33,7 +34,7 @@ type PhysicalBorderRadius = BorderRadius; use self::itemrenderer::CanvasRc; -mod fonts; +mod font_cache; mod images; mod itemrenderer; #[cfg(feature = "opengl")] @@ -285,9 +286,19 @@ impl RendererSealed for FemtoVGRenderer { text: &str, max_width: Option, scale_factor: ScaleFactor, - _text_wrap: TextWrap, //TODO: Add support for char-wrap + text_wrap: TextWrap, ) -> LogicalSize { - crate::fonts::text_size(&font_request, scale_factor, text, max_width) + let layout = sharedparley::layout( + text, + scale_factor, + sharedparley::LayoutOptions { + max_width, + text_wrap, + font_request: Some(font_request), + ..Default::default() + }, + ); + PhysicalSize::new(layout.width(), layout.height()) / scale_factor } fn font_metrics( @@ -295,7 +306,15 @@ impl RendererSealed for FemtoVGRenderer { font_request: i_slint_core::graphics::FontRequest, _scale_factor: ScaleFactor, ) -> i_slint_core::items::FontMetrics { - crate::fonts::font_metrics(font_request) + let font = font_request.query_fontique().unwrap(); + let face = sharedfontique::ttf_parser::Face::parse(font.blob.data(), font.index).unwrap(); + + i_slint_core::items::FontMetrics { + ascent: face.ascender() as _, + descent: face.descender() as _, + x_height: face.x_height().unwrap_or_default() as _, + cap_height: face.capital_height().unwrap_or_default() as _, + } } fn text_input_byte_offset_for_position( @@ -308,49 +327,28 @@ impl RendererSealed for FemtoVGRenderer { let pos = pos * scale_factor; let text = text_input.text(); - let mut result = text.len(); - - let width = text_input.width() * scale_factor; - let height = text_input.height() * scale_factor; + let width = text_input.width(); + let height = text_input.height(); if width.get() <= 0. || height.get() <= 0. || pos.y < 0. { return 0; } - let font = crate::fonts::FONT_CACHE - .with(|cache| cache.borrow_mut().font(font_request, scale_factor, &text_input.text())); - - let visual_representation = text_input.visual_representation(None); - - let paint = font.init_paint(text_input.letter_spacing() * scale_factor, Default::default()); - let text_context = - crate::fonts::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()); - let font_height = text_context.measure_font(&paint).unwrap().height(); - crate::fonts::layout_text_lines( - &visual_representation.text, - &font, - PhysicalSize::from_lengths(width, height), - (text_input.horizontal_alignment(), text_input.vertical_alignment()), - text_input.wrap(), - i_slint_core::items::TextOverflow::Clip, - text_input.single_line(), - None, - &paint, - |line_text, line_pos, start, metrics| { - if (line_pos.y..(line_pos.y + font_height)).contains(&pos.y) { - let mut current_x = 0.; - for glyph in &metrics.glyphs { - if line_pos.x + current_x + glyph.advance_x / 2. >= pos.x { - result = start + glyph.byte_index; - return; - } - current_x += glyph.advance_x; - } - result = start + line_text.trim_end().len(); - } + let layout = sharedparley::layout( + &text, + scale_factor, + sharedparley::LayoutOptions { + font_request: Some(font_request), + max_width: Some(width), + max_height: Some(height), + vertical_align: text_input.vertical_alignment(), + ..Default::default() }, ); + let cursor = + parley::layout::cursor::Cursor::from_point(&layout, pos.x, pos.y - layout.y_offset); - visual_representation.map_byte_offset_from_byte_offset_in_visual_text(result) + let visual_representation = text_input.visual_representation(None); + visual_representation.map_byte_offset_from_byte_offset_in_visual_text(cursor.index()) } fn text_input_cursor_rect_for_byte_offset( @@ -362,10 +360,10 @@ impl RendererSealed for FemtoVGRenderer { ) -> LogicalRect { let text = text_input.text(); - let font_size = font_request.pixel_size.unwrap_or(fonts::DEFAULT_FONT_SIZE); + let font_size = font_request.pixel_size.unwrap_or(sharedparley::DEFAULT_FONT_SIZE); - let width = text_input.width() * scale_factor; - let height = text_input.height() * scale_factor; + let width = text_input.width(); + let height = text_input.height(); if width.get() <= 0. || height.get() <= 0. { return LogicalRect::new( LogicalPoint::default(), @@ -373,26 +371,24 @@ impl RendererSealed for FemtoVGRenderer { ); } - let font = crate::fonts::FONT_CACHE - .with(|cache| cache.borrow_mut().font(font_request, scale_factor, &text_input.text())); - - let paint = font.init_paint(text_input.letter_spacing() * scale_factor, Default::default()); - let cursor_point = fonts::layout_text_lines( - text.as_str(), - &font, - PhysicalSize::from_lengths(width, height), - (text_input.horizontal_alignment(), text_input.vertical_alignment()), - text_input.wrap(), - i_slint_core::items::TextOverflow::Clip, - text_input.single_line(), - Some(byte_offset), - &paint, - |_, _, _, _| {}, + let layout = sharedparley::layout( + &text, + scale_factor, + sharedparley::LayoutOptions { + max_width: Some(width), + max_height: Some(height), + ..Default::default() + }, ); - + let cursor = parley::layout::cursor::Cursor::from_byte_index( + &layout, + byte_offset, + Default::default(), + ); + let rect = cursor.geometry(&layout, (text_input.text_cursor_width()).get()); LogicalRect::new( - cursor_point.unwrap_or_default() / scale_factor, - LogicalSize::from_lengths(LogicalLength::new(1.0), font_size), + LogicalPoint::new(rect.min_x() as _, rect.min_y() as f32 + layout.y_offset), + LogicalSize::new(rect.width() as _, rect.height() as _), ) } @@ -415,7 +411,7 @@ impl RendererSealed for FemtoVGRenderer { } fn default_font_size(&self) -> LogicalLength { - self::fonts::DEFAULT_FONT_SIZE + sharedparley::DEFAULT_FONT_SIZE } fn set_rendering_notifier( diff --git a/internal/renderers/femtovg/opengl.rs b/internal/renderers/femtovg/opengl.rs index 7780a4cc971..10f19878d4c 100644 --- a/internal/renderers/femtovg/opengl.rs +++ b/internal/renderers/femtovg/opengl.rs @@ -137,7 +137,7 @@ impl OpenGLBackend { let femtovg_canvas = femtovg::Canvas::new_with_text_context( gl_renderer, - crate::fonts::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()), + crate::font_cache::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()), ) .unwrap(); diff --git a/internal/renderers/femtovg/wgpu.rs b/internal/renderers/femtovg/wgpu.rs index c3d0109551f..601682da823 100644 --- a/internal/renderers/femtovg/wgpu.rs +++ b/internal/renderers/femtovg/wgpu.rs @@ -159,7 +159,7 @@ impl FemtoVGRenderer { let wgpu_renderer = femtovg::renderer::WGPURenderer::new(device, queue); let femtovg_canvas = femtovg::Canvas::new_with_text_context( wgpu_renderer, - crate::fonts::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()), + crate::font_cache::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()), ) .unwrap();