feature/fontique: switch femtovg to parley (#9466)
Some checks are pending
autofix.ci / format_fix (push) Waiting to run
autofix.ci / lint_typecheck (push) Waiting to run
CI / node_test (macos-14) (push) Blocked by required conditions
CI / files-changed (push) Waiting to run
CI / build_and_test (--exclude bevy-example, ubuntu-22.04, 1.85) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, --exclude bevy-example, windows-2022, 1.85) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, macos-14, stable) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, beta) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, stable) (push) Blocked by required conditions
CI / build_and_test (ubuntu-22.04, nightly) (push) Blocked by required conditions
CI / node_test (ubuntu-22.04) (push) Blocked by required conditions
CI / node_test (windows-2022) (push) Blocked by required conditions
CI / python_test (macos-14) (push) Blocked by required conditions
CI / python_test (ubuntu-22.04) (push) Blocked by required conditions
CI / python_test (windows-2022) (push) Blocked by required conditions
CI / cpp_cmake (ubuntu-22.04, stable) (push) Blocked by required conditions
CI / cpp_test_driver (macos-14) (push) Blocked by required conditions
CI / cpp_test_driver (ubuntu-22.04) (push) Blocked by required conditions
CI / cpp_test_driver (windows-2022) (push) Blocked by required conditions
CI / cpp_cmake (macos-14, 1.85) (push) Blocked by required conditions
CI / cpp_cmake (windows-2022, nightly) (push) Blocked by required conditions
CI / cpp_package_test (push) Blocked by required conditions
CI / vsce_build_test (push) Blocked by required conditions
CI / mcu (pico-st7789, thumbv6m-none-eabi) (push) Blocked by required conditions
CI / mcu (pico2-st7789, thumbv8m.main-none-eabihf) (push) Blocked by required conditions
CI / mcu (stm32h735g, thumbv7em-none-eabihf) (push) Blocked by required conditions
CI / mcu-embassy (push) Blocked by required conditions
CI / ffi_32bit_build (push) Blocked by required conditions
CI / docs (push) Blocked by required conditions
CI / wasm (push) Blocked by required conditions
CI / wasm_demo (push) Blocked by required conditions
CI / tree-sitter (push) Blocked by required conditions
CI / updater_test (0.3.0) (push) Blocked by required conditions
CI / fmt_test (push) Blocked by required conditions
CI / esp-idf-quick (push) Blocked by required conditions
CI / android (push) Blocked by required conditions
CI / miri (push) Blocked by required conditions
CI / test-figma-inspector (push) Blocked by required conditions
CI / material-components (push) Blocked by required conditions

* Parlay init

* Start on femtovg

* Cargo fmt

* Decimate fonts.rs

* Use fill_glyphs

* [autofix.ci] apply automated fixes

* Use positioned_glyphs instead

* Clean up a little

* Format

* [autofix.ci] apply automated fixes

* Few fixes

* [autofix.ci] apply automated fixes

* More small changes

* Clean up

* [autofix.ci] apply automated fixes

* Display text cursor

* Handle text_input_cursor_rect_for_byte_offset

* stoke glyphs

* Handle text selections

* Stroke selection as well

* Fix wierd cargo.toml padding

* Move selection and stroking to brush settings

* Removed commented out code

* [autofix.ci] apply automated fixes

* Cursor sizing

* [autofix.ci] apply automated fixes

* Mark unused variables

* _scale -> scale

* Handle a lot more layout options

* Use the parley cursor

* Removed unused PhysicalPoint

* Start combining stuff WIP

* Move things into i_slint_core::textlayout::sharedparley

* [autofix.ci] apply automated fixes

* Go back to splitting paragraphs correctly

* Handle font_metrics via ttf_parser

* Move (lack of) overflow handling to sharedparley

* [autofix.ci] apply automated fixes

* impl Deref for Layout

* Be more explit about the width passed to layout being physical

* Cargo fmt, rename fonts to font_cache

* Use a thread local for layout context

* Use parley selection

* fix femtovg wgpu

* Switch femtovg branch

* max_physical_width -> max_width

* Box contexts

* [autofix.ci] apply automated fixes

* Fallback to GenericFamily::SansSerif if no font is set

* Add paint.set_font_size

* Use max_physical_height

* Fix text_size to return correct logical sizes

* Use femtovg from crates.io

* Fix C++ build

The `sharedparley` module declares a public `Layout` struct, which
clashes with the `Layout` struct we use in the C++ API. The former
however is entirely internal to Rust, so we can instruct cbindgen to
ignore the module.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev>
This commit is contained in:
Ashley 2025-09-25 16:48:48 +02:00 committed by GitHub
parent e4bf75b458
commit 13623bc152
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 406 additions and 937 deletions

View file

@ -7,7 +7,7 @@ pub use ttf_parser;
use std::collections::HashMap;
use std::sync::Arc;
static COLLECTION: std::sync::LazyLock<Collection> = std::sync::LazyLock::new(|| {
pub static COLLECTION: std::sync::LazyLock<Collection> = 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<HashMap<std::path::PathBuf, fontique::QueryFont>>,
}

View file

@ -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;

View file

@ -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 }

View file

@ -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

View file

@ -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};

View file

@ -0,0 +1,153 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<Brush>,
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<Box<Contexts>> = Default::default();
}
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub struct Brush {
pub stroke: Option<TextStrokeStyle>,
}
pub struct LayoutOptions {
pub max_width: Option<LogicalLength>,
pub max_height: Option<LogicalLength>,
pub horizontal_align: TextHorizontalAlignment,
pub vertical_align: TextVerticalAlignment,
pub stroke: Option<TextStrokeStyle>,
pub font_request: Option<FontRequest>,
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<Brush>,
pub y_offset: f32,
}
impl std::ops::Deref for Layout {
type Target = parley::Layout<Brush>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}

View file

@ -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" }

View file

@ -0,0 +1,39 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<FontCache> = RefCell::new(Default::default())
}

View file

@ -1,705 +0,0 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<i32>,
italic: bool,
}
#[derive(Clone)]
pub struct Font {
fonts: SharedVector<femtovg::FontId>,
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<PhysicalLength>,
) -> 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<LogicalLength>,
) -> 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<unicode_script::Script, bool>,
// 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<char, bool>,
}
enum GlyphCoverageCheckResult {
Incomplete,
Improved,
Complete,
}
pub struct FontCache {
loaded_fonts: HashMap<FontCacheKey, LoadedFont>,
// for a given font family id, this tells us what we've learned about the script
// coverage of the font.
loaded_font_coverage: HashMap<fontique::FamilyId, GlyphCoverage>,
pub(crate) text_context: TextContext,
available_families: HashSet<SharedString>,
}
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<FontCache> = 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<unicode_script::Script, char> = Default::default();
let mut chars_required: HashSet<char> = 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::<SharedVector<_>>();
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<SharedString> {
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::<Vec<_>>()
}
#[cfg(target_os = "windows")]
fn font_fallbacks_for_request(
&self,
_family: Option<&SharedString>,
_pixel_size: PhysicalLength,
_primary_font: &LoadedFont,
reference_text: &str,
) -> Vec<SharedString> {
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<u16> = 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<SharedString> {
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> {
[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> {
[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<unicode_script::Script, char>,
chars_without_coverage: &mut HashSet<char>,
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<usize>,
paint: &femtovg::Paint,
mut layout_line: impl FnMut(&str, PhysicalPoint, usize, &femtovg::TextMetrics),
) -> Option<PhysicalPoint> {
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<PhysicalPoint> = 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)
})
})
}

View file

@ -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<R> = BoxShadowCache<ItemGraphicsCacheEntry<R>>;
@ -194,6 +195,52 @@ impl<'a, R: femtovg::Renderer + TextureImporter> GLItemRenderer<'a, R> {
}
}
fn draw_glyphs<R: femtovg::Renderer + TextureImporter>(
layout: &sharedparley::Layout,
canvas: &mut Canvas<R>,
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(
&cursor_rect,
&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_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) {

View file

@ -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<f32, PhysicalPx>;
use self::itemrenderer::CanvasRc;
mod fonts;
mod font_cache;
mod images;
mod itemrenderer;
#[cfg(feature = "opengl")]
@ -285,9 +286,19 @@ impl<B: GraphicsBackend> RendererSealed for FemtoVGRenderer<B> {
text: &str,
max_width: Option<LogicalLength>,
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<B: GraphicsBackend> RendererSealed for FemtoVGRenderer<B> {
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<B: GraphicsBackend> RendererSealed for FemtoVGRenderer<B> {
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<B: GraphicsBackend> RendererSealed for FemtoVGRenderer<B> {
) -> 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<B: GraphicsBackend> RendererSealed for FemtoVGRenderer<B> {
);
}
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<B: GraphicsBackend> RendererSealed for FemtoVGRenderer<B> {
}
fn default_font_size(&self) -> LogicalLength {
self::fonts::DEFAULT_FONT_SIZE
sharedparley::DEFAULT_FONT_SIZE
}
fn set_rendering_notifier(

View file

@ -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();

View file

@ -159,7 +159,7 @@ impl FemtoVGRenderer<WGPUBackend> {
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();