mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-03 07:04:34 +00:00
Add support for a font-metrics property to Text/TextInput. (#6452)
The struct held provides access to the design metrics of the font scaled to the font pixel size used by the element. ChangeLog: Slint Language: Added font-metrics property to `Text` and `TextInput`. Closes #6047
This commit is contained in:
parent
b41b389e55
commit
0b028bfb6f
36 changed files with 742 additions and 55 deletions
|
@ -784,6 +784,7 @@ When not part of a layout, its width or height defaults to 100% of the parent el
|
|||
- **`font-size`** (_in_ _length_): The font size of the text.
|
||||
- **`font-weight`** (_in_ _int_): The weight of the font. The values range from 100 (lightest) to 900 (thickest). 400 is the normal weight.
|
||||
- **`font-italic`** (_in_ _bool_): Whether or not the font face should be drawn italicized or not. (default value: false)
|
||||
- **`font-metrics`** (_out_ _struct [`FontMetrics`](structs.md#fontmetrics)_): The design metrics of the font scaled to the font pixel size used by the element.
|
||||
- **`has-focus`** (_out_ _bool_): `TextInput` sets this to `true` when it's focused. Only then it receives [`KeyEvent`](structs.md#keyevent)s.
|
||||
- **`horizontal-alignment`** (_in_ _enum [`TextHorizontalAlignment`](enums.md#texthorizontalalignment)_): The horizontal alignment of the text.
|
||||
- **`input-type`** (_in_ _enum [`InputType`](enums.md#inputtype)_): Use this to configure `TextInput` for editing special input, such as password fields. (default value: `text`)
|
||||
|
@ -847,6 +848,7 @@ and the text itself.
|
|||
- **`font-size`** (_in_ _length_): The font size of the text.
|
||||
- **`font-weight`** (_in_ _int_): The weight of the font. The values range from 100 (lightest) to 900 (thickest). 400 is the normal weight.
|
||||
- **`font-italic`** (_in_ _bool_): Whether or not the font face should be drawn italicized or not. (default value: false)
|
||||
- **`font-metrics`** (_out_ _struct [`FontMetrics`](structs.md#fontmetrics)_): The design metrics of the font scaled to the font pixel size used by the element.
|
||||
- **`horizontal-alignment`** (_in_ _enum [`TextHorizontalAlignment`](enums.md#texthorizontalalignment)_): The horizontal alignment of the text.
|
||||
- **`letter-spacing`** (_in_ _length_): The letter spacing allows changing the spacing between the glyphs. A positive value increases the spacing and a negative value decreases the distance. (default value: 0)
|
||||
- **`overflow`** (_in_ _enum [`TextOverflow`](enums.md#textoverflow)_): What happens when the text overflows (default value: clip).
|
||||
|
|
|
@ -2163,13 +2163,27 @@ impl i_slint_core::renderer::RendererSealed for QtWindow {
|
|||
_scale_factor: ScaleFactor,
|
||||
text_wrap: TextWrap,
|
||||
) -> LogicalSize {
|
||||
get_font(font_request).text_size(
|
||||
get_font(font_request).font_metrics().text_size(
|
||||
text,
|
||||
max_width.map(|logical_width| logical_width.get()),
|
||||
text_wrap,
|
||||
)
|
||||
}
|
||||
|
||||
fn font_metrics(
|
||||
&self,
|
||||
font_request: i_slint_core::graphics::FontRequest,
|
||||
_scale_factor: ScaleFactor,
|
||||
) -> i_slint_core::items::FontMetrics {
|
||||
let qt_font_metrics = get_font(font_request).font_metrics();
|
||||
i_slint_core::items::FontMetrics {
|
||||
ascent: qt_font_metrics.ascent(),
|
||||
descent: -qt_font_metrics.descent(),
|
||||
x_height: qt_font_metrics.x_height(),
|
||||
cap_height: qt_font_metrics.cap_height(),
|
||||
}
|
||||
}
|
||||
|
||||
fn text_input_byte_offset_for_position(
|
||||
&self,
|
||||
text_input: Pin<&i_slint_core::items::TextInput>,
|
||||
|
@ -2407,9 +2421,9 @@ fn get_font(request: FontRequest) -> QFont {
|
|||
})
|
||||
}
|
||||
|
||||
cpp_class! {pub unsafe struct QFont as "QFont"}
|
||||
cpp_class! {pub unsafe struct QFontMetricsF as "QFontMetricsF"}
|
||||
|
||||
impl QFont {
|
||||
impl QFontMetricsF {
|
||||
fn text_size(&self, text: &str, max_width: Option<f32>, text_wrap: TextWrap) -> LogicalSize {
|
||||
let string = qttypes::QString::from(text);
|
||||
let char_wrap = text_wrap == TextWrap::CharWrap;
|
||||
|
@ -2418,12 +2432,50 @@ impl QFont {
|
|||
r.height = f32::MAX as _;
|
||||
r.width = max as _;
|
||||
}
|
||||
let size = cpp! { unsafe [self as "const QFont*", string as "QString", r as "QRectF", char_wrap as "bool"]
|
||||
-> qttypes::QSizeF as "QSizeF"{
|
||||
return QFontMetricsF(*self).boundingRect(r, r.isEmpty() ? 0 : ((char_wrap) ? Qt::TextWrapAnywhere : Qt::TextWordWrap) , string).size();
|
||||
let size = cpp! { unsafe [self as "const QFontMetricsF*", string as "QString", r as "QRectF", char_wrap as "bool"]
|
||||
-> qttypes::QSizeF as "QSizeF" {
|
||||
return self->boundingRect(r, r.isEmpty() ? 0 : ((char_wrap) ? Qt::TextWrapAnywhere : Qt::TextWordWrap) , string).size();
|
||||
}};
|
||||
LogicalSize::new(size.width as _, size.height as _)
|
||||
}
|
||||
|
||||
fn ascent(&self) -> f32 {
|
||||
cpp! { unsafe [self as "const QFontMetricsF*"]
|
||||
-> f32 as "float" {
|
||||
return self->ascent();
|
||||
}}
|
||||
}
|
||||
|
||||
fn descent(&self) -> f32 {
|
||||
cpp! { unsafe [self as "const QFontMetricsF*"]
|
||||
-> f32 as "float" {
|
||||
return self->descent();
|
||||
}}
|
||||
}
|
||||
|
||||
fn cap_height(&self) -> f32 {
|
||||
cpp! { unsafe [self as "const QFontMetricsF*"]
|
||||
-> f32 as "float" {
|
||||
return self->capHeight();
|
||||
}}
|
||||
}
|
||||
|
||||
fn x_height(&self) -> f32 {
|
||||
cpp! { unsafe [self as "const QFontMetricsF*"]
|
||||
-> f32 as "float" {
|
||||
return self->xHeight();
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
cpp_class! {pub unsafe struct QFont as "QFont"}
|
||||
|
||||
impl QFont {
|
||||
fn font_metrics(&self) -> QFontMetricsF {
|
||||
cpp! { unsafe [self as "const QFont *"] -> QFontMetricsF as "QFontMetricsF" {
|
||||
return QFontMetricsF(*self);
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
|
|
|
@ -173,6 +173,14 @@ impl RendererSealed for TestingWindow {
|
|||
LogicalSize::new(text.len() as f32 * 10., 10.)
|
||||
}
|
||||
|
||||
fn font_metrics(
|
||||
&self,
|
||||
_font_request: i_slint_core::graphics::FontRequest,
|
||||
_scale_factor: ScaleFactor,
|
||||
) -> i_slint_core::items::FontMetrics {
|
||||
i_slint_core::items::FontMetrics { ascent: 7., descent: 3., x_height: 3., cap_height: 7. }
|
||||
}
|
||||
|
||||
// this works only for single line text
|
||||
fn text_input_byte_offset_for_position(
|
||||
&self,
|
||||
|
|
|
@ -18,10 +18,11 @@ path = "lib.rs"
|
|||
|
||||
[features]
|
||||
default = []
|
||||
shared-fontdb = ["dep:fontdb", "dep:libloading", "derive_more", "cfg-if"]
|
||||
shared-fontdb = ["dep:fontdb", "dep:libloading", "derive_more", "cfg-if", "dep:ttf-parser"]
|
||||
|
||||
[dependencies]
|
||||
fontdb = { workspace = true, optional = true }
|
||||
ttf-parser = { workspace = true, optional = true }
|
||||
derive_more = { workspace = true, optional = true }
|
||||
cfg-if = { version = "1", optional = true }
|
||||
|
||||
|
|
|
@ -162,6 +162,26 @@ macro_rules! for_each_builtin_structs {
|
|||
change_time: crate::animations::Instant,
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure to hold metrics of a font for a specified pixel size.
|
||||
struct FontMetrics {
|
||||
@name = "slint::private_api::FontMetrics"
|
||||
export {
|
||||
/// The distance between the baseline and the top of the tallest glyph in the font.
|
||||
ascent: Coord,
|
||||
/// The distance between the baseline and the bottom of the tallest glyph in the font.
|
||||
/// This is usually negative.
|
||||
descent: Coord,
|
||||
/// The distance between the baseline and the horizontal midpoint of the tallest glyph in the font,
|
||||
/// or zero if not specified by the font.
|
||||
x_height: Coord,
|
||||
/// The distance between the baseline and the top of a regular upper-case glyph in the font,
|
||||
/// or zero if not specified by the font.
|
||||
cap_height: Coord,
|
||||
}
|
||||
private {
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::cell::RefCell;
|
|||
use std::sync::Arc;
|
||||
|
||||
pub use fontdb;
|
||||
pub use ttf_parser;
|
||||
|
||||
#[derive(derive_more::Deref)]
|
||||
pub struct FontDatabase {
|
||||
|
@ -220,3 +221,26 @@ pub fn register_font_from_path(_path: &std::path::Path) -> Result<(), Box<dyn st
|
|||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
/// Font metrics in design space. Scale with desired pixel size and divided by units_per_em
|
||||
/// to obtain pixel metrics.
|
||||
#[derive(Clone)]
|
||||
pub struct DesignFontMetrics {
|
||||
pub ascent: f32,
|
||||
pub descent: f32,
|
||||
pub x_height: f32,
|
||||
pub cap_height: f32,
|
||||
pub units_per_em: f32,
|
||||
}
|
||||
|
||||
impl DesignFontMetrics {
|
||||
pub fn new(face: ttf_parser::Face<'_>) -> Self {
|
||||
Self {
|
||||
ascent: face.ascender() as f32,
|
||||
descent: face.descender() as f32,
|
||||
x_height: face.x_height().unwrap_or_default() as f32,
|
||||
cap_height: face.capital_height().unwrap_or_default() as f32,
|
||||
units_per_em: face.units_per_em() as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,6 +78,8 @@ pub struct BitmapFont {
|
|||
pub units_per_em: f32,
|
||||
pub ascent: f32,
|
||||
pub descent: f32,
|
||||
pub x_height: f32,
|
||||
pub cap_height: f32,
|
||||
pub glyphs: Vec<BitmapGlyphs>,
|
||||
pub weight: u16,
|
||||
pub italic: bool,
|
||||
|
|
|
@ -45,6 +45,7 @@ pub enum BuiltinFunction {
|
|||
SetSelectionOffsets,
|
||||
/// A function that belongs to an item (such as TextInput's select-all function).
|
||||
ItemMemberFunction(String),
|
||||
ItemFontMetrics,
|
||||
/// the "42".to_float()
|
||||
StringToFloat,
|
||||
/// the "42".is_float()
|
||||
|
@ -167,6 +168,10 @@ impl BuiltinFunction {
|
|||
return_type: Box::new(Type::Void),
|
||||
args: vec![Type::ElementReference],
|
||||
},
|
||||
BuiltinFunction::ItemFontMetrics => Type::Function {
|
||||
return_type: Box::new(crate::typeregister::font_metrics_type()),
|
||||
args: vec![Type::ElementReference],
|
||||
},
|
||||
BuiltinFunction::StringToFloat => {
|
||||
Type::Function { return_type: Box::new(Type::Float32), args: vec![Type::String] }
|
||||
}
|
||||
|
@ -353,6 +358,7 @@ impl BuiltinFunction {
|
|||
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
|
||||
BuiltinFunction::SetSelectionOffsets => false,
|
||||
BuiltinFunction::ItemMemberFunction(..) => false,
|
||||
BuiltinFunction::ItemFontMetrics => false, // depends also on Window's font properties
|
||||
BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true,
|
||||
BuiltinFunction::ColorRgbaStruct
|
||||
| BuiltinFunction::ColorHsvaStruct
|
||||
|
@ -419,6 +425,7 @@ impl BuiltinFunction {
|
|||
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
|
||||
BuiltinFunction::SetSelectionOffsets => false,
|
||||
BuiltinFunction::ItemMemberFunction(..) => false,
|
||||
BuiltinFunction::ItemFontMetrics => true,
|
||||
BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true,
|
||||
BuiltinFunction::ColorRgbaStruct
|
||||
| BuiltinFunction::ColorHsvaStruct
|
||||
|
|
|
@ -899,6 +899,8 @@ fn embed_resource(
|
|||
units_per_em,
|
||||
ascent,
|
||||
descent,
|
||||
x_height,
|
||||
cap_height,
|
||||
glyphs,
|
||||
weight,
|
||||
italic,
|
||||
|
@ -998,6 +1000,8 @@ fn embed_resource(
|
|||
.units_per_em = {units_per_em},
|
||||
.ascent = {ascent},
|
||||
.descent = {descent},
|
||||
.x_height = {x_height},
|
||||
.cap_height = {cap_height},
|
||||
.glyphs = slint::cbindgen_private::Slice<slint::cbindgen_private::BitmapGlyphs>{{ {glyphsets_var}, {glyphsets_size} }},
|
||||
.weight = {weight},
|
||||
.italic = {italic},
|
||||
|
@ -3542,6 +3546,15 @@ fn compile_builtin_function_call(
|
|||
panic!("internal error: invalid args to ItemMemberFunction {:?}", arguments)
|
||||
}
|
||||
}
|
||||
BuiltinFunction::ItemFontMetrics => {
|
||||
if let [llr::Expression::PropertyReference(pr)] = arguments {
|
||||
let item_rc = access_item_rc(pr, ctx);
|
||||
let window = access_window_field(ctx);
|
||||
format!("slint_cpp_text_item_fontmetrics(&{window}.handle(), &{item_rc})")
|
||||
} else {
|
||||
panic!("internal error: invalid args to ItemFontMetrics {:?}", arguments)
|
||||
}
|
||||
}
|
||||
BuiltinFunction::ItemAbsolutePosition => {
|
||||
if let [llr::Expression::PropertyReference(pr)] = arguments {
|
||||
let item_rc = access_item_rc(pr, ctx);
|
||||
|
|
|
@ -2644,6 +2644,19 @@ fn compile_builtin_function_call(
|
|||
panic!("internal error: invalid args to ItemMemberFunction {:?}", arguments)
|
||||
}
|
||||
}
|
||||
BuiltinFunction::ItemFontMetrics => {
|
||||
if let [Expression::PropertyReference(pr)] = arguments {
|
||||
let item = access_member(pr, ctx);
|
||||
let window_adapter_tokens = access_window_adapter_field(ctx);
|
||||
item.then(|item| {
|
||||
quote!(
|
||||
#item.font_metrics(#window_adapter_tokens)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
panic!("internal error: invalid args to ItemMemberFunction {:?}", arguments)
|
||||
}
|
||||
}
|
||||
BuiltinFunction::ImplicitLayoutInfo(orient) => {
|
||||
if let [Expression::PropertyReference(pr)] = arguments {
|
||||
let item = access_member(pr, ctx);
|
||||
|
@ -2985,7 +2998,7 @@ fn generate_resources(doc: &Document) -> Vec<TokenStream> {
|
|||
)
|
||||
},
|
||||
#[cfg(feature = "software-renderer")]
|
||||
crate::embedded_resources::EmbeddedResourcesKind::BitmapFontData(crate::embedded_resources::BitmapFont { family_name, character_map, units_per_em, ascent, descent, glyphs, weight, italic }) => {
|
||||
crate::embedded_resources::EmbeddedResourcesKind::BitmapFontData(crate::embedded_resources::BitmapFont { family_name, character_map, units_per_em, ascent, descent, x_height, cap_height, glyphs, weight, italic }) => {
|
||||
|
||||
let character_map_size = character_map.len();
|
||||
|
||||
|
@ -3037,6 +3050,8 @@ fn generate_resources(doc: &Document) -> Vec<TokenStream> {
|
|||
units_per_em: #units_per_em,
|
||||
ascent: #ascent,
|
||||
descent: #descent,
|
||||
x_height: #x_height,
|
||||
cap_height: #cap_height,
|
||||
glyphs: sp::Slice::from_slice({
|
||||
#link_section
|
||||
static GLYPHS : [sp::BitmapGlyphs; #glyphs_size] = [#(#glyphs),*];
|
||||
|
|
|
@ -347,19 +347,41 @@ impl Type {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
pub enum BuiltinPropertyDefault {
|
||||
None,
|
||||
Expr(Expression),
|
||||
Fn(fn(&crate::object_tree::ElementRc) -> Expression),
|
||||
}
|
||||
|
||||
impl BuiltinPropertyDefault {
|
||||
pub fn expr(&self, elem: &crate::object_tree::ElementRc) -> Option<Expression> {
|
||||
match self {
|
||||
BuiltinPropertyDefault::None => None,
|
||||
BuiltinPropertyDefault::Expr(expression) => Some(expression.clone()),
|
||||
BuiltinPropertyDefault::Fn(init_expr) => Some(init_expr(elem)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about properties in NativeClass
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuiltinPropertyInfo {
|
||||
/// The property type
|
||||
pub ty: Type,
|
||||
/// When set, this is the initial value that we will have to set if no other binding were specified
|
||||
pub default_value: Option<Expression>,
|
||||
/// When != None, this is the initial value that we will have to set if no other binding were specified
|
||||
pub default_value: BuiltinPropertyDefault,
|
||||
pub property_visibility: PropertyVisibility,
|
||||
}
|
||||
|
||||
impl BuiltinPropertyInfo {
|
||||
pub fn new(ty: Type) -> Self {
|
||||
Self { ty, default_value: None, property_visibility: PropertyVisibility::InOut }
|
||||
Self {
|
||||
ty,
|
||||
default_value: BuiltinPropertyDefault::None,
|
||||
property_visibility: PropertyVisibility::InOut,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_native_output(&self) -> bool {
|
||||
|
@ -405,7 +427,11 @@ impl ElementType {
|
|||
} else {
|
||||
Cow::Borrowed(name)
|
||||
};
|
||||
match b.properties.get(resolved_name.as_ref()) {
|
||||
match b
|
||||
.properties
|
||||
.get(resolved_name.as_ref())
|
||||
.or_else(|| b.reserved_properties.get(resolved_name.as_ref()))
|
||||
{
|
||||
None => {
|
||||
if b.is_non_item_type {
|
||||
PropertyLookupResult {
|
||||
|
@ -472,9 +498,12 @@ impl ElementType {
|
|||
);
|
||||
r
|
||||
}
|
||||
Self::Builtin(b) => {
|
||||
b.properties.iter().map(|(k, t)| (k.clone(), t.ty.clone())).collect()
|
||||
}
|
||||
Self::Builtin(b) => b
|
||||
.properties
|
||||
.iter()
|
||||
.chain(b.reserved_properties.iter())
|
||||
.map(|(k, t)| (k.clone(), t.ty.clone()))
|
||||
.collect(),
|
||||
Self::Native(n) => {
|
||||
n.properties.iter().map(|(k, t)| (k.clone(), t.ty.clone())).collect()
|
||||
}
|
||||
|
@ -719,6 +748,7 @@ pub struct BuiltinElement {
|
|||
pub name: String,
|
||||
pub native_class: Rc<NativeClass>,
|
||||
pub properties: BTreeMap<String, BuiltinPropertyInfo>,
|
||||
pub reserved_properties: BTreeMap<String, BuiltinPropertyInfo>,
|
||||
pub additional_accepted_child_types: HashMap<String, ElementType>,
|
||||
pub disallow_global_types_as_child_elements: bool,
|
||||
/// Non-item type do not have reserved properties (x/width/rowspan/...) added to them (eg: PropertyAnimation)
|
||||
|
|
|
@ -99,6 +99,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
|
|||
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => isize::MAX,
|
||||
BuiltinFunction::SetSelectionOffsets => isize::MAX,
|
||||
BuiltinFunction::ItemMemberFunction(..) => isize::MAX,
|
||||
BuiltinFunction::ItemFontMetrics => PROPERTY_ACCESS_COST,
|
||||
BuiltinFunction::StringToFloat => 50,
|
||||
BuiltinFunction::StringIsFloat => 50,
|
||||
BuiltinFunction::ColorRgbaStruct => 50,
|
||||
|
|
|
@ -11,7 +11,8 @@ use std::rc::Rc;
|
|||
|
||||
use crate::expression_tree::{BuiltinFunction, Expression};
|
||||
use crate::langtype::{
|
||||
BuiltinElement, BuiltinPropertyInfo, DefaultSizeBinding, ElementType, NativeClass, Type,
|
||||
BuiltinElement, BuiltinPropertyDefault, BuiltinPropertyInfo, DefaultSizeBinding, ElementType,
|
||||
NativeClass, Type,
|
||||
};
|
||||
use crate::object_tree::{self, *};
|
||||
use crate::parser::{identifier_text, syntax_nodes, SyntaxKind, SyntaxNode};
|
||||
|
@ -93,7 +94,7 @@ pub(crate) fn load_builtins(register: &mut TypeRegister) {
|
|||
|
||||
if let Some(e) = p.BindingExpression() {
|
||||
let ty = info.ty.clone();
|
||||
info.default_value = Some(compiled(e, register, ty));
|
||||
info.default_value = BuiltinPropertyDefault::Expr(compiled(e, register, ty));
|
||||
}
|
||||
|
||||
(prop_name, info)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
use crate::diagnostics::{BuildDiagnostics, SourceLocation, Spanned};
|
||||
use crate::expression_tree::{self, BindingExpression, Expression, Unit};
|
||||
use crate::langtype::{BuiltinElement, Enumeration, NativeClass, Type};
|
||||
use crate::langtype::{BuiltinElement, BuiltinPropertyDefault, Enumeration, NativeClass, Type};
|
||||
use crate::langtype::{ElementType, PropertyLookupResult};
|
||||
use crate::layout::{LayoutConstraints, Orientation};
|
||||
use crate::namedreference::NamedReference;
|
||||
|
@ -1851,7 +1851,7 @@ fn apply_default_type_properties(element: &mut Element) {
|
|||
// Apply default property values on top:
|
||||
if let ElementType::Builtin(builtin_base) = &element.base_type {
|
||||
for (prop, info) in &builtin_base.properties {
|
||||
if let Some(expr) = &info.default_value {
|
||||
if let BuiltinPropertyDefault::Expr(expr) = &info.default_value {
|
||||
element.bindings.entry(prop.clone()).or_insert_with(|| {
|
||||
let mut binding = BindingExpression::from(expr.clone());
|
||||
binding.priority = i32::MAX;
|
||||
|
|
|
@ -20,6 +20,7 @@ struct Font {
|
|||
id: fontdb::ID,
|
||||
#[deref]
|
||||
fontdue_font: Rc<fontdue::Font>,
|
||||
metrics: i_slint_common::sharedfontdb::DesignFontMetrics,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
@ -212,6 +213,20 @@ fn embed_glyphs_with_fontdb<'a>(
|
|||
}
|
||||
};
|
||||
|
||||
let Some(Ok(metrics)) = i_slint_common::sharedfontdb::FONT_DB.with(|fontdb| {
|
||||
fontdb.borrow().with_face_data(face_id, |font_data, face_index| {
|
||||
i_slint_common::sharedfontdb::ttf_parser::Face::parse(font_data, face_index)
|
||||
.map(|face| i_slint_common::sharedfontdb::DesignFontMetrics::new(face))
|
||||
})
|
||||
}) else {
|
||||
diag.push_error(
|
||||
format!("error parsing font for embedding {}", path.display()),
|
||||
&generic_diag_location,
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(family_name) = fontdb
|
||||
.face(face_id)
|
||||
.expect("must succeed as we are within face_data with same face_id")
|
||||
|
@ -232,7 +247,7 @@ fn embed_glyphs_with_fontdb<'a>(
|
|||
let embedded_bitmap_font = embed_font(
|
||||
&fontdb,
|
||||
family_name,
|
||||
Font { id: face_id, fontdue_font },
|
||||
Font { id: face_id, fontdue_font, metrics },
|
||||
&pixel_sizes,
|
||||
characters_seen.iter().cloned(),
|
||||
&fallback_fonts,
|
||||
|
@ -373,19 +388,16 @@ fn embed_font(
|
|||
})
|
||||
.collect();
|
||||
|
||||
// Get the basic metrics in design coordinates
|
||||
let metrics = font
|
||||
.horizontal_line_metrics(font.units_per_em())
|
||||
.expect("encountered font without hmtx table");
|
||||
|
||||
let face_info = fontdb.face(font.id).unwrap();
|
||||
|
||||
BitmapFont {
|
||||
family_name,
|
||||
character_map,
|
||||
units_per_em: font.units_per_em(),
|
||||
ascent: metrics.ascent,
|
||||
descent: metrics.descent,
|
||||
units_per_em: font.metrics.units_per_em,
|
||||
ascent: font.metrics.ascent,
|
||||
descent: font.metrics.descent,
|
||||
x_height: font.metrics.x_height,
|
||||
cap_height: font.metrics.cap_height,
|
||||
glyphs,
|
||||
weight: face_info.weight.0,
|
||||
italic: face_info.style != fontdb::Style::Normal,
|
||||
|
|
|
@ -93,7 +93,12 @@ fn should_materialize(
|
|||
}
|
||||
let has_declared_property = match base_type {
|
||||
ElementType::Component(c) => has_declared_property(&c.root_element.borrow(), prop),
|
||||
ElementType::Builtin(b) => b.properties.contains_key(prop),
|
||||
ElementType::Builtin(b) => {
|
||||
if let Some(info) = b.reserved_properties.get(prop) {
|
||||
return Some(info.ty.clone());
|
||||
}
|
||||
b.properties.contains_key(prop)
|
||||
}
|
||||
ElementType::Native(n) => {
|
||||
n.lookup_property(prop).map_or(false, |prop_type| prop_type.is_property_type())
|
||||
}
|
||||
|
@ -128,6 +133,14 @@ fn has_declared_property(elem: &Element, prop: &str) -> bool {
|
|||
|
||||
/// Initialize a sensible default binding for the now materialized property
|
||||
pub fn initialize(elem: &ElementRc, name: &str) -> Option<Expression> {
|
||||
if let ElementType::Builtin(b) = &elem.borrow().base_type {
|
||||
if let Some(expr) =
|
||||
b.reserved_properties.get(name).and_then(|prop| prop.default_value.expr(elem))
|
||||
{
|
||||
return Some(expr);
|
||||
}
|
||||
}
|
||||
|
||||
let expr = match name {
|
||||
"min-height" => layout_constraint_prop(elem, "min", Orientation::Vertical),
|
||||
"min-width" => layout_constraint_prop(elem, "min", Orientation::Horizontal),
|
||||
|
|
13
internal/compiler/tests/syntax/elements/text.slint
Normal file
13
internal/compiler/tests/syntax/elements/text.slint
Normal file
|
@ -0,0 +1,13 @@
|
|||
// 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
|
||||
|
||||
export component Foo inherits Rectangle {
|
||||
Text {
|
||||
font-metrics: 42;
|
||||
// ^error{Cannot assign to output property 'font-metrics'}
|
||||
// ^^error{Cannot convert float to slint::private_api::FontMetrics}
|
||||
|
||||
}
|
||||
|
||||
property <length> font-metrics: 100px;
|
||||
}
|
|
@ -9,7 +9,8 @@ use std::rc::Rc;
|
|||
|
||||
use crate::expression_tree::BuiltinFunction;
|
||||
use crate::langtype::{
|
||||
BuiltinElement, BuiltinPropertyInfo, ElementType, Enumeration, PropertyLookupResult, Type,
|
||||
BuiltinElement, BuiltinPropertyDefault, BuiltinPropertyInfo, ElementType, Enumeration,
|
||||
PropertyLookupResult, Type,
|
||||
};
|
||||
use crate::object_tree::{Component, PropertyVisibility};
|
||||
use crate::typeloader;
|
||||
|
@ -370,6 +371,25 @@ impl TypeRegister {
|
|||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let font_metrics_prop = crate::langtype::BuiltinPropertyInfo {
|
||||
ty: font_metrics_type(),
|
||||
property_visibility: PropertyVisibility::Output,
|
||||
default_value: BuiltinPropertyDefault::Fn(|elem| {
|
||||
crate::expression_tree::Expression::FunctionCall {
|
||||
function: Box::new(
|
||||
crate::expression_tree::Expression::BuiltinFunctionReference(
|
||||
BuiltinFunction::ItemFontMetrics,
|
||||
None,
|
||||
),
|
||||
),
|
||||
arguments: vec![crate::expression_tree::Expression::ElementReference(
|
||||
Rc::downgrade(elem),
|
||||
)],
|
||||
source_location: None,
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
match &mut register.elements.get_mut("TextInput").unwrap() {
|
||||
ElementType::Builtin(ref mut b) => {
|
||||
let text_input = Rc::get_mut(b).unwrap();
|
||||
|
@ -380,6 +400,18 @@ impl TypeRegister {
|
|||
text_input
|
||||
.member_functions
|
||||
.insert("set-selection-offsets".into(), BuiltinFunction::SetSelectionOffsets);
|
||||
text_input
|
||||
.reserved_properties
|
||||
.insert("font-metrics".into(), font_metrics_prop.clone());
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match &mut register.elements.get_mut("Text").unwrap() {
|
||||
ElementType::Builtin(ref mut b) => {
|
||||
let text = Rc::get_mut(b).unwrap();
|
||||
text.reserved_properties.insert("font-metrics".into(), font_metrics_prop);
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
|
@ -548,3 +580,18 @@ pub fn logical_point_type() -> Type {
|
|||
rust_attributes: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font_metrics_type() -> Type {
|
||||
Type::Struct {
|
||||
fields: IntoIterator::into_iter([
|
||||
("ascent".to_string(), Type::LogicalLength),
|
||||
("descent".to_string(), Type::LogicalLength),
|
||||
("x-height".to_string(), Type::LogicalLength),
|
||||
("cap-height".to_string(), Type::LogicalLength),
|
||||
])
|
||||
.collect(),
|
||||
name: Some("slint::private_api::FontMetrics".into()),
|
||||
node: None,
|
||||
rust_attributes: None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,10 @@ pub struct BitmapFont {
|
|||
pub ascent: f32,
|
||||
/// The font descent in design metrics (typically negative)
|
||||
pub descent: f32,
|
||||
/// The font's x-height.
|
||||
pub x_height: f32,
|
||||
/// The font's cap-height.
|
||||
pub cap_height: f32,
|
||||
/// A vector of pre-rendered glyph sets. Each glyph set must have the same number of glyphs,
|
||||
/// which must be at least as big as the largest glyph index in the character map.
|
||||
pub glyphs: Slice<'static, BitmapGlyphs>,
|
||||
|
|
|
@ -8,9 +8,9 @@ When adding an item or a property, it needs to be kept in sync with different pl
|
|||
Lookup the [`crate::items`] module documentation.
|
||||
*/
|
||||
use super::{
|
||||
InputType, Item, ItemConsts, ItemRc, KeyEventResult, KeyEventType, PointArg,
|
||||
PointerEventButton, RenderingResult, TextHorizontalAlignment, TextOverflow, TextStrokeStyle,
|
||||
TextVerticalAlignment, TextWrap, VoidArg,
|
||||
FontMetrics, InputType, Item, ItemConsts, ItemRc, ItemRef, KeyEventResult, KeyEventType,
|
||||
PointArg, PointerEventButton, RenderingResult, TextHorizontalAlignment, TextOverflow,
|
||||
TextStrokeStyle, TextVerticalAlignment, TextWrap, VoidArg,
|
||||
};
|
||||
use crate::graphics::{Brush, Color, FontRequest};
|
||||
use crate::input::{
|
||||
|
@ -200,6 +200,15 @@ impl RenderText for ComplexText {
|
|||
}
|
||||
}
|
||||
|
||||
impl ComplexText {
|
||||
pub fn font_metrics(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>) -> FontMetrics {
|
||||
let window_inner = WindowInner::from_pub(window_adapter.window());
|
||||
let scale_factor = ScaleFactor::new(window_inner.scale_factor());
|
||||
let font_request = self.font_request(window_inner);
|
||||
window_adapter.renderer().font_metrics(font_request, scale_factor)
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of the `Text` element
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement)]
|
||||
|
@ -349,6 +358,15 @@ impl RenderText for SimpleText {
|
|||
}
|
||||
}
|
||||
|
||||
impl SimpleText {
|
||||
pub fn font_metrics(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>) -> FontMetrics {
|
||||
let window_inner = WindowInner::from_pub(window_adapter.window());
|
||||
let scale_factor = ScaleFactor::new(window_inner.scale_factor());
|
||||
let font_request = self.font_request(window_inner);
|
||||
window_adapter.renderer().font_metrics(font_request, scale_factor)
|
||||
}
|
||||
}
|
||||
|
||||
fn text_layout_info(
|
||||
text: Pin<&dyn RenderText>,
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
|
@ -1822,6 +1840,13 @@ impl TextInput {
|
|||
undo_items.push(last);
|
||||
self.undo_items.set(undo_items);
|
||||
}
|
||||
|
||||
pub fn font_metrics(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>) -> FontMetrics {
|
||||
let window_inner = WindowInner::from_pub(window_adapter.window());
|
||||
let scale_factor = ScaleFactor::new(window_inner.scale_factor());
|
||||
let font_request = self.font_request(window_adapter);
|
||||
window_adapter.renderer().font_metrics(font_request, scale_factor)
|
||||
}
|
||||
}
|
||||
|
||||
fn next_paragraph_boundary(text: &str, last_cursor_pos: usize) -> usize {
|
||||
|
@ -1944,3 +1969,31 @@ pub unsafe extern "C" fn slint_textinput_paste(
|
|||
let self_rc = ItemRc::new(self_component.clone(), self_index);
|
||||
text_input.paste(window_adapter, &self_rc);
|
||||
}
|
||||
|
||||
pub fn slint_text_item_fontmetrics(
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
item_ref: Pin<ItemRef<'_>>,
|
||||
) -> FontMetrics {
|
||||
if let Some(simple_text) = ItemRef::downcast_pin::<SimpleText>(item_ref) {
|
||||
simple_text.font_metrics(&window_adapter)
|
||||
} else if let Some(complex_text) = ItemRef::downcast_pin::<ComplexText>(item_ref) {
|
||||
complex_text.font_metrics(&window_adapter)
|
||||
} else if let Some(text_input) = ItemRef::downcast_pin::<TextInput>(item_ref) {
|
||||
text_input.font_metrics(&window_adapter)
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn slint_cpp_text_item_fontmetrics(
|
||||
window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
|
||||
self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
|
||||
self_index: u32,
|
||||
) -> FontMetrics {
|
||||
let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
|
||||
let self_rc = ItemRc::new(self_component.clone(), self_index);
|
||||
let self_ref = self_rc.borrow();
|
||||
slint_text_item_fontmetrics(window_adapter, self_ref)
|
||||
}
|
||||
|
|
|
@ -38,6 +38,13 @@ pub trait RendererSealed {
|
|||
text_wrap: TextWrap,
|
||||
) -> LogicalSize;
|
||||
|
||||
/// Returns the metrics of the given font.
|
||||
fn font_metrics(
|
||||
&self,
|
||||
font_request: crate::graphics::FontRequest,
|
||||
scale_factor: ScaleFactor,
|
||||
) -> crate::items::FontMetrics;
|
||||
|
||||
/// Returns the (UTF-8) byte offset in the text property that refers to the character that contributed to
|
||||
/// the glyph cluster that's visually nearest to the given coordinate. This is used for hit-testing,
|
||||
/// for example when receiving a mouse click into a text field. Then this function returns the "cursor"
|
||||
|
|
|
@ -47,6 +47,7 @@ macro_rules! declare_ValueType_2 {
|
|||
crate::lengths::LogicalLength,
|
||||
crate::component_factory::ComponentFactory,
|
||||
crate::api::LogicalPosition,
|
||||
crate::items::FontMetrics,
|
||||
$(crate::items::$Name,)*
|
||||
];
|
||||
};
|
||||
|
|
|
@ -672,6 +672,14 @@ impl RendererSealed for SoftwareRenderer {
|
|||
fonts::text_size(font_request, text, max_width, scale_factor, text_wrap)
|
||||
}
|
||||
|
||||
fn font_metrics(
|
||||
&self,
|
||||
font_request: crate::graphics::FontRequest,
|
||||
scale_factor: ScaleFactor,
|
||||
) -> crate::items::FontMetrics {
|
||||
fonts::font_metrics(font_request, scale_factor)
|
||||
}
|
||||
|
||||
fn text_input_byte_offset_for_position(
|
||||
&self,
|
||||
text_input: Pin<&crate::items::TextInput>,
|
||||
|
|
|
@ -13,7 +13,7 @@ use super::{PhysicalLength, PhysicalSize};
|
|||
use crate::graphics::{BitmapFont, FontRequest};
|
||||
use crate::items::TextWrap;
|
||||
use crate::lengths::{LogicalLength, LogicalSize, ScaleFactor};
|
||||
use crate::textlayout::TextLayout;
|
||||
use crate::textlayout::{FontMetrics, TextLayout};
|
||||
use crate::Coord;
|
||||
|
||||
thread_local! {
|
||||
|
@ -61,6 +61,48 @@ pub enum Font {
|
|||
VectorFont(vectorfont::VectorFont),
|
||||
}
|
||||
|
||||
impl crate::textlayout::FontMetrics<PhysicalLength> for Font {
|
||||
fn ascent(&self) -> PhysicalLength {
|
||||
match self {
|
||||
Font::PixelFont(pixel_font) => pixel_font.ascent(),
|
||||
#[cfg(all(feature = "software-renderer-systemfonts", not(target_arch = "wasm32")))]
|
||||
Font::VectorFont(vector_font) => vector_font.ascent(),
|
||||
}
|
||||
}
|
||||
|
||||
fn height(&self) -> PhysicalLength {
|
||||
match self {
|
||||
Font::PixelFont(pixel_font) => pixel_font.height(),
|
||||
#[cfg(all(feature = "software-renderer-systemfonts", not(target_arch = "wasm32")))]
|
||||
Font::VectorFont(vector_font) => vector_font.height(),
|
||||
}
|
||||
}
|
||||
|
||||
fn descent(&self) -> PhysicalLength {
|
||||
match self {
|
||||
Font::PixelFont(pixel_font) => pixel_font.descent(),
|
||||
#[cfg(all(feature = "software-renderer-systemfonts", not(target_arch = "wasm32")))]
|
||||
Font::VectorFont(vector_font) => vector_font.descent(),
|
||||
}
|
||||
}
|
||||
|
||||
fn x_height(&self) -> PhysicalLength {
|
||||
match self {
|
||||
Font::PixelFont(pixel_font) => pixel_font.x_height(),
|
||||
#[cfg(all(feature = "software-renderer-systemfonts", not(target_arch = "wasm32")))]
|
||||
Font::VectorFont(vector_font) => vector_font.x_height(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cap_height(&self) -> PhysicalLength {
|
||||
match self {
|
||||
Font::PixelFont(pixel_font) => pixel_font.cap_height(),
|
||||
#[cfg(all(feature = "software-renderer-systemfonts", not(target_arch = "wasm32")))]
|
||||
Font::VectorFont(vector_font) => vector_font.cap_height(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_font(request: &FontRequest, scale_factor: ScaleFactor) -> Font {
|
||||
let requested_weight = request
|
||||
.weight
|
||||
|
@ -173,3 +215,22 @@ pub fn text_size(
|
|||
|
||||
(PhysicalSize::from_lengths(longest_line_width, height).cast() / scale_factor).cast()
|
||||
}
|
||||
|
||||
pub fn font_metrics(
|
||||
font_request: FontRequest,
|
||||
scale_factor: ScaleFactor,
|
||||
) -> crate::items::FontMetrics {
|
||||
let font = match_font(&font_request, scale_factor);
|
||||
|
||||
let ascent: LogicalLength = (font.ascent().cast() / scale_factor).cast();
|
||||
let descent: LogicalLength = (font.descent().cast() / scale_factor).cast();
|
||||
let x_height: LogicalLength = (font.x_height().cast() / scale_factor).cast();
|
||||
let cap_height: LogicalLength = (font.cap_height().cast() / scale_factor).cast();
|
||||
|
||||
crate::items::FontMetrics {
|
||||
ascent: ascent.get() as _,
|
||||
descent: descent.get() as _,
|
||||
x_height: x_height.get() as _,
|
||||
cap_height: cap_height.get() as _,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,14 @@ impl BitmapGlyphs {
|
|||
pub fn pixel_size(&self) -> PhysicalLength {
|
||||
PhysicalLength::new(self.pixel_size)
|
||||
}
|
||||
/// Returns the x-height of the font scaled to the font's pixel size.
|
||||
pub fn x_height(&self, font: &BitmapFont) -> PhysicalLength {
|
||||
(PhysicalLength::new(self.pixel_size).cast() * font.x_height / font.units_per_em).cast()
|
||||
}
|
||||
/// Returns the cap-height of the font scaled to the font's pixel size.
|
||||
pub fn cap_height(&self, font: &BitmapFont) -> PhysicalLength {
|
||||
(PhysicalLength::new(self.pixel_size).cast() * font.cap_height / font.units_per_em).cast()
|
||||
}
|
||||
}
|
||||
|
||||
// A font that is resolved to a specific pixel size.
|
||||
|
@ -127,4 +135,12 @@ impl crate::textlayout::FontMetrics<PhysicalLength> for PixelFont {
|
|||
fn height(&self) -> PhysicalLength {
|
||||
self.glyphs.height(self.bitmap_font)
|
||||
}
|
||||
|
||||
fn x_height(&self) -> PhysicalLength {
|
||||
self.glyphs.x_height(&self.bitmap_font)
|
||||
}
|
||||
|
||||
fn cap_height(&self) -> PhysicalLength {
|
||||
self.glyphs.cap_height(&self.bitmap_font)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ pub struct VectorFont {
|
|||
height: PhysicalLength,
|
||||
scale: FontScaleFactor,
|
||||
pixel_size: PhysicalLength,
|
||||
x_height: PhysicalLength,
|
||||
cap_height: PhysicalLength,
|
||||
}
|
||||
|
||||
impl VectorFont {
|
||||
|
@ -68,6 +70,9 @@ impl VectorFont {
|
|||
let ascender = FontLength::new(face.ascender() as _);
|
||||
let descender = FontLength::new(face.descender() as _);
|
||||
let height = FontLength::new(face.height() as _);
|
||||
let x_height = FontLength::new(face.x_height().unwrap_or_default() as _);
|
||||
let cap_height =
|
||||
FontLength::new(face.capital_height().unwrap_or_default() as _);
|
||||
let units_per_em = face.units_per_em();
|
||||
let scale = FontScaleFactor::new(pixel_size.get() as f32 / units_per_em as f32);
|
||||
Self {
|
||||
|
@ -78,6 +83,8 @@ impl VectorFont {
|
|||
height: (height.cast() * scale).cast(),
|
||||
scale,
|
||||
pixel_size,
|
||||
x_height: (x_height.cast() * scale).cast(),
|
||||
cap_height: (cap_height.cast() * scale).cast(),
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
|
@ -173,6 +180,14 @@ impl crate::textlayout::FontMetrics<PhysicalLength> for VectorFont {
|
|||
fn descent(&self) -> PhysicalLength {
|
||||
self.descender
|
||||
}
|
||||
|
||||
fn x_height(&self) -> PhysicalLength {
|
||||
self.x_height
|
||||
}
|
||||
|
||||
fn cap_height(&self) -> PhysicalLength {
|
||||
self.cap_height
|
||||
}
|
||||
}
|
||||
|
||||
impl super::GlyphRenderer for VectorFont {
|
||||
|
|
|
@ -413,6 +413,14 @@ impl FontMetrics<f32> for FixedTestFont {
|
|||
fn descent(&self) -> f32 {
|
||||
-5.
|
||||
}
|
||||
|
||||
fn x_height(&self) -> f32 {
|
||||
3.
|
||||
}
|
||||
|
||||
fn cap_height(&self) -> f32 {
|
||||
4.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -74,6 +74,8 @@ pub trait FontMetrics<Length: Copy + core::ops::Sub<Output = Length>> {
|
|||
}
|
||||
fn ascent(&self) -> Length;
|
||||
fn descent(&self) -> Length;
|
||||
fn x_height(&self) -> Length;
|
||||
fn cap_height(&self) -> Length;
|
||||
}
|
||||
|
||||
pub trait AbstractFont: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
|
||||
|
@ -298,6 +300,14 @@ impl<'a> FontMetrics<f32> for &rustybuzz::Face<'a> {
|
|||
fn descent(&self) -> f32 {
|
||||
self.descender() as _
|
||||
}
|
||||
|
||||
fn x_height(&self) -> f32 {
|
||||
rustybuzz::ttf_parser::Face::x_height(self).unwrap_or_default() as _
|
||||
}
|
||||
|
||||
fn cap_height(&self) -> f32 {
|
||||
rustybuzz::ttf_parser::Face::capital_height(self).unwrap_or_default() as _
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -745,6 +745,37 @@ fn call_builtin_function(
|
|||
panic!("internal error: argument to set-selection-offsetsAll must be an element")
|
||||
}
|
||||
}
|
||||
BuiltinFunction::ItemFontMetrics => {
|
||||
if arguments.len() != 1 {
|
||||
panic!(
|
||||
"internal error: incorrect argument count to item font metrics function call"
|
||||
)
|
||||
}
|
||||
let component = match local_context.component_instance {
|
||||
ComponentInstance::InstanceRef(c) => c,
|
||||
ComponentInstance::GlobalComponent(_) => {
|
||||
panic!(
|
||||
"Cannot invoke item font metrics function on item from a global component"
|
||||
)
|
||||
}
|
||||
};
|
||||
if let Expression::ElementReference(element) = &arguments[0] {
|
||||
generativity::make_guard!(guard);
|
||||
|
||||
let elem = element.upgrade().unwrap();
|
||||
let enclosing_component = enclosing_component_for_element(&elem, component, guard);
|
||||
let description = enclosing_component.description;
|
||||
let item_info = &description.items[elem.borrow().id.as_str()];
|
||||
let item_ref =
|
||||
unsafe { item_info.item_from_item_tree(enclosing_component.as_ptr()) };
|
||||
let window_adapter = component.window_adapter();
|
||||
let metrics =
|
||||
i_slint_core::items::slint_text_item_fontmetrics(&window_adapter, item_ref);
|
||||
metrics.into()
|
||||
} else {
|
||||
panic!("internal error: argument to set-selection-offsetsAll must be an element")
|
||||
}
|
||||
}
|
||||
BuiltinFunction::StringIsFloat => {
|
||||
if arguments.len() != 1 {
|
||||
panic!("internal error: incorrect argument count to StringIsFloat")
|
||||
|
|
|
@ -107,10 +107,32 @@ pub(crate) fn text_size(
|
|||
/ scale_factor
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) fn font_metrics(
|
||||
font_request: i_slint_core::graphics::FontRequest,
|
||||
) -> i_slint_core::items::FontMetrics {
|
||||
let primary_font = FONT_CACHE.with(|cache| {
|
||||
let query = font_request.to_fontdb_query();
|
||||
|
||||
cache.borrow_mut().load_single_font(font_request.family.as_ref(), query)
|
||||
});
|
||||
|
||||
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,
|
||||
fontdb_face_id: fontdb::ID,
|
||||
design_font_metrics: i_slint_common::sharedfontdb::DesignFontMetrics,
|
||||
}
|
||||
|
||||
struct SharedFontData(std::sync::Arc<dyn AsRef<[u8]>>);
|
||||
|
@ -189,7 +211,7 @@ impl FontCache {
|
|||
};
|
||||
|
||||
if let Some(loaded_font) = self.loaded_fonts.get(&cache_key) {
|
||||
return *loaded_font;
|
||||
return loaded_font.clone();
|
||||
}
|
||||
|
||||
//let now = std::time::Instant::now();
|
||||
|
@ -233,13 +255,18 @@ impl FontCache {
|
|||
.expect("invalid fontdb face id")
|
||||
});
|
||||
|
||||
let design_font_metrics = {
|
||||
let face = ttf_parser::Face::parse(shared_data.as_ref().as_ref(), face_index).unwrap();
|
||||
i_slint_common::sharedfontdb::DesignFontMetrics::new(face)
|
||||
};
|
||||
|
||||
let femtovg_font_id = text_context
|
||||
.add_shared_font_with_index(SharedFontData(shared_data), face_index)
|
||||
.unwrap();
|
||||
|
||||
//println!("Loaded {:#?} in {}ms.", request, now.elapsed().as_millis());
|
||||
let new_font = LoadedFont { femtovg_font_id, fontdb_face_id };
|
||||
self.loaded_fonts.insert(cache_key, new_font);
|
||||
let new_font = LoadedFont { femtovg_font_id, fontdb_face_id, design_font_metrics };
|
||||
self.loaded_fonts.insert(cache_key, new_font.clone());
|
||||
new_font
|
||||
}
|
||||
|
||||
|
|
|
@ -357,6 +357,14 @@ impl RendererSealed for FemtoVGRenderer {
|
|||
crate::fonts::text_size(&font_request, scale_factor, text, max_width)
|
||||
}
|
||||
|
||||
fn font_metrics(
|
||||
&self,
|
||||
font_request: i_slint_core::graphics::FontRequest,
|
||||
_scale_factor: ScaleFactor,
|
||||
) -> i_slint_core::items::FontMetrics {
|
||||
crate::fonts::font_metrics(font_request)
|
||||
}
|
||||
|
||||
fn text_input_byte_offset_for_position(
|
||||
&self,
|
||||
text_input: Pin<&i_slint_core::items::TextInput>,
|
||||
|
|
|
@ -426,6 +426,14 @@ impl i_slint_core::renderer::RendererSealed for SkiaRenderer {
|
|||
/ scale_factor
|
||||
}
|
||||
|
||||
fn font_metrics(
|
||||
&self,
|
||||
font_request: i_slint_core::graphics::FontRequest,
|
||||
scale_factor: ScaleFactor,
|
||||
) -> i_slint_core::items::FontMetrics {
|
||||
textlayout::font_metrics(font_request, scale_factor)
|
||||
}
|
||||
|
||||
fn text_input_byte_offset_for_position(
|
||||
&self,
|
||||
text_input: std::pin::Pin<&i_slint_core::items::TextInput>,
|
||||
|
|
|
@ -22,7 +22,7 @@ enum CustomFontSource {
|
|||
}
|
||||
|
||||
struct FontCache {
|
||||
font_collection: skia_safe::textlayout::FontCollection,
|
||||
font_collection: RefCell<skia_safe::textlayout::FontCollection>,
|
||||
font_mgr: skia_safe::FontMgr,
|
||||
type_face_font_provider: RefCell<skia_safe::textlayout::TypefaceFontProvider>,
|
||||
custom_fonts: RefCell<HashMap<String, CustomFontSource>>,
|
||||
|
@ -39,7 +39,7 @@ thread_local! {
|
|||
// to pick up the custom font.
|
||||
font_collection.set_asset_font_manager(Some(type_face_font_provider.clone().into()));
|
||||
font_collection.set_dynamic_font_manager(font_mgr.clone());
|
||||
FontCache { font_collection, font_mgr, type_face_font_provider: RefCell::new(type_face_font_provider), custom_fonts: Default::default() }
|
||||
FontCache { font_collection: RefCell::new(font_collection), font_mgr, type_face_font_provider: RefCell::new(type_face_font_provider), custom_fonts: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,18 @@ pub struct Selection {
|
|||
pub underline: bool,
|
||||
}
|
||||
|
||||
fn font_style_for_request(font_request: &FontRequest) -> skia_safe::FontStyle {
|
||||
skia_safe::FontStyle::new(
|
||||
font_request.weight.map_or(skia_safe::font_style::Weight::NORMAL, |w| w.into()),
|
||||
skia_safe::font_style::Width::NORMAL,
|
||||
if font_request.italic {
|
||||
skia_safe::font_style::Slant::Italic
|
||||
} else {
|
||||
skia_safe::font_style::Slant::Upright
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_layout(
|
||||
font_request: FontRequest,
|
||||
scale_factor: ScaleFactor,
|
||||
|
@ -73,7 +85,7 @@ pub fn create_layout(
|
|||
) -> (skia_safe::textlayout::Paragraph, PhysicalPoint) {
|
||||
let mut text_style = text_style.unwrap_or_default();
|
||||
|
||||
if let Some(family_name) = font_request.family {
|
||||
if let Some(family_name) = font_request.family.as_ref() {
|
||||
text_style.set_font_families(&[family_name.as_str()]);
|
||||
}
|
||||
|
||||
|
@ -83,15 +95,7 @@ pub fn create_layout(
|
|||
text_style.set_letter_spacing((letter_spacing * scale_factor).get());
|
||||
}
|
||||
text_style.set_font_size(pixel_size.get());
|
||||
text_style.set_font_style(skia_safe::FontStyle::new(
|
||||
font_request.weight.map_or(skia_safe::font_style::Weight::NORMAL, |w| w.into()),
|
||||
skia_safe::font_style::Width::NORMAL,
|
||||
if font_request.italic {
|
||||
skia_safe::font_style::Slant::Italic
|
||||
} else {
|
||||
skia_safe::font_style::Slant::Upright
|
||||
},
|
||||
));
|
||||
text_style.set_font_style(font_style_for_request(&font_request));
|
||||
|
||||
let mut style = skia_safe::textlayout::ParagraphStyle::new();
|
||||
|
||||
|
@ -113,7 +117,10 @@ pub fn create_layout(
|
|||
style.set_text_style(&text_style);
|
||||
|
||||
let mut builder = FONT_CACHE.with(|font_cache| {
|
||||
skia_safe::textlayout::ParagraphBuilder::new(&style, font_cache.font_collection.clone())
|
||||
skia_safe::textlayout::ParagraphBuilder::new(
|
||||
&style,
|
||||
font_cache.font_collection.borrow().clone(),
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(selection) = selection {
|
||||
|
@ -166,6 +173,40 @@ pub fn create_layout(
|
|||
(paragraph, PhysicalPoint::from_lengths(Default::default(), layout_top_y))
|
||||
}
|
||||
|
||||
pub fn font_metrics(
|
||||
font_request: i_slint_core::graphics::FontRequest,
|
||||
scale_factor: ScaleFactor,
|
||||
) -> i_slint_core::items::FontMetrics {
|
||||
let (layout, _) = create_layout(
|
||||
font_request,
|
||||
scale_factor,
|
||||
" ",
|
||||
None,
|
||||
None,
|
||||
PhysicalLength::new(f32::MAX),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
let fonts = layout.get_fonts();
|
||||
|
||||
let Some(font_info) = fonts.first() else {
|
||||
return Default::default();
|
||||
};
|
||||
|
||||
let metrics = font_info.font.metrics().1;
|
||||
|
||||
i_slint_core::items::FontMetrics {
|
||||
ascent: -metrics.ascent / scale_factor.get(),
|
||||
descent: -metrics.descent / scale_factor.get(),
|
||||
x_height: metrics.x_height / scale_factor.get(),
|
||||
cap_height: metrics.cap_height / scale_factor.get(),
|
||||
}
|
||||
}
|
||||
|
||||
fn register_font(source: CustomFontSource) -> Result<(), Box<dyn std::error::Error>> {
|
||||
FONT_CACHE.with(|font_cache| {
|
||||
if font_cache
|
||||
|
|
35
tests/cases/text/metrics.slint
Normal file
35
tests/cases/text/metrics.slint
Normal file
|
@ -0,0 +1,35 @@
|
|||
// 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
|
||||
|
||||
export component TestCase inherits Window {
|
||||
width: 100phx;
|
||||
height: 100phx;
|
||||
simple-text := Text { }
|
||||
|
||||
complex-text := Text {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
text-input := TextInput { }
|
||||
|
||||
out property <bool> test: simple-text.font-metrics.ascent == complex-text.font-metrics.ascent && complex-text.font-metrics.ascent == text-input.font-metrics.ascent && text-input.font-metrics.ascent == 7px;
|
||||
}
|
||||
|
||||
/*
|
||||
```rust
|
||||
let instance = TestCase::new().unwrap();
|
||||
assert!(instance.get_test());
|
||||
```
|
||||
|
||||
```cpp
|
||||
auto handle = TestCase::create();
|
||||
const TestCase &instance = *handle;
|
||||
assert(instance.get_test());
|
||||
```
|
||||
|
||||
```js
|
||||
let instance = new slint.TestCase({});
|
||||
assert(instance.test);
|
||||
```
|
||||
|
||||
*/
|
91
tests/manual/font-metrics.slint
Normal file
91
tests/manual/font-metrics.slint
Normal file
|
@ -0,0 +1,91 @@
|
|||
// 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
|
||||
|
||||
import { Slider, Palette } from "std-widgets.slint";
|
||||
|
||||
component MetricsLabel {
|
||||
in property <string> name;
|
||||
in property <length> value;
|
||||
in property <length> baseline;
|
||||
in property <color> color: t.color;
|
||||
out property <length> text-width: t.preferred-width;
|
||||
|
||||
Rectangle {
|
||||
y: root.baseline - root.value;
|
||||
height: 1px;
|
||||
border-color: root.color;
|
||||
border-width: 1px;
|
||||
|
||||
t := Text {
|
||||
x: 0; //parent.width - self.width;
|
||||
y: - self.height;
|
||||
text: {
|
||||
if root.name == "baseline" {
|
||||
return root.name;
|
||||
}
|
||||
"\{root.name} (\{Math.round(root.value / 1px)}px)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import "../../../examples/printerdemo/ui/fonts/NotoSans-Regular.ttf";
|
||||
|
||||
export component AppWindow inherits Window {
|
||||
width: l.x + l.width;
|
||||
height: l.y + 2 * l.font-size;
|
||||
|
||||
l := Text {
|
||||
y: self.font-size / 2;
|
||||
x: max(baseline.text-width, ascent.text-width, descent.text-width, x-height.text-width, cap-height.text-width);
|
||||
text: "Sphinx";
|
||||
font-family: "Noto Sans";
|
||||
font-size: 96px;
|
||||
}
|
||||
|
||||
baseline := MetricsLabel {
|
||||
x: 0;
|
||||
y: l.y;
|
||||
width: 100%;
|
||||
name: "baseline";
|
||||
value: 0;
|
||||
baseline: l.font-metrics.ascent;
|
||||
color: red;
|
||||
}
|
||||
|
||||
ascent := MetricsLabel {
|
||||
x: 0;
|
||||
y: l.y;
|
||||
width: 100%;
|
||||
name: "ascent";
|
||||
value: l.font-metrics.ascent;
|
||||
baseline: l.font-metrics.ascent;
|
||||
}
|
||||
|
||||
descent := MetricsLabel {
|
||||
x: 0;
|
||||
y: l.y;
|
||||
width: 100%;
|
||||
name: "descent";
|
||||
value: l.font-metrics.descent;
|
||||
baseline: l.font-metrics.ascent;
|
||||
}
|
||||
|
||||
x-height := MetricsLabel {
|
||||
x: 0;
|
||||
y: l.y;
|
||||
width: 100%;
|
||||
name: "x-height";
|
||||
value: l.font-metrics.x-height;
|
||||
baseline: l.font-metrics.ascent;
|
||||
}
|
||||
|
||||
cap-height := MetricsLabel {
|
||||
x: 0;
|
||||
y: l.y;
|
||||
width: 100%;
|
||||
name: "cap-height";
|
||||
value: l.font-metrics.cap-height;
|
||||
baseline: l.font-metrics.ascent;
|
||||
}
|
||||
}
|
|
@ -321,9 +321,11 @@ fn insert_property_definitions(
|
|||
}
|
||||
match &element.borrow().base_type {
|
||||
ElementType::Component(c) => binding_value(&c.root_element, prop, &mut 0),
|
||||
ElementType::Builtin(b) => {
|
||||
b.properties.get(prop).and_then(|p| p.default_value.clone()).unwrap_or_default()
|
||||
}
|
||||
ElementType::Builtin(b) => b
|
||||
.properties
|
||||
.get(prop)
|
||||
.and_then(|p| p.default_value.expr(element))
|
||||
.unwrap_or_default(),
|
||||
_ => Expression::Invalid,
|
||||
}
|
||||
}
|
||||
|
@ -383,7 +385,7 @@ pub(super) fn get_properties(
|
|||
ty: t.ty.clone(),
|
||||
declared_at: None,
|
||||
defined_at: None,
|
||||
default_value: t.default_value.clone(),
|
||||
default_value: t.default_value.expr(¤t_element),
|
||||
group: b.name.clone(),
|
||||
group_priority: depth,
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue