mirror of
https://github.com/slint-ui/slint.git
synced 2025-12-23 09:19:32 +00:00
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
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:
parent
e4bf75b458
commit
13623bc152
13 changed files with 406 additions and 937 deletions
|
|
@ -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>>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
153
internal/core/textlayout/sharedparley.rs
Normal file
153
internal/core/textlayout/sharedparley.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
|
|
|
|||
39
internal/renderers/femtovg/font_cache.rs
Normal file
39
internal/renderers/femtovg/font_cache.rs
Normal 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())
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue