Text rendering quality improvements. (#3855)

This commit is contained in:
Wojciech Daniło 2022-11-08 19:15:05 +01:00 committed by GitHub
parent 45276b243d
commit cee7f27dc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 242 additions and 108 deletions

View File

@ -89,6 +89,9 @@
instances are now reusing the shape shaders and the same sprite system under
the hood. This drastically reduces the amount of required draw calls for
scenes with a lot of text.
- [Text rendering quality improvements][3855]. Glyphs are now hinted in a better
way. Also, additional fine-tuning is performed per font and per host operating
system.
#### Enso Standard Library
@ -369,6 +372,7 @@
[3804]: https://github.com/enso-org/enso/pull/3804
[3818]: https://github.com/enso-org/enso/pull/3818
[3776]: https://github.com/enso-org/enso/pull/3776
[3855]: https://github.com/enso-org/enso/pull/3855
[3836]: https://github.com/enso-org/enso/pull/3836
[3782]: https://github.com/enso-org/enso/pull/3782

7
Cargo.lock generated
View File

@ -2018,6 +2018,7 @@ dependencies = [
"nalgebra 0.26.2",
"percent-encoding 2.1.0",
"unicode-segmentation",
"unidecode",
"wasm-bindgen",
"web-sys",
]
@ -7128,6 +7129,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "unidecode"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc"
[[package]]
name = "unreachable"
version = "1.0.0"

View File

@ -1321,6 +1321,10 @@ impl TextModel {
if truncated {
break;
}
// FIXME[WD]: This is a workaround for a bug in the MSDFgen binding. It
// should be fixed after updating the MSDFgen library.
// See: https://www.pivotaltracker.com/n/projects/2539304/stories/183747513
let magic_scale = 2048.0 / shaped_glyph_set.units_per_em as f32;
for shaped_glyph in &shaped_glyph_set.glyphs {
let glyph_byte_start = shaped_glyph.start_byte();
// Drop styles assigned to skipped bytes. One byte will be skipped
@ -1361,11 +1365,11 @@ impl TextModel {
glyph.set_color(style.color);
glyph.skip_color_animation();
glyph.set_sdf_weight(style.sdf_weight.value);
glyph.set_size(style.size);
glyph.set_size(formatting::Size(style.size.value * magic_scale));
glyph.set_properties(shaped_glyph_set.non_variable_variations);
glyph.set_glyph_id(shaped_glyph.id());
glyph.x_advance.set(x_advance);
glyph.view.set_position_xy(glyph_render_offset);
glyph.view.set_position_xy(glyph_render_offset * magic_scale);
glyph.set_position_xy(Vector2(glyph_offset_x, 0.0));
glyph_offset_x += x_advance;

View File

@ -67,7 +67,7 @@ pub use owned_ttf_parser::Width;
/// eliminate accidental mistakes, the same way as it's done in CSS:
/// https://stackoverflow.com/questions/17967371/are-property-values-in-css-case-sensitive
#[allow(missing_docs)]
#[derive(Clone, Debug, Display, Hash, PartialEq, Eq)]
#[derive(Clone, Debug, Deref, Display, Hash, PartialEq, Eq)]
pub struct Name {
pub normalized: String,
}

View File

@ -1,51 +1,47 @@
const bool DEBUG = false;
highp float median(highp vec3 v) {
return max(min(v.x, v.y), min(max(v.x, v.y), v.z));
}
/// Compute the uv coordinates of the MSDF texture fragment where it should be sampled.
///
/// Essentially, it's an input_uv which is a bit transformed to "cut off" the half of the MSDF cell from each side. This
/// way we have better pixel alignment on low resolutions.
highp vec2 msdf_fragment_uv() {
highp vec2 msdf_cell_size = 1.0/input_msdf_size;
highp vec2 offset = msdf_cell_size/2.0;
highp vec2 scale = 1.0 - msdf_cell_size;
return offset + input_uv * scale;
}
highp vec2 get_texture_coord() {
highp vec2 msdf_fragment_size = input_msdf_size / vec2(textureSize(input_atlas, 0));
highp vec2 offset = vec2(0.0, input_atlas_index) * msdf_fragment_size;
return offset + msdf_fragment_uv() * msdf_fragment_size;
highp vec2 uv_to_texture_coord(vec2 uv) {
highp vec2 texture_glyph_offset = input_msdf_size / vec2(textureSize(input_atlas, 0));
highp vec2 offset = vec2(0.0, input_atlas_index) * texture_glyph_offset;
return offset + uv * texture_glyph_offset;
}
highp float get_fatting() {
highp vec2 local_to_px_ratio = 1.0 / fwidth(input_local.xy);
highp float font_size_px = input_font_size * (local_to_px_ratio.x + local_to_px_ratio.y) / 2.0;
highp float fatting = input_sdf_weight;
highp vec2 local_to_px_ratio = 1.0 / fwidth(input_local.xy);
highp float font_size_px = input_font_size * (local_to_px_ratio.x + local_to_px_ratio.y) / 2.0;
highp float fatting = input_sdf_weight;
return font_size_px * fatting;
}
highp float msdf_alpha() {
highp vec2 tex_coord = get_texture_coord();
highp vec2 msdf_unit_tex = input_msdf_range / vec2(textureSize(input_atlas,0));
highp vec2 msdf_unit_px = msdf_unit_tex / fwidth(tex_coord);
highp float get_alpha(vec2 uv) {
highp vec2 tex_coord = uv_to_texture_coord(uv);
highp vec2 msdf_unit_tex = input_msdf_range / vec2(textureSize(input_atlas, 0));
highp vec2 msdf_unit_px = msdf_unit_tex / fwidth(tex_coord);
highp float avg_msdf_unit_px = (msdf_unit_px.x + msdf_unit_px.y) / 2.0;
// We use this parameter to fatten somewhat font on low resolutions. The thershold and exact
// value of this fattening was picked by trial an error, searching for best rendering effect.
highp float dpi_dilate = avg_msdf_unit_px < input_msdf_range*0.49 ? 1.0 : 0.0;
highp vec3 msdf_sample = texture(input_atlas,tex_coord).rgb;
highp float sig_dist = median(msdf_sample) - 0.5;
highp float sig_dist = median(msdf_sample) - 0.5;
highp float sig_dist_px = sig_dist * avg_msdf_unit_px + get_fatting();
highp float opacity = 0.5 + sig_dist_px + dpi_dilate * 0.08;
highp float opacity = 0.5 + sig_dist_px;
opacity += input_opacity_increase;
opacity = clamp(opacity, 0.0, 1.0);
opacity = pow(opacity, input_opacity_exponent);
return opacity;
}
highp vec4 color_from_msdf() {
highp vec4 color = input_color;
color.a *= msdf_alpha();
color.a *= get_alpha(input_uv);
color.rgb *= color.a; // premultiply
if(DEBUG) {
vec4 bg_box = vec4(input_uv * input_size / 10.0, 0.0, 1.0);
color = (color * 0.7 + bg_box * 0.3);
}
return color;
}

View File

@ -1,59 +0,0 @@
highp float median(highp vec3 v) {
return max(min(v.x, v.y), min(max(v.x, v.y), v.z));
}
/// Compute the uv coordinates of the MSDF texture fragment where it should be sampled.
///
/// Essentially, it's an input_uv which is a bit transformed to "cut off" the half of the MSDF cell from each side. This
/// way we have better pixel alignment on low resolutions.
highp vec2 msdf_fragment_uv() {
highp vec2 msdf_cell_size = 1.0/input_msdf_size;
highp vec2 offset = msdf_cell_size/2.0;
highp vec2 scale = 1.0 - msdf_cell_size;
return offset + input_uv * scale;
}
highp vec2 get_texture_coord() {
highp vec2 msdf_fragment_size = input_msdf_size / vec2(textureSize(input_atlas,0));
highp vec2 offset = vec2(0.0, input_atlas_index) * msdf_fragment_size;
return offset + get_scaled_uv() * msdf_fragment_size;
}
highp float get_fatting() {
highp vec2 local_to_px_ratio = 1.0 / fwidth(input_local.xy);
highp float font_size_px = input_font_size * (local_to_px_ratio.x + local_to_px_ratio.y) / 2.0;
highp float fatting = input_sdf_weight;
return font_size_px * fatting;
}
// FIXME
// The following function uses non-standard font adjustiments (lines marked with FIXME). They make
// the font bolder and more crisp. It was designed to look nice on nodes in the GUI but leaves the
// fonts with a non-standard look (not the one defined by the font author). This should be
// revisited, generalized, and refactored out in the future.
highp float msdf_alpha() {
highp vec2 tex_coord = get_texture_coord();
highp vec2 msdf_unit_tex = input_msdf_range / vec2(textureSize(input_atlas,0));
highp vec2 msdf_unit_px = msdf_unit_tex / fwidth(tex_coord);
highp float avg_msdf_unit_px = (msdf_unit_px.x + msdf_unit_px.y) / 2.0;
// We use this parameter to fatten somewhat font on low resolutions. The thershold and exact
// value of this fattening was picked by trial an error, searching for best rendering effect.
highp float dpi_dilate = avg_msdf_unit_px < input_msdf_range*0.49 ? 1.0 : 0.0;
highp vec3 msdf_sample = texture(input_atlas,tex_coord).rgb;
highp float sig_dist = median(msdf_sample) - 0.5;
highp float sig_dist_px = sig_dist * avg_msdf_unit_px + get_fatting();
highp float opacity = 0.5 + sig_dist_px + dpi_dilate * 0.08;
opacity += 0.6; // FIXME: Widen + sharpen
opacity = clamp(opacity, 0.0, 1.0);
opacity = pow(opacity,3.0); // FIXME: sharpen
return opacity;
}
highp vec4 color_from_msdf() {
highp vec4 color = input_color;
color.a *= msdf_alpha();
color.rgb *= color.a; // premultiply
return color;
}

View File

@ -21,15 +21,61 @@ use ensogl_core::display::scene::Scene;
use ensogl_core::display::symbol::material::Material;
use ensogl_core::display::symbol::shader::builder::CodeTemplate;
use ensogl_core::frp;
use ensogl_core::frp::io::keyboard::Key;
use ensogl_core::system::gpu::texture;
#[cfg(target_arch = "wasm32")]
use ensogl_core::system::gpu::Texture;
use ensogl_core::system::web::platform;
use font::FontWithAtlas;
use font::GlyphRenderInfo;
use font::Style;
use font::Weight;
use font::Width;
use owned_ttf_parser::GlyphId;
use std::sync::LazyLock;
// ===============
// === Hinting ===
// ===============
/// System- and font-specific hinting properties. They affect the way the font is rasterized. In
/// order to understand how these variables affect the font rendering, see the GLSL file (the
/// [`FUNCTIONS`] variable).
///
/// Also, you can interactively change the values by holding `ctrl + alt + o` or `ctrl + alt + e`
/// keys and using the `+` and `-` key to increment or decrement the value.
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug)]
pub struct Hinting {
pub opacity_increase: f32,
pub opacity_exponent: f32,
}
impl Default for Hinting {
fn default() -> Self {
Self { opacity_increase: 0.0, opacity_exponent: 1.0 }
}
}
static HINTING_MAP: LazyLock<HashMap<(Option<platform::Platform>, &'static str), Hinting>> =
LazyLock::new(|| {
HashMap::from([
((Some(platform::Platform::MacOS), "mplus1p"), Hinting {
opacity_increase: 0.4,
opacity_exponent: 4.0,
}),
((Some(platform::Platform::Windows), "mplus1p"), Hinting {
opacity_increase: 0.3,
opacity_exponent: 3.0,
}),
((Some(platform::Platform::Linux), "mplus1p"), Hinting {
opacity_increase: 0.3,
opacity_exponent: 3.0,
}),
])
});
@ -59,12 +105,7 @@ ensogl_core::define_endpoints_2! {
#[allow(missing_docs)]
pub struct SystemData {}
#[cfg(target_os = "macos")]
const FUNCTIONS: &str = include_str!("glsl/glyph_mac.glsl");
#[cfg(not(target_os = "macos"))]
const FUNCTIONS: &str = include_str!("glsl/glyph.glsl");
const MAIN: &str = "output_color = color_from_msdf(); output_id=vec4(0.0,0.0,0.0,0.0);";
impl SystemData {
@ -80,6 +121,9 @@ impl SystemData {
material.add_input("font_size", 10.0);
material.add_input("color", Vector4::new(0.0, 0.0, 0.0, 1.0));
material.add_input("sdf_weight", 0.0);
// === Adjusting look and feel of different fonts on different operating systems ===
material.add_input("opacity_increase", 0.0);
material.add_input("opacity_exponent", 1.0);
// TODO[WD]: We need to use this output, as we need to declare the same amount of shader
// outputs as the number of attachments to framebuffer. We should manage this more
// intelligent. For example, we could allow defining output shader fragments,
@ -118,7 +162,15 @@ mod glyph_shape {
ensogl_core::shape! {
type SystemData = SystemData;
type ShapeData = ShapeData;
(style: Style, font_size: f32, color: Vector4<f32>, sdf_weight: f32, atlas_index: f32) {
(
style: Style,
font_size: f32,
color: Vector4<f32>,
sdf_weight: f32,
atlas_index: f32,
opacity_increase: f32,
opacity_exponent: f32
) {
// The shape does not matter. The [`SystemData`] defines custom GLSL code.
Plane().into()
}
@ -132,7 +184,6 @@ impl ensogl_core::display::shape::CustomSystemData<glyph_shape::Shape> for Syste
shape_data: &ShapeData,
) -> Self {
let font = &shape_data.font;
let size = font::msdf::Texture::size();
let sprite_system = &data.model.sprite_system;
let symbol = sprite_system.symbol();
@ -522,17 +573,72 @@ impl System {
let color_animation = color::Animation::new(frp.network());
let x_advance = default();
let attached_to_cursor = default();
let platform = platform::current();
let hinting = HINTING_MAP.get(&(platform, font.name())).copied().unwrap_or_default();
let view = glyph_shape::View::new_with_data(ShapeData { font });
view.color.set(Vector4::new(0.0, 0.0, 0.0, 0.0));
view.atlas_index.set(0.0);
view.opacity_increase.set(hinting.opacity_increase);
view.opacity_exponent.set(hinting.opacity_exponent);
display_object.add_child(&view);
let network = frp.network();
frp::extend! {network
let scene = scene();
let keyboard = &scene.keyboard.frp;
frp::extend! { network
frp.private.output.target_color <+ frp.set_color;
color_animation.target <+ frp.set_color;
color_animation.skip <+ frp.skip_color_animation;
eval color_animation.value ((c) view.color.set(Rgba::from(c).into()));
// === Debug mode ===
// Allows changing hinting parameters on the fly. See [`Hinting`] to learn more.
debug_mode <- all_with(&keyboard.is_control_down, &keyboard.is_alt_down, |a, b| *a && *b);
plus <- keyboard.down.map(|t| t == &Key::Character("=".into()));
minus <- keyboard.down.map(|t| t == &Key::Character("-".into()));
key_e_down <- keyboard.down.map(|t| t == &Key::Character("e".into())).on_true();
key_e_up <- keyboard.up.map(|t| t == &Key::Character("e".into())).on_true();
key_e <- bool(&key_e_up, &key_e_down);
key_o_down <- keyboard.down.map(|t| t == &Key::Character("o".into())).on_true();
key_o_up <- keyboard.up.map(|t| t == &Key::Character("o".into())).on_true();
key_o <- bool(&key_o_up, &key_o_down);
plus2 <- all_with(&plus, &debug_mode, |a, b| *a && *b);
minus2 <- all_with(&minus, &debug_mode, |a, b| *a && *b);
plus_e <- keyboard.down.gate(&plus2).gate(&key_e);
minus_e <- keyboard.down.gate(&minus2).gate(&key_e);
plus_o <- keyboard.down.gate(&plus2).gate(&key_o);
minus_o <- keyboard.down.gate(&minus2).gate(&key_o);
eval_ plus_o (view.opacity_increase.modify(|t| {
let opacity_increase = t + 0.01;
warn!("opacity_increase: {opacity_increase}");
opacity_increase
}));
eval_ minus_o (view.opacity_increase.modify(|t| {
let opacity_increase = t - 0.01;
warn!("opacity_increase: {opacity_increase}");
opacity_increase
}));
eval_ plus_e (view.opacity_exponent.modify(|t| {
let opacity_exponent = t + 0.1;
warn!("opacity_exponent: {opacity_exponent}");
opacity_exponent
}));
eval_ minus_e (view.opacity_exponent.modify(|t| {
let opacity_exponent = t - 0.1;
warn!("opacity_exponent: {opacity_exponent}");
opacity_exponent
}));
}
Glyph {

View File

@ -9,7 +9,7 @@ use ide_ci::log::setup_logging;
pub const PACKAGE: GithubRelease<&str> = GithubRelease {
project_url: "https://github.com/enso-org/msdfgen-wasm",
version: "v1.4",
version: "v1.4.1",
filename: "msdfgen_wasm.js",
};

View File

@ -16,6 +16,7 @@
#![feature(let_chains)]
#![feature(step_trait)]
#![feature(specialization)]
#![feature(once_cell)]
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]

View File

@ -17,6 +17,7 @@ pub mod hysteretic;
pub mod overshoot;
// =================
// === Animation ===
// =================

View File

@ -29,6 +29,9 @@ use ensogl_core::application::command::FrpNetworkProvider;
use ensogl_core::application::Application;
use ensogl_core::data::color;
use ensogl_core::display::navigation::navigator::Navigator;
use ensogl_core::display::Scene;
use ensogl_core::frp::io::timer::DocumentOps;
use ensogl_core::frp::io::timer::HtmlElementOps;
use ensogl_core::system::web;
use ensogl_core::system::web::Closure;
use ensogl_core::system::web::JsCast;
@ -169,8 +172,12 @@ fn init(app: Application) {
let zalgo = "Z̮̞̠͙͔ͅḀ̗̞͈̻̗Ḷ͙͎̯̹̞͓G̻O̭̗̮";
let _text = quote.to_string() + snowman + zalgo;
let _text = "test".to_string();
area.set_content("aஓbc🧑🏾de\nfghij\nklmno\npqrst\n01234\n56789");
area.set_property_default(color::Rgba::red());
let content = "abcdefghijk";
// This is a testing string left here for convenience.
// area.set_content("aஓbc🧑🏾de\nfghij\nklmno\npqrst\n01234\n56789");
area.set_content(content);
area.set_font("mplus1p");
area.set_property_default(color::Rgba::black());
area.focus();
area.hover();
@ -182,15 +189,45 @@ fn init(app: Application) {
app.display.default_scene.add_child(&area);
let area = Rc::new(RefCell::new(Some(area)));
init_debug_hotkeys(&area);
// Initialization of HTML div displaying the same text. It allows for switching between
// WebGL and HTML versions to compare them.
let style = web::document.create_element_or_panic("style");
let css = web::document.create_text_node("@import url('https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@400;700&display=swap');");
style.append_child(&css).unwrap();
web::document.head().unwrap().append_child(&style).unwrap();
let div = web::document.create_div_or_panic();
div.set_style_or_warn("width", "100px");
div.set_style_or_warn("height", "100px");
div.set_style_or_warn("position", "absolute");
div.set_style_or_warn("z-index", "100");
div.set_style_or_warn("font-family", "'M PLUS 1p'");
div.set_style_or_warn("font-size", "12px");
div.set_style_or_warn("display", "none");
div.set_inner_text(content);
web::document.body().unwrap().append_child(&div).unwrap();
init_debug_hotkeys(&app.display.default_scene, &area, &div);
let scene = scene.clone_ref();
let handler = app.display.on.before_frame.add(move |_time| {
let shape = scene.dom.shape();
div.set_style_or_warn("left", &format!("{}px", shape.width / 2.0));
div.set_style_or_warn("top", &format!("{}px", shape.height / 2.0 - 0.5));
});
mem::forget(handler);
mem::forget(navigator);
mem::forget(app);
}
fn init_debug_hotkeys(area: &Rc<RefCell<Option<Text>>>) {
fn init_debug_hotkeys(scene: &Scene, area: &Rc<RefCell<Option<Text>>>, div: &web::HtmlDivElement) {
let html_version = Rc::new(Cell::new(false));
let scene = scene.clone_ref();
let area = area.clone_ref();
let closure: Closure<dyn Fn(JsValue)> = Closure::new(move |val: JsValue| {
let div = div.clone();
let mut fonts_cycle = ["dejavusans", "dejavusansmono", "mplus1p"].iter().cycle();
let closure: Closure<dyn FnMut(JsValue)> = Closure::new(move |val: JsValue| {
let event = val.unchecked_into::<web::KeyboardEvent>();
if event.ctrl_key() {
let key = event.code();
@ -200,10 +237,22 @@ fn init_debug_hotkeys(area: &Rc<RefCell<Option<Text>>>) {
}
}
if let Some(area) = &*area.borrow() {
div.set_inner_text(&area.content.value().to_string());
if event.ctrl_key() {
let key = event.code();
warn!("{:?}", key);
if key == "Digit1" {
if key == "KeyH" {
html_version.set(!html_version.get());
if html_version.get() {
warn!("Showing the HTML version.");
area.unset_parent();
div.set_style_or_warn("display", "block");
} else {
warn!("Showing the WebGL version.");
scene.add_child(&area);
div.set_style_or_warn("display", "none");
}
} else if key == "Digit1" {
if event.shift_key() {
area.set_property_default(color::Rgba::black());
} else {
@ -243,7 +292,7 @@ fn init_debug_hotkeys(area: &Rc<RefCell<Option<Text>>>) {
} else {
area.set_property(buffer::RangeLike::Selections, formatting::Weight::Bold);
}
} else if key == "KeyH" {
} else if key == "KeyN" {
if event.shift_key() {
area.set_property_default(formatting::SdfWeight(0.02));
} else {
@ -258,6 +307,10 @@ fn init_debug_hotkeys(area: &Rc<RefCell<Option<Text>>>) {
} else {
area.set_property(buffer::RangeLike::Selections, formatting::Style::Italic);
}
} else if key == "KeyF" {
let font = fonts_cycle.next().unwrap();
warn!("Switching to font '{}'.", font);
area.set_font(font);
} else if key == "Equal" {
if event.shift_key() {
area.set_property_default(formatting::Size(16.0));

View File

@ -18,6 +18,7 @@ keyboard-types = { version = "0.5.0" }
nalgebra = { version = "0.26.1" }
percent-encoding = { version = "2.1.0" }
unicode-segmentation = { version = "1.6.0" }
unidecode = { version = "0.3.0" }
# We require exact version of wasm-bindgen because we do patching final js in our build process,
# and this is vulnerable to any wasm-bindgen version change.
wasm-bindgen = { version = "0.2.78", features = ["nightly"] }

View File

@ -9,6 +9,7 @@ use crate::io::js::Listener;
use enso_web::KeyboardEvent;
use inflector::Inflector;
use unicode_segmentation::UnicodeSegmentation;
use unidecode::unidecode;
@ -122,7 +123,7 @@ impl Key {
if key == " " {
Self::Space
} else if key.graphemes(true).count() == 1 {
Self::Character(key)
Self::Character(unidecode(&key).to_lowercase())
} else {
let key = KEY_NAME_MAP.get(key_ref).cloned().unwrap_or(Self::Other(key));
match (key, code) {

View File

@ -33,6 +33,7 @@ features = [
'Element',
'HtmlElement',
'HtmlDivElement',
'HtmlHeadElement',
'HtmlCollection',
'CssStyleDeclaration',
'HtmlCanvasElement',
@ -49,6 +50,7 @@ features = [
'Event',
'MouseEvent',
'EventTarget',
'Text',
'DomRect',
'DomRectReadOnly',
'Location',

View File

@ -464,6 +464,8 @@ mock_data! { Document => EventTarget
fn body(&self) -> Option<HtmlElement>;
fn create_element(&self, local_name: &str) -> Result<Element, JsValue>;
fn get_element_by_id(&self, element_id: &str) -> Option<Element>;
fn create_text_node(&self, data: &str) -> Text;
fn head(&self) -> Option<HtmlHeadElement>;
}
@ -484,6 +486,11 @@ mock_data! { Window => EventTarget
}
// === HtmlHeadElement ===
mock_data! { HtmlHeadElement => HtmlElement
}
// === Function ===
mock_data! { Function
fn call1(&self, context: &JsValue, arg1: &JsValue) -> Result<JsValue, JsValue>;
@ -599,6 +606,16 @@ impl From<HtmlDivElement> for EventTarget {
}
// === HtmlDivElement ===
mock_data! { Text => CharacterData }
// === CharacterData ===
mock_data! { CharacterData => Node }
// === HtmlCanvasElement ===
mock_data! { HtmlCanvasElement => HtmlElement
fn width(&self) -> u32;

View File

@ -9,7 +9,7 @@ use std::convert::TryFrom;
// ================
/// This enumeration lists all the supported platforms.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum Platform {
Android,