servo/components/script/dom/fontface.rs
Uthman Yahaya Baba 8ac9dfcde6 Add Helper method for creating new WebFontDocumentContext
Signed-off-by: Uthman Yahaya Baba <uthmanyahayababa@gmail.com>
2025-05-19 20:59:36 +01:00

581 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use cssparser::{Parser, ParserInput};
use dom_struct::dom_struct;
use fonts::{FontContext, FontContextWebFontMethods, FontTemplate, LowercaseFontFamilyName};
use js::rust::HandleObject;
use style::error_reporting::ParseErrorReporter;
use style::font_face::SourceList;
use style::parser::ParserContext;
use style::stylesheets::{CssRuleType, FontFaceRule, Origin, UrlExtraData};
use style_traits::{ParsingMode, ToCss};
use super::bindings::cell::DomRefCell;
use super::bindings::codegen::UnionTypes::StringOrArrayBufferViewOrArrayBuffer;
use super::bindings::error::{Error, ErrorResult, Fallible};
use super::bindings::refcounted::Trusted;
use super::bindings::reflector::DomGlobal;
use super::bindings::root::MutNullableDom;
use super::types::FontFaceSet;
use crate::dom::bindings::codegen::Bindings::FontFaceBinding::{
FontFaceDescriptors, FontFaceLoadStatus, FontFaceMethods,
};
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::codegen::UnionTypes;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::globalscope::GlobalScope;
use crate::dom::promise::Promise;
use crate::dom::window::Window;
use crate::script_runtime::CanGc;
/// <https://drafts.csswg.org/css-font-loading/#fontface-interface>
#[dom_struct]
pub struct FontFace {
reflector: Reflector,
status: Cell<FontFaceLoadStatus>,
family_name: DomRefCell<DOMString>,
descriptors: DomRefCell<FontFaceDescriptors>,
/// A reference to the [`FontFaceSet`] that this `FontFace` is a member of, if it has been
/// added to one. `None` otherwise. The spec suggests that a `FontFace` can be a member of
/// multiple `FontFaceSet`s, but this doesn't seem to be the case in practice, as the
/// `FontFaceSet` constructor is not exposed on the global scope.
font_face_set: MutNullableDom<FontFaceSet>,
/// This holds the [`FontTemplate`] resulting from loading this `FontFace`, to be used when the
/// `FontFace` is added to the global `FontFaceSet` and thus the `[FontContext]`.
//
// TODO: This could potentially share the `FontTemplateRef` created by `FontContext`, rather
// than having its own copy of the template.
#[no_trace = "Does not contain managed objects"]
template: RefCell<Option<(LowercaseFontFamilyName, FontTemplate)>>,
#[no_trace = "Does not contain managed objects"]
/// <https://drafts.csswg.org/css-font-loading/#m-fontface-urls-slot>
urls: DomRefCell<Option<SourceList>>,
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-fontstatuspromise-slot>
#[ignore_malloc_size_of = "Rc"]
font_status_promise: Rc<Promise>,
}
/// Given the various font face descriptors, construct the equivalent `@font-face` css rule as a
/// string and parse it using `style` crate. Returns `Err(Error::Syntax)` if parsing fails.
///
/// Due to lack of support in the `style` crate, parsing the whole `@font-face` rule is much easier
/// to implement than parsing each declaration on its own.
fn parse_font_face_descriptors(
global: &GlobalScope,
family_name: &DOMString,
sources: Option<&str>,
input_descriptors: &FontFaceDescriptors,
) -> Fallible<FontFaceRule> {
let window = global.as_window(); // TODO: Support calling FontFace APIs from Worker
let quirks_mode = window.Document().quirks_mode();
let url_data = UrlExtraData(window.get_url().get_arc());
let error_reporter = FontFaceErrorReporter {
not_encountered_error: Cell::new(true),
};
let parser_context = ParserContext::new(
Origin::Author,
&url_data,
Some(CssRuleType::FontFace),
ParsingMode::DEFAULT,
quirks_mode,
/* namespaces = */ Default::default(),
Some(&error_reporter as &dyn ParseErrorReporter),
None,
);
let FontFaceDescriptors {
ascentOverride,
descentOverride,
display,
featureSettings,
lineGapOverride,
stretch,
style,
unicodeRange,
variationSettings,
weight,
} = input_descriptors;
let _ = variationSettings; // TODO: Stylo doesn't parse font-variation-settings yet.
let maybe_sources = sources.map_or_else(String::new, |sources| format!("src: {sources};"));
let font_face_rule = format!(
r"
ascent-override: {ascentOverride};
descent-override: {descentOverride};
font-display: {display};
font-family: {family_name};
font-feature-settings: {featureSettings};
font-stretch: {stretch};
font-style: {style};
font-weight: {weight};
line-gap-override: {lineGapOverride};
unicode-range: {unicodeRange};
{maybe_sources}
"
);
// TODO: Should this be the source location in the script that invoked the font face API?
let location = cssparser::SourceLocation { line: 0, column: 0 };
let mut input = ParserInput::new(&font_face_rule);
let mut parser = Parser::new(&mut input);
let mut parsed_font_face_rule =
style::font_face::parse_font_face_block(&parser_context, &mut parser, location);
if let Some(ref mut sources) = parsed_font_face_rule.sources {
let supported_sources: Vec<_> = sources
.0
.iter()
.rev()
.filter(FontContext::is_supported_web_font_source)
.cloned()
.collect();
if supported_sources.is_empty() {
error_reporter.not_encountered_error.set(false);
} else {
sources.0 = supported_sources;
}
}
if error_reporter.not_encountered_error.get() {
Ok(parsed_font_face_rule)
} else {
Err(Error::Syntax)
}
}
fn serialize_parsed_descriptors(font_face_rule: &FontFaceRule) -> FontFaceDescriptors {
FontFaceDescriptors {
ascentOverride: font_face_rule.ascent_override.to_css_string().into(),
descentOverride: font_face_rule.descent_override.to_css_string().into(),
display: font_face_rule.display.to_css_string().into(),
featureSettings: font_face_rule.feature_settings.to_css_string().into(),
lineGapOverride: font_face_rule.line_gap_override.to_css_string().into(),
stretch: font_face_rule.stretch.to_css_string().into(),
style: font_face_rule.style.to_css_string().into(),
unicodeRange: font_face_rule.unicode_range.to_css_string().into(),
variationSettings: font_face_rule.variation_settings.to_css_string().into(),
weight: font_face_rule.weight.to_css_string().into(),
}
}
struct FontFaceErrorReporter {
not_encountered_error: Cell<bool>,
}
impl ParseErrorReporter for FontFaceErrorReporter {
fn report_error(
&self,
_url: &UrlExtraData,
_location: cssparser::SourceLocation,
_error: style::error_reporting::ContextualParseError,
) {
self.not_encountered_error.set(false);
}
}
impl FontFace {
/// Construct a [`FontFace`] to be used in the case of failure in parsing the
/// font face descriptors.
fn new_failed_font_face(global: &GlobalScope, can_gc: CanGc) -> Self {
let font_status_promise = Promise::new(global, can_gc);
// If any of them fail to parse correctly, reject font faces [[FontStatusPromise]] with a
// DOMException named "SyntaxError"
font_status_promise.reject_error(Error::Syntax, can_gc);
// set font faces corresponding attributes to the empty string, and set font faces status
// attribute to "error"
Self {
reflector: Reflector::new(),
font_face_set: MutNullableDom::default(),
font_status_promise,
family_name: DomRefCell::default(),
urls: DomRefCell::default(),
descriptors: DomRefCell::new(FontFaceDescriptors {
ascentOverride: DOMString::new(),
descentOverride: DOMString::new(),
display: DOMString::new(),
featureSettings: DOMString::new(),
lineGapOverride: DOMString::new(),
stretch: DOMString::new(),
style: DOMString::new(),
unicodeRange: DOMString::new(),
variationSettings: DOMString::new(),
weight: DOMString::new(),
}),
status: Cell::new(FontFaceLoadStatus::Error),
template: RefCell::default(),
}
}
/// <https://drafts.csswg.org/css-font-loading/#font-face-constructor>
fn new_inherited(
global: &GlobalScope,
family_name: DOMString,
source: StringOrArrayBufferViewOrArrayBuffer,
descriptors: &FontFaceDescriptors,
can_gc: CanGc,
) -> Self {
// TODO: Add support for ArrayBuffer and ArrayBufferView sources.
let StringOrArrayBufferViewOrArrayBuffer::String(ref source_string) = source else {
return Self::new_failed_font_face(global, can_gc);
};
// Step 1. Parse the family argument, and the members of the descriptors argument,
// according to the grammars of the corresponding descriptors of the CSS @font-face rule If
// the source argument is a CSSOMString, parse it according to the grammar of the CSS src
// descriptor of the @font-face rule.
let parse_result =
parse_font_face_descriptors(global, &family_name, Some(source_string), descriptors);
let Ok(ref parsed_font_face_rule) = parse_result else {
// If any of them fail to parse correctly, reject font faces
// [[FontStatusPromise]] with a DOMException named "SyntaxError", set font faces
// corresponding attributes to the empty string, and set font faces status attribute
// to "error".
return Self::new_failed_font_face(global, can_gc);
};
// Set its internal [[FontStatusPromise]] slot to a fresh pending Promise object.
let font_status_promise = Promise::new(global, can_gc);
let sources = parsed_font_face_rule
.sources
.clone()
.expect("Sources should be non-None after validation");
// Let font face be a fresh FontFace object.
Self {
reflector: Reflector::new(),
// Set font faces status attribute to "unloaded".
status: Cell::new(FontFaceLoadStatus::Unloaded),
// Set font faces corresponding attributes to the serialization of the parsed values.
descriptors: DomRefCell::new(serialize_parsed_descriptors(parsed_font_face_rule)),
font_face_set: MutNullableDom::default(),
family_name: DomRefCell::new(family_name.clone()),
urls: DomRefCell::new(Some(sources)),
template: RefCell::default(),
font_status_promise,
}
}
pub(crate) fn new(
global: &GlobalScope,
proto: Option<HandleObject>,
font_family: DOMString,
source: StringOrArrayBufferViewOrArrayBuffer,
descriptors: &FontFaceDescriptors,
can_gc: CanGc,
) -> DomRoot<Self> {
reflect_dom_object_with_proto(
Box::new(Self::new_inherited(
global,
font_family,
source,
descriptors,
can_gc,
)),
global,
proto,
can_gc,
)
}
pub(super) fn set_associated_font_face_set(&self, font_face_set: &FontFaceSet) {
self.font_face_set.set(Some(font_face_set));
}
pub(super) fn loaded(&self) -> bool {
self.status.get() == FontFaceLoadStatus::Loaded
}
pub(super) fn template(&self) -> Option<(LowercaseFontFamilyName, FontTemplate)> {
self.template.borrow().clone()
}
/// Implements the body of the setter for the descriptor attributes of the [`FontFace`] interface.
///
/// <https://drafts.csswg.org/css-font-loading/#fontface-interface>:
/// On setting, parse the string according to the grammar for the corresponding @font-face
/// descriptor. If it does not match the grammar, throw a SyntaxError; otherwise, set the attribute
/// to the serialization of the parsed value.
fn validate_and_set_descriptors(&self, new_descriptors: FontFaceDescriptors) -> ErrorResult {
let global = self.global();
let parsed_font_face_rule = parse_font_face_descriptors(
&global,
&self.family_name.borrow(),
None,
&new_descriptors,
)?;
*self.descriptors.borrow_mut() = serialize_parsed_descriptors(&parsed_font_face_rule);
Ok(())
}
}
impl FontFaceMethods<crate::DomTypeHolder> for FontFace {
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-family>
fn Family(&self) -> DOMString {
self.family_name.borrow().clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-family>
fn SetFamily(&self, family_name: DOMString) -> ErrorResult {
let descriptors = self.descriptors.borrow();
let global = self.global();
let _ = parse_font_face_descriptors(&global, &family_name, None, &descriptors)?;
*self.family_name.borrow_mut() = family_name;
Ok(())
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-style>
fn Style(&self) -> DOMString {
self.descriptors.borrow().style.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-style>
fn SetStyle(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.style = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-weight>
fn Weight(&self) -> DOMString {
self.descriptors.borrow().weight.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-weight>
fn SetWeight(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.weight = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-stretch>
fn Stretch(&self) -> DOMString {
self.descriptors.borrow().stretch.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-stretch>
fn SetStretch(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.stretch = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-unicoderange>
fn UnicodeRange(&self) -> DOMString {
self.descriptors.borrow().unicodeRange.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-unicoderange>
fn SetUnicodeRange(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.unicodeRange = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-featuresettings>
fn FeatureSettings(&self) -> DOMString {
self.descriptors.borrow().featureSettings.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-featuresettings>
fn SetFeatureSettings(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.featureSettings = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-variationsettings>
fn VariationSettings(&self) -> DOMString {
self.descriptors.borrow().variationSettings.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-variationsettings>
fn SetVariationSettings(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.variationSettings = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-display>
fn Display(&self) -> DOMString {
self.descriptors.borrow().display.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-display>
fn SetDisplay(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.display = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-ascentoverride>
fn AscentOverride(&self) -> DOMString {
self.descriptors.borrow().ascentOverride.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-ascentoverride>
fn SetAscentOverride(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.ascentOverride = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-descentoverride>
fn DescentOverride(&self) -> DOMString {
self.descriptors.borrow().descentOverride.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-descentoverride>
fn SetDescentOverride(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.descentOverride = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-linegapoverride>
fn LineGapOverride(&self) -> DOMString {
self.descriptors.borrow().lineGapOverride.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-linegapoverride>
fn SetLineGapOverride(&self, value: DOMString) -> ErrorResult {
let mut new_descriptors = self.descriptors.borrow().clone();
new_descriptors.lineGapOverride = value;
self.validate_and_set_descriptors(new_descriptors)
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-status>
fn Status(&self) -> FontFaceLoadStatus {
self.status.get()
}
/// The load() method of FontFace forces a url-based font face to request its font data and
/// load. For fonts constructed from a buffer source, or fonts that are already loading or
/// loaded, it does nothing.
/// <https://drafts.csswg.org/css-font-loading/#font-face-load>
fn Load(&self) -> Rc<Promise> {
let Some(sources) = self.urls.borrow_mut().take() else {
// Step 2. If font faces [[Urls]] slot is null, or its status attribute is anything
// other than "unloaded", return font faces [[FontStatusPromise]] and abort these
// steps.
return self.font_status_promise.clone();
};
// FontFace must not be loaded at this point as `self.urls` is not None, implying `Load`
// wasn't called already. In our implementation, `urls` is set after parsing, so it
// cannot be `Some` if the status is `Error`.
debug_assert_eq!(self.status.get(), FontFaceLoadStatus::Unloaded);
let global = self.global();
let trusted = Trusted::new(self);
let task_source = global
.task_manager()
.font_loading_task_source()
.to_sendable();
let finished_callback = Box::new(
move |family_name: LowercaseFontFamilyName, load_result: Option<_>| {
let trusted = trusted.clone();
// Step 5. When the load operation completes, successfully or not, queue a task to
// run the following steps synchronously:
task_source.queue(task!(resolve_font_face_load_task: move || {
let font_face = trusted.root();
match load_result {
None => {
// Step 5.1. If the attempt to load fails, reject font faces
// [[FontStatusPromise]] with a DOMException whose name is "NetworkError"
// and set font faces status attribute to "error".
font_face.status.set(FontFaceLoadStatus::Error);
font_face.font_status_promise.reject_error(Error::Network, CanGc::note());
}
Some(template) => {
// Step 5.2. Otherwise, font face now represents the loaded font;
// fulfill font faces [[FontStatusPromise]] with font face and set
// font faces status attribute to "loaded".
font_face.status.set(FontFaceLoadStatus::Loaded);
let old_template = font_face.template.borrow_mut().replace((family_name, template));
debug_assert!(old_template.is_none(), "FontFace's template must be intialized only once");
font_face.font_status_promise.resolve_native(&font_face, CanGc::note());
}
}
if let Some(font_face_set) = font_face.font_face_set.get() {
// For each FontFaceSet font face is in: ...
//
// This implements steps 5.1.1, 5.1.2, 5.2.1 and 5.2.2 - these
// take care of changing the status of the `FontFaceSet` in which this
// `FontFace` is a member, for both failed and successful load.
font_face_set.handle_font_face_status_changed(&font_face);
}
}));
},
);
// We parse the descriptors again because they are stored as `DOMString`s in this `FontFace`
// but the `load_web_font_for_script` API needs parsed values.
let parsed_font_face_rule = parse_font_face_descriptors(
&global,
&self.family_name.borrow(),
None,
&self.descriptors.borrow(),
)
.expect("Parsing shouldn't fail as descriptors are valid by construction");
// Construct a WebFontDocumentContext object for the current document.
let document_context = global.as_window().new_document_context();
// Step 4. Using the value of font faces [[Urls]] slot, attempt to load a font as defined
// in [CSS-FONTS-3], as if it was the value of a @font-face rules src descriptor.
// TODO: FontFaceSet is not supported on Workers yet. The `as_window` call below should be
// replaced when we do support it.
global.as_window().font_context().load_web_font_for_script(
global.webview_id(),
sources,
(&parsed_font_face_rule).into(),
finished_callback,
&document_context,
);
// Step 3. Set font faces status attribute to "loading", return font faces
// [[FontStatusPromise]], and continue executing the rest of this algorithm asynchronously.
self.status.set(FontFaceLoadStatus::Loading);
self.font_status_promise.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#dom-fontface-loaded>
fn Loaded(&self) -> Rc<Promise> {
self.font_status_promise.clone()
}
/// <https://drafts.csswg.org/css-font-loading/#font-face-constructor>
fn Constructor(
window: &Window,
proto: Option<HandleObject>,
can_gc: CanGc,
family: DOMString,
source: UnionTypes::StringOrArrayBufferViewOrArrayBuffer,
descriptors: &FontFaceDescriptors,
) -> DomRoot<FontFace> {
let global = window.as_global_scope();
FontFace::new(global, proto, family, source, descriptors, can_gc)
}
}