fix(text): render CJK glyphs at non-regular weights on macOS

Vendor femtovg 0.23.2 with a targeted patch: the swash raster path can
fail to scale some variable fonts at a non-default variation instance
(the CFF2-based CJK system fonts on modern macOS), and femtovg cached
those glyphs as permanently missing — so track titles and any other
text at medium/semibold/bold weight rendered blank for CJK while
regular weight (empty coords) worked. render_glyph now retries the
glyph at the font's default instance before giving up: worst case the
glyph draws at regular weight instead of not at all. Fonts and
instances that already render are untouched.

Fixes #543

cc
This commit is contained in:
vicrodh committed 2026-07-05 08:23:17 -06:00
1 parent cf16fa263c
commit 3a1c367d1a
38 files changed
+18170 -2

No files matched your search

-2
View File
@@ -2011,8 +2011,6 @@ dependencies = [
[[package]]
name = "femtovg"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727c5dd9d601f3f4ea133b8a6ba37d37ca67ed59781675dc1c9c9f1701e0d197"
dependencies = [
"bitflags 2.11.0",
"bytemuck",
+9
View File
@@ -121,3 +121,12 @@ i-slint-renderer-femtovg = { path = "vendor/i-slint-renderer-femtovg-1.16.1" }
# collection creation, which takes priority over the CoreText query. macOS-only
# (#[cfg]); Linux keeps its working fontconfig fallback.
i-slint-common = { path = "vendor/i-slint-common-1.16.1" }
# Slint compiles femtovg with the swash raster path. Swash fails to scale
# some variable fonts at a non-default variation instance (the CFF2-based
# CJK system fonts on modern macOS), and femtovg cached those glyphs as
# permanently missing — so CJK text at any non-regular weight rendered
# blank (#543; regular weight = empty coords = renders fine). The patch
# retries the glyph at the font's DEFAULT instance before giving up: worst
# case the glyph draws at regular weight instead of not at all. No effect
# on fonts/instances that already render.
femtovg = { path = "vendor/femtovg-0.23.2" }
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "902c9dda464088159d7426f4b04648ba893d0b8b"
},
"path_in_vcs": ""
}
+382
View File
@@ -0,0 +1,382 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.23.2] - 2026-04-13
- Fall back to path rendering for glyphs under non-translation transforms.
## [0.23.1] - 2026-03-31
- Fix variable font glyph caching returning wrong variation instance when
using swash. Stale normalized coordinates in swash's ScaleContext could
cause glyphs from a previous variation (e.g. bold) to appear for
subsequent default-instance renders.
## [0.23.0] - 2026-03-30
- Added variable font support. New methods on `Paint`: `set_font_weight()`,
`set_font_italic()`, `set_font_slant()`, and generic `set_font_variation()`
with corresponding getters, `with_` builders, and `clear_` methods.
- Added named font weight constants on `Paint` (`FONT_WEIGHT_THIN` through
`FONT_WEIGHT_BLACK`).
- Added `Canvas::font_variation_axes()` to query available variation axes
for variable fonts, and new `VariationAxisInfo` public type.
- **Breaking:** `Canvas::fill_glyph_run()` and `Canvas::stroke_glyph_run()`
now take an additional `normalized_coords: &[i16]` parameter for variable
font axis positions. Pass `&[]` for default behavior.
- Replaced bundled Roboto with Roboto Flex for examples and added slant
support with italic fallback.
## [0.22.0] - 2026-03-26
- WGPU: Changed API from `flush_to_surface()` to `flush_to_output()` and
accept a type that can be converted to a new WGPURenderOutput struct,
making it possible to render into texture views.
- WGPU: Simplified CommandBuffer to be an Option<>, so that it can be
passed to queue's `submit()` without additional wrapping.
## [0.21.0] - 2026-03-23
- Re-release 0.20.5 as 0.21 as the glow dependency upgrade is a public
dependency that came with a new major version.
## [0.20.5] - 2026-03-23
- Fix text rendering with wgpu and mesa versions that have difficulties
with pipeline override constants.
- Make the ttf-parser dependency optional.
## [0.20.4] - 2026-02-23
- Fix occasional fringes around swash rendered glyphs caused by uninitialized
padding.
- Ported examples to latest winit/glutin/parley/cosmic-text versions.
## [0.20.3] - 2026-02-18
- Upgraded swash to the latest version.
## [0.20.2] - 2026-02-18
- Added `swash` feature to enable rasterization of glyphs with swash instead of
the built-in path renderer.
## [0.20.1] - 2026-01-15
- Lowered MSRV requirement as it's only opt-in via wgpu feature.
## [0.20.0] - 2026-01-14
- Bumped wgpu dependency to wgpu-28
- Bumped MSRV to 1.92
## [0.19.3] - 2025-10-13
- Fix regression in text rendering performance.
## [0.19.2] - 2025-10-10
- Fix erroneous glyph position regression of commit a1e215782a60df7e4fde9271fe7f95c134ac832f
that accidentally subtracted bearing.
## [0.19.1] - 2025-10-08
- Fix docs.rs build
## [0.19.0] - 2025-10-07
- Bump MSRV to 1.88.
- breaking: Upgraded WGPU renderer to use WGPU 27.x.
- breaking: Make text layout an optional feature
## [0.18.1] - 2025-09-25
- Fix regression causing panic when rendering text.
## [0.18.0] - 2025-09-25
- Added API to drawing runs of glyphs from the same font.
## [0.17.0] - 2025-09-05
- Bump MSRV to 1.85.
- Added support for conical gradients.
## [0.16.0] - 2025-08-03
- Bumped WGPU renderer to use WGPU 26.x.
## [0.15.0] - 2025-07-03
- Bumped WGPU renderer to use WGPU 25.x.
## [0.14.1] - 2025-06-14
- Fixed accidental rendering of newline (and other control characters) when using the Inter font. (#236) (thanks @peterprototypes)
- WGPU renderer: Fixed panic when rendering empty scenes.
## [0.14.0] - 2025-03-24
- Bump MSRV to 1.84.
- Fixed WGPU web rendering (thanks @JoshBurbidge)
## [0.13.0] - 2025-01-29
- Bump MSRV to 1.81.
- Bump wgpu to 0.24.
- **breaking**: The WGPU renderer is now constructed with a wgpu Device/Queue
ithat's not wrapped in an Arc anymore. These types implement clone themselves.
## [0.12.0] - 2025-01-14
- WGPU renderer: Changed `flush_to_surface()` API to return a command buffer,
to let the application decide when to submit.
- Bumped glow dependency.
## [0.11.3] - 2024-12-26
- WGPU renderer: Fix crash when rendering without always calling `set_size()`. (#226)
## [0.11.1] - 2024-11-17
- No code changes, just a release for docs.rs.
## [0.11.0] - 2024-11-17
- Added WGPU renderer, behind `wgpu` feature flag.
- Fixed rendering of glyphs with overlaps (#183). Thanks to Richard Hozák.
- Bumped MSRV to 1.76.
## [0.10.1] - 2024-10-24
- Fix accidental breakage with scissor clipping.
## [0.10.0] - 2024-10-23
- **breaking**: Removed the mutable reference of self in `new` constructor for Transform2D
- Implemented arithmetic operations for `Transform2D`
- Completed and improved documentation
- **breaking**: Removed methods `multipy` from `Transform2D`, since arithmetic operations are defined for `Transform2D` now.
- **breaking**: Renamed `Transform2D` methods `inversed` to `inverse` and `inverse` to `invert`.
- **breaking**: Renamed `Transform2D` constructor `new_translation` to `translation` and added new constructors `rotation` and `scaling`.
- Reimplemented `Transform2D` transformation functions (`translate`, `rotate`, `scale`, `skew_x`, `skew_y`) to do what they are supposed to.
- **breaking**: glow dependency bumped.
## [0.9.2] - 2024-06-27
- Fix path rendering where the default path solidity would interfere with the path's
own winding direction (https://github.com/femtovg/femtovg/issues/124)
- Fix blurry text rendering when drawing on non-integer coordinates
- Bumped MSRV to 1.68.
## [0.9.1] - 2024-04-12
- Fixed inability to introspect `Path` verbs by making `PathIter` and `Verb` public.
- Fixed rendering of text strokes with large font sizes.
## [0.9.0] - 2024-02-27
- **breaking**: Removed pub key field in ImageId. This accidentally
exposed the implementation detail of the image store (generational-arena),
which has been replaced with slotmap.
- For WASM builds, require WebGL 2. This is supported by all major browsers
and needed to make `ImageFlags::REPEAT_X/Y` work.
- Bumped MSRV to 1.66.
## [0.8.2] - 2024-01-20
- Improved performance when rendering large texts.
- Replace error logging to stderr with use of log crate.
## [0.8.1] - 2023-12-18
- Fix documentation build on docs.rs.
## [0.8.0] - 2023-11-02
- Re-release 0.7.2 with major version bump. 0.7.2 was yanked because
glow is a re-exported public dependency, that was bumped.
## [0.7.2] - 2023-11-02
- Bump internal dependencies.
## [0.7.1] - 2023-06-14
- Fix performance regression when drawing unclipped image path fills.
## [0.7.0] - 2023-05-26
### Changed
- Path drawing functions now take a `&Path` instead of a `&mut Path` and use interior mutability
for caching.
## [0.6.0] - 2023-02-06
### Changed
- Changed `linear_gradient_stops` and `radial_gradient_stops` to take an `IntoIterator`
instead of a slice slice for the color stops.
## [0.5.0] - 2023-02-06
### Added
- added a new `Size` struct, having a `width` and a `height`.
- added `size` function to `Image` type, which returns both, `width` and `height` as a `Size`
### Changed
- Renamed `draw_glyph_cmds` to `draw_glyph_commands`.
- Renamed `DrawCmd` to `DrawCommand`.
- `set_transform` takes a value of type `Transform2D` now instead of a parameter list.
- `dimensions` of `ImageSource` returns a new `Size` type now.
## [0.4.0] - 2023-01-27
### Added
- `OpenGl::new_from_function_cstr` to create the renderer from a GL loading function that
takes an `&std::ffi::CStr`.
### Fixed
- Fixed erroneously multiply applied global alpha when mixing color glyphs with regular glyphs.
### Changed
- MRSV was bumped to Rust 1.63, the crate now uses Rust Edition 2021.
- `new_from_glutin_context` can now be used with headless contexts.
- All const-safe `Color` constructors are now const.
- `Canvas`'s text layout methods no longer require a mutable reference.
- Removed the copy trait from `Paint` to avoid accidental copies.
- `Paint` is always supplied by reference now.
- `TextContext`'s `resize_shaping_run_cache` and `resize_shaped_words_cache` functions now take a
`std::num::NonZeroUsize` for the capacity value.
- As part of the glutin update, `OpenGL::new_from_glutin_context` was renamed to `new_from_glutin_display` and takes a glutin display now.
- Removed `glutin` from the default features.
## [0.3.7] - 2022-10-24
### Fixed
- Fix build with latest rustybuzz release after 0.5.2 breakage. 0.5.3 doesn't
re-export the ttf_parser module anymore.
## [0.3.6] - 2022-10-23
### Fixed
- Fix build with latest rustybuzz release.
## [0.3.5] - 2022-05-23
### Changed
- Optimized the OpenGL renderer to perform better on older GPUs by splitting the large fragment shader
into smaller programs.
## [0.3.4] - 2022-04-07
### Added
- Added support for importing backend-specific textures into the rendering of a scene with `Canvas::create_image_from_native_texture`.
- Added functions to `TextContext` to configure the text shaping caches: `resize_shaping_run_cache` and `resize_shaped_words_cache`.
### Changed
- Added optimized rendering code path for the common case of filling a rectangular path with an image and anti-aliasing
on the paint disabled.
### Fixed
- Fixed line breaking to permit a break in the middle of a word if it is the first word in the paragraph
and it doesn't fit otherwise.
## [0.3.3] - 2022-02-21
### Changed
- Bumped rustybuzz and ttf-parser dependencies.
## [0.3.2] - 2022-02-09
### Fixed
- Correctly detect when WebGL is disabled in a web browser in the `renderer::OpenGL::new_from_html_canvas` function.
## [0.3.1] - 2022-02-08
### Fixed
- Don't require default features of glutin. We don't need any and this way other users of glutin
have the ability to opt out.
## [0.3.0] - 2022-02-04
### Changed
- **Breaking:** The dependency to the `image` crate was bumped from `0.23` to `0.24`.
Since the types of this crate are used in public femtovg API, users need to upgrade
their dependency to the `image` crate as well.
- **Breaking**: Removed deprecated `renderer::OpenGL::new` function. Use `renderer::OpenGl::new_from_function`
or `renderer::OpenGl::new_from_glutin_context`.
### Added
- Use `Paint::image_tint` to create an image paint that not only applies an alpha but an entire color (tint).
### Fixed
- Improved performance of `fill_path` and `stroke_path`
[0.3.0]: https://github.com/femtovg/femtovg/releases/tag/v0.3.0
[0.3.1]: https://github.com/femtovg/femtovg/releases/tag/v0.3.1
[0.3.2]: https://github.com/femtovg/femtovg/releases/tag/v0.3.2
[0.3.3]: https://github.com/femtovg/femtovg/releases/tag/v0.3.3
[0.3.4]: https://github.com/femtovg/femtovg/releases/tag/v0.3.4
[0.3.5]: https://github.com/femtovg/femtovg/releases/tag/v0.3.5
[0.3.6]: https://github.com/femtovg/femtovg/releases/tag/v0.3.6
[0.3.7]: https://github.com/femtovg/femtovg/releases/tag/v0.3.7
[0.4.0]: https://github.com/femtovg/femtovg/releases/tag/v0.4.0
[0.5.0]: https://github.com/femtovg/femtovg/releases/tag/v0.5.0
[0.6.0]: https://github.com/femtovg/femtovg/releases/tag/v0.6.0
[0.7.0]: https://github.com/femtovg/femtovg/releases/tag/v0.7.0
[0.7.1]: https://github.com/femtovg/femtovg/releases/tag/v0.7.1
[0.7.2]: https://github.com/femtovg/femtovg/releases/tag/v0.7.2
[0.8.0]: https://github.com/femtovg/femtovg/releases/tag/v0.8.0
[0.8.1]: https://github.com/femtovg/femtovg/releases/tag/v0.8.1
[0.8.2]: https://github.com/femtovg/femtovg/releases/tag/v0.8.2
[0.9.0]: https://github.com/femtovg/femtovg/releases/tag/v0.9.0
[0.9.1]: https://github.com/femtovg/femtovg/releases/tag/v0.9.1
[0.9.2]: https://github.com/femtovg/femtovg/releases/tag/v0.9.2
[0.10.0]: https://github.com/femtovg/femtovg/releases/tag/v0.10.0
[0.10.1]: https://github.com/femtovg/femtovg/releases/tag/v0.10.1
[0.11.0]: https://github.com/femtovg/femtovg/releases/tag/v0.11.0
[0.11.1]: https://github.com/femtovg/femtovg/releases/tag/v0.11.1
[0.11.3]: https://github.com/femtovg/femtovg/releases/tag/v0.11.3
[0.12.0]: https://github.com/femtovg/femtovg/releases/tag/v0.12.0
[0.13.0]: https://github.com/femtovg/femtovg/releases/tag/v0.13.0
[0.14.0]: https://github.com/femtovg/femtovg/releases/tag/v0.14.0
[0.14.1]: https://github.com/femtovg/femtovg/releases/tag/v0.14.1
[0.15.0]: https://github.com/femtovg/femtovg/releases/tag/v0.15.0
[0.16.0]: https://github.com/femtovg/femtovg/releases/tag/v0.16.0
[0.17.0]: https://github.com/femtovg/femtovg/releases/tag/v0.17.0
[0.18.0]: https://github.com/femtovg/femtovg/releases/tag/v0.18.0
[0.18.1]: https://github.com/femtovg/femtovg/releases/tag/v0.18.1
[0.19.0]: https://github.com/femtovg/femtovg/releases/tag/v0.19.0
[0.19.1]: https://github.com/femtovg/femtovg/releases/tag/v0.19.1
[0.19.2]: https://github.com/femtovg/femtovg/releases/tag/v0.19.2
[0.19.3]: https://github.com/femtovg/femtovg/releases/tag/v0.19.3
[0.20.0]: https://github.com/femtovg/femtovg/releases/tag/v0.20.0
[0.20.1]: https://github.com/femtovg/femtovg/releases/tag/v0.20.1
[0.20.2]: https://github.com/femtovg/femtovg/releases/tag/v0.20.2
[0.20.3]: https://github.com/femtovg/femtovg/releases/tag/v0.20.3
[0.20.4]: https://github.com/femtovg/femtovg/releases/tag/v0.20.4
[0.20.5]: https://github.com/femtovg/femtovg/releases/tag/v0.20.5
[0.21.0]: https://github.com/femtovg/femtovg/releases/tag/v0.21.0
[0.22.0]: https://github.com/femtovg/femtovg/releases/tag/v0.22.0
[0.23.0]: https://github.com/femtovg/femtovg/releases/tag/v0.23.0
[0.23.1]: https://github.com/femtovg/femtovg/releases/tag/v0.23.1
[0.23.2]: https://github.com/femtovg/femtovg/releases/tag/v0.23.2
+3833
View File
File diff suppressed because it is too large. Load diff
+271
View File
@@ -0,0 +1,271 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.88"
name = "femtovg"
version = "0.23.2"
authors = [
"Peter Todorov <ptodorov@cytec.bg>",
"Adam Nemecek <adamnemecek@gmail.com>",
]
build = false
exclude = [
"assets",
"examples",
]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Antialiased 2D vector drawing library"
readme = "README.md"
keywords = [
"graphics",
"canvas",
"vector",
"drawing",
"gpu",
]
categories = ["graphics"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/femtovg/femtovg"
[package.metadata.docs.rs]
features = ["glutin/egl"]
all-features = true
rustdoc-args = [
"--cfg",
"docsrs",
]
[features]
debug_inspector = []
default = [
"image-loading",
"textlayout",
]
image-loading = ["image"]
swash = ["dep:swash"]
textlayout = [
"dep:rustybuzz",
"dep:unicode-bidi",
"dep:unicode-segmentation",
"dep:lru",
"dep:ttf-parser",
]
wgpu = ["dep:wgpu"]
[lib]
name = "femtovg"
path = "src/lib.rs"
[[example]]
name = "book_example_1_1"
path = "book/src/1_getting_started/1_setting_up.rs"
[[example]]
name = "book_example_1_2"
path = "book/src/1_getting_started/2_rendering.rs"
[[example]]
name = "book_example_1_3"
path = "book/src/1_getting_started/3_event_loop.rs"
[[test]]
name = "integration"
path = "tests/integration.rs"
[[test]]
name = "transform2D"
path = "tests/transform2D.rs"
[dependencies.bitflags]
version = "2.6.0"
[dependencies.bytemuck]
version = "1.16"
features = ["derive"]
[dependencies.fnv]
version = "1.0.7"
[dependencies.glow]
version = "0.17.0"
default-features = false
[dependencies.image]
version = "0.25.0"
optional = true
default-features = false
[dependencies.imgref]
version = "1.11.0"
[dependencies.itertools]
version = "0.14"
[dependencies.log]
version = "0.4"
[dependencies.lru]
version = "0.16.0"
optional = true
default-features = false
[dependencies.rgb]
version = "0.8.50"
[dependencies.rustybuzz]
version = "0.20.0"
optional = true
[dependencies.serde]
version = "1.0"
features = [
"derive",
"rc",
]
optional = true
[dependencies.slotmap]
version = "1.0.7"
[dependencies.swash]
version = "0.2.6"
optional = true
[dependencies.ttf-parser]
version = "0.25.1"
optional = true
[dependencies.unicode-bidi]
version = "0.3.17"
optional = true
[dependencies.unicode-segmentation]
version = "1.12.0"
optional = true
[dependencies.wgpu]
version = "28"
features = ["wgsl"]
optional = true
default-features = false
[dev-dependencies.cosmic-text]
version = "0.18.2"
[dev-dependencies.euclid]
version = "0.22.3"
[dev-dependencies.image]
version = "0.25.0"
features = [
"jpeg",
"png",
]
default-features = false
[dev-dependencies.instant]
version = "0.1"
features = ["now"]
[dev-dependencies.lazy_static]
version = "1.4.0"
[dev-dependencies.pollster]
version = "0.4"
[dev-dependencies.rand]
version = "0.10"
[dev-dependencies.resource]
version = "0.6.1"
[dev-dependencies.swash]
version = "0.2.6"
[dev-dependencies.usvg]
version = "0.47.0"
[dev-dependencies.wgpu]
version = "28"
[dev-dependencies.winit]
version = "0.30.12"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.glutin]
version = "0.32"
optional = true
default-features = false
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.glutin]
version = "0.32.3"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.glutin-winit]
version = "0.5.0"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.parley]
version = "0.8.0"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.raw-window-handle]
version = "0.6.2"
[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen]
version = "0.2"
[target.'cfg(target_arch = "wasm32")'.dependencies.web_sys]
version = "0.3"
features = [
"WebGlContextAttributes",
"HtmlImageElement",
"WebGl2RenderingContext",
]
package = "web-sys"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.console_error_panic_hook]
version = "0.1.5"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.getrandom]
version = "0.4.1"
features = ["wasm_js"]
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.instant]
version = "0.1"
features = [
"wasm-bindgen",
"now",
]
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.resource]
version = "0.6.1"
features = ["force-static"]
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.wasm-bindgen-futures]
version = "0.4.45"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.web_sys]
version = "0.3"
features = [
"console",
"WebGlContextAttributes",
]
package = "web-sys"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.wgpu]
version = "28"
features = ["webgl"]
[profile.release]
debug = 2
+110
View File
@@ -0,0 +1,110 @@
[package]
name = "femtovg"
description = "Antialiased 2D vector drawing library"
version = "0.23.2"
license = "MIT OR Apache-2.0"
readme = "README.md"
authors = [
"Peter Todorov <ptodorov@cytec.bg>",
"Adam Nemecek <adamnemecek@gmail.com>",
]
keywords = ["graphics", "canvas", "vector", "drawing", "gpu"]
categories = ["graphics"]
repository = "https://github.com/femtovg/femtovg"
edition = "2021"
exclude = ["assets", "examples"]
rust-version = "1.88"
[profile.release]
debug = true
[dependencies]
fnv = { version = "1.0.7" }
rgb = "0.8.50"
bytemuck = { version = "1.16", features = ["derive"] }
imgref = "1.11.0"
bitflags = "2.6.0"
rustybuzz = { version = "0.20.0", optional = true }
unicode-bidi = { version = "0.3.17", optional = true }
unicode-segmentation = { version = "1.12.0", optional = true }
slotmap = "1.0.7"
lru = { version = "0.16.0", optional = true, default-features = false }
image = { version = "0.25.0", optional = true, default-features = false }
serde = { version = "1.0", optional = true, features = ["derive", "rc"] }
glow = { version = "0.17.0", default-features = false }
log = "0.4"
wgpu = { version = "28", optional = true, default-features = false, features = ["wgsl"] }
itertools = "0.14"
ttf-parser = { version = "0.25.1", optional = true }
swash = { version = "0.2.6", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
glutin = { version = "0.32", optional = true, default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
web_sys = { version = "0.3", package = "web-sys", features = [
"WebGlContextAttributes",
"HtmlImageElement",
"WebGl2RenderingContext",
] }
wasm-bindgen = "0.2"
[features]
default = ["image-loading", "textlayout"]
image-loading = ["image"]
debug_inspector = []
wgpu = ["dep:wgpu"]
textlayout = ["dep:rustybuzz", "dep:unicode-bidi", "dep:unicode-segmentation", "dep:lru", "dep:ttf-parser"]
swash = ["dep:swash"]
[dev-dependencies]
winit = { version = "0.30.12" }
euclid = "0.22.3"
rand = "0.10"
usvg = { version = "0.47.0" }
instant = { version = "0.1", features = ["now"] }
resource = "0.6.1"
image = { version = "0.25.0", default-features = false, features = [
"jpeg",
"png",
] }
cosmic-text = "0.18.2"
swash = "0.2.6"
lazy_static = "1.4.0"
pollster = "0.4"
wgpu = { version = "28" }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
glutin = "0.32.3"
glutin-winit = "0.5.0"
raw-window-handle = "0.6.2"
parley = "0.8.0"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
web_sys = { version = "0.3", package = "web-sys", features = [
"console",
"WebGlContextAttributes",
] }
console_error_panic_hook = "0.1.5"
instant = { version = "0.1", features = ["wasm-bindgen", "now"] }
resource = { version = "0.6.1", features = ["force-static"] }
getrandom = { version = "0.4.1", features = ["wasm_js"] }
wgpu = { version = "28", features = ["webgl"] }
wasm-bindgen-futures = { version = "0.4.45" }
[[example]]
name = "book_example_1_1"
path = "book/src/1_getting_started/1_setting_up.rs"
[[example]]
name = "book_example_1_2"
path = "book/src/1_getting_started/2_rendering.rs"
[[example]]
name = "book_example_1_3"
path = "book/src/1_getting_started/3_event_loop.rs"
[package.metadata.docs.rs]
features = ["glutin/egl"]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
+13
View File
@@ -0,0 +1,13 @@
Copyright 2021 femtovg contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+25
View File
@@ -0,0 +1,25 @@
Copyright 2021 Developers of the femtovg project
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+86
View File
@@ -0,0 +1,86 @@
[![Crates.io](https://img.shields.io/crates/v/femtovg?color=4d76ae)](https://crates.io/crates/femtovg)
[![API](https://docs.rs/femtovg/badge.svg)](https://docs.rs/femtovg)
[![dependency status](https://deps.rs/repo/github/femtovg/femtovg/status.svg)](https://deps.rs/repo/github/femtovg/femtovg)
[![rust: 1.70](https://img.shields.io/badge/MSRV-1.70-success.svg)](#msrv)
[![Discord](https://img.shields.io/discord/691716054560407592)](https://discord.gg/V69VdVu)
<h2 align="center">femtovg</h2>
<p align="center">GPU Antialiased 2D vector drawing library written in Rust.</p>
- [Cargo crate](https://crates.io/crates/femtovg)
- [Book](https://femtovg.github.io)
- [Join the femtovg Discord channel](https://discord.gg/V69VdVu)
---
Ported from https://github.com/memononen/nanovg
Most of the implementation is the same as the original C code with some bug fixes, some features added and several parts have been made more Rust-y. Rendering is done via one OpenGl (ES) 3.0+ backend.
## Screenshots
### Demo
![demo](assets/demo.png)
Run with `cargo run --example demo`
### Breakout
![breakout](assets/breakout.png)
Run with `cargo run --example breakout`
### SVG
![svg](assets/svg.png)
Run with `cargo run --example svg`
### Text
![text](assets/text.png)
Run with `cargo run --example text`
## Features
* [x] Anti-aliasing
* [x] Bézier paths filling and stroking
* [x] Solid color and image pattern fills and strokes
* [x] Gradients - box, linear and radial
* [x] Stroke width and miterlimit
* [x] Stroke caps: butt, round and square
* [x] Stroke joins: miter, round and bevel
* [x] Fill rules - EvenOdd/NonZero
* [x] Rectangle scissoring
* [x] Composition modes (SourceOver, SourceIn, SourceOut, Atop, etc..)
* [x] Global alpha
* [x] Text filling and stroking
* [x] Text shaping
* [x] Text alignment: (left center right), (top, middle, alphabetic, bottom)
* [x] Nearest font matching
* [x] Path hit testing
* [x] OpenGl ES2 backend
* [x] WGPU backend
* [x] Color fonts
## Not supported
* [ ] Stroke dashing
* [ ] Path scissoring
* [ ] Custom shaders
* [ ] 3D transforms
## Ecosystem
FemtoVG is used by these projects:
- pn-editor <https://gitlab.com/porky11/pn-editor>:
view, edit, and simulate petri nets
- Slint <https://slint.dev>:
A declarative GUI toolkit to build native user interfaces for embedded, desktop, and mobile application
- inlottie <https://github.com/mhfan/inlottie>:
lib/tool in Rust for Lottie (Bodymovin) Animation
- Mapvas <https://github.com/UdHo/mapvas>:
A map canvas showing OSM tiles with drawing functionality
## License
Licensed under either of
* Apache License, Version 2.0 ([LICENSE-APACHE](docs/LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](docs/LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
Fonts used in examples:
- Roboto licensed under [Apache license](http://www.apache.org/licenses/LICENSE-2.0)
- Entypo licensed under CC BY-SA 4.0.
- Amiri licensed under [SIL Open Font License, Version 1.1](http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
+4
View File
@@ -0,0 +1,4 @@
max_width = 120
reorder_modules = true
# Only available in +nightly.
#imports_layout = "Vertical"
+161
View File
@@ -0,0 +1,161 @@
/// Struct representing a color with red, green, blue, and alpha components.
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Color {
/// Red component of the color (0.0 to 1.0)
pub r: f32,
/// Green component of the color (0.0 to 1.0)
pub g: f32,
/// Blue component of the color (0.0 to 1.0)
pub b: f32,
/// Alpha (opacity) component of the color (0.0 to 1.0)
pub a: f32,
}
impl Color {
/// Creates a color from red, green, and blue u8 values. Alpha is set to 255.
pub fn rgb(r: u8, g: u8, b: u8) -> Self {
Self::rgbf(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
}
/// Creates a color from red, green, and blue f32 values. Alpha is set to 1.0.
pub const fn rgbf(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
/// Creates a color from red, green, blue, and alpha u8 values.
pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self::rgbaf(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)
}
/// Creates a color from red, green, blue, and alpha f32 values.
pub const fn rgbaf(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
/// Creates a color from hue, saturation, and lightness f32 values. Alpha is set to 1.0.
/// All values are all in range [0..1].
pub fn hsl(h: f32, s: f32, l: f32) -> Self {
Self::hsla(h, s, l, 1.0)
}
/// Creates a color from hue, saturation, lightness, and alpha f32 values.
/// All values are all in range [0..1].
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Self {
let mut h = h % 1.0;
if h < 0.0 {
h += 1.0;
}
let s = s.clamp(0.0, 1.0);
let l = l.clamp(0.0, 1.0);
let m2 = if l <= 0.5 { l * (1.0 + s) } else { l + s - l * s };
let m1 = 2.0 * l - m2;
Self {
r: hue(h + 1.0 / 3.0, m1, m2).clamp(0.0, 1.0),
g: hue(h, m1, m2).clamp(0.0, 1.0),
b: hue(h - 1.0 / 3.0, m1, m2).clamp(0.0, 1.0),
a,
}
}
/// Creates a color from a 6-digit (`RRGGBB`) or 8-digit (`RRGGBBAA`) HTML hexadecimal string.
/// Any other length produces `rgb(0,0,0)`.
/// The “#” is optional.
pub fn hex(raw_hex: &str) -> Self {
let hex = raw_hex.trim_start_matches('#');
if hex.len() == 8 {
Self::rgba(
hex_to_u8(&hex[0..2]),
hex_to_u8(&hex[2..4]),
hex_to_u8(&hex[4..6]),
hex_to_u8(&hex[6..8]),
)
} else if hex.len() == 6 {
Self::rgb(hex_to_u8(&hex[0..2]), hex_to_u8(&hex[2..4]), hex_to_u8(&hex[4..6]))
} else {
Self::rgb(0, 0, 0)
}
}
/// Returns a white color (1.0, 1.0, 1.0, 1.0)
pub const fn white() -> Self {
Self::rgbaf(1.0, 1.0, 1.0, 1.0)
}
/// Returns a black color (0.0, 0.0, 0.0, 1.0)
pub const fn black() -> Self {
Self::rgbaf(0.0, 0.0, 0.0, 1.0)
}
/// Sets the alpha (opacity) component of the color from a u8 value.
pub fn set_alpha(&mut self, a: u8) {
self.set_alphaf(a as f32 / 255.0);
}
/// Sets the alpha (opacity) component of the color from an f32 value.
pub fn set_alphaf(&mut self, a: f32) {
self.a = a;
}
/// Returns a color with premultiplied alpha components.
pub fn premultiplied(self) -> Self {
Self {
r: self.r * self.a,
g: self.g * self.a,
b: self.b * self.a,
a: self.a,
}
}
/// Converts the color to a [f32; 4] array.
pub const fn to_array(self) -> [f32; 4] {
[self.r, self.g, self.b, self.a]
}
/// Checks if the color is black (0.0, 0.0, 0.0, 0.0)
pub fn is_black(&self) -> bool {
self.r == 0.0 && self.g == 0.0 && self.b == 0.0 && self.a == 0.0
}
}
impl Default for Color {
fn default() -> Self {
Self {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}
}
}
fn hue(mut h: f32, m1: f32, m2: f32) -> f32 {
if h < 0.0 {
h += 1.0;
}
if h > 1.0 {
h -= 1.0;
}
if h < 1.0 / 6.0 {
return m1 + (m2 - m1) * h * 6.0;
}
if h < 3.0 / 6.0 {
return m2;
}
if h < 4.0 / 6.0 {
return m1 + (m2 - m1) * (2.0 / 3.0 - h) * 6.0;
}
m1
}
// Convert a hex string to decimal. Eg. "00" -> 0. "FF" -> 255.
fn hex_to_u8(hex_string: &str) -> u8 {
u8::from_str_radix(hex_string, 16).unwrap_or(0)
}
+72
View File
@@ -0,0 +1,72 @@
use std::{
error::Error,
ffi::NulError,
fmt::{self, Display, Formatter},
io,
};
/// Enum representing different types of errors that can occur in the canvas.
#[derive(Debug)]
#[non_exhaustive]
pub enum ErrorKind {
/// An unknown error occurred.
UnknownError,
/// A general error with a custom error message.
GeneralError(String),
/// An error related to image loading (requires "image-loading" feature).
#[cfg(feature = "image-loading")]
ImageError(::image::ImageError),
/// An I/O error.
IoError(io::Error),
/// An error occurred while parsing a font.
FontParseError,
/// No font was found.
NoFontFound,
/// An error occurred while extracting font information.
FontInfoExtractionError,
/// The font size is too large for the font atlas.
FontSizeTooLargeForAtlas,
/// An error occurred while compiling a shader.
ShaderCompileError(String),
/// An error occurred while linking a shader.
ShaderLinkError(String),
/// An error related to a render target.
RenderTargetError(String),
/// The specified image ID was not found.
ImageIdNotFound,
/// An error occurred while updating an image, as it is out of bounds.
ImageUpdateOutOfBounds,
/// An error occurred while updating an image with a different format.
ImageUpdateWithDifferentFormat,
/// The specified image format is not supported.
UnsupportedImageFormat,
/// The requested operation is not supported (for example screenshot by wgpu renderer).
UnsupportedOperation,
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "canvas error")
}
}
#[cfg(feature = "image-loading")]
impl From<::image::ImageError> for ErrorKind {
fn from(error: ::image::ImageError) -> Self {
Self::ImageError(error)
}
}
impl From<io::Error> for ErrorKind {
fn from(error: io::Error) -> Self {
Self::IoError(error)
}
}
impl From<NulError> for ErrorKind {
fn from(error: NulError) -> Self {
Self::GeneralError(error.to_string())
}
}
impl Error for ErrorKind {}
+531
View File
@@ -0,0 +1,531 @@
use std::{
hash::{Hash, Hasher},
ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign},
};
use fnv::FnvHasher;
#[derive(Copy, Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Position {
pub x: f32,
pub y: f32,
}
impl Add<Vector> for Position {
type Output = Self;
#[inline]
fn add(self, other: Vector) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl Sub<Vector> for Position {
type Output = Self;
#[inline]
fn sub(self, other: Vector) -> Self {
Self {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
impl Sub for Position {
type Output = Vector;
#[inline]
fn sub(self, other: Self) -> Vector {
Vector {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
impl Position {
pub(crate) fn equals(p1: Self, p2: Self, tol: f32) -> bool {
(p2 - p1).mag2() < tol * tol
}
pub(crate) fn segment_distance(pos: Self, ppos: Self, qpos: Self) -> f32 {
let pq = qpos - ppos;
let dpos = pos - ppos;
let d = pq.mag2();
let mut t = pq.dot(dpos);
if d > 0.0 {
t /= d;
}
t = t.clamp(0.0, 1.0);
let dpos = (ppos - pos) + pq * t;
dpos.mag2()
}
}
#[derive(Copy, Clone, Debug, Default)]
pub struct Vector {
pub x: f32,
pub y: f32,
}
impl Vector {
pub fn zero() -> Self {
Self { x: 0.0, y: 0.0 }
}
pub fn x(x: f32) -> Self {
Self { x, y: 0.0 }
}
pub fn y(y: f32) -> Self {
Self { x: 0.0, y }
}
pub fn with_basis(self, basis_x: Self, basis_y: Self) -> Self {
basis_x * self.x + basis_y * self.y
}
pub fn cross(self, other: Self) -> f32 {
self.orthogonal().dot(other)
}
#[inline]
pub fn dot(self, other: Self) -> f32 {
self.x * other.x + self.y * other.y
}
#[inline]
pub fn mag2(self) -> f32 {
self.dot(self)
}
#[inline]
pub fn orthogonal(self) -> Self {
Self { x: self.y, y: -self.x }
}
#[inline]
pub fn from_angle(angle: f32) -> Self {
let (y, x) = angle.sin_cos();
Self { x, y }
}
#[inline]
pub fn angle(&self) -> f32 {
self.y.atan2(self.x)
}
pub fn normalize(&mut self) -> f32 {
let d = self.x.hypot(self.y);
if d > 1e-6 {
let id = 1.0 / d;
self.x *= id;
self.y *= id;
}
d
}
}
impl Add for Vector {
type Output = Self;
#[inline]
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl Sub for Vector {
type Output = Self;
#[inline]
fn sub(self, other: Self) -> Self {
Self {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
impl Neg for Vector {
type Output = Self;
#[inline]
fn neg(self) -> Self {
Self { x: -self.x, y: -self.y }
}
}
impl Mul<f32> for Vector {
type Output = Self;
#[inline]
fn mul(self, other: f32) -> Self {
Self {
x: self.x * other,
y: self.y * other,
}
}
}
impl MulAssign<f32> for Vector {
#[inline]
fn mul_assign(&mut self, other: f32) {
self.x *= other;
self.y *= other;
}
}
pub fn quantize(a: f32, d: f32) -> f32 {
(a / d + 0.5).trunc() * d
}
/// 2×3 matrix (2 rows, 3 columns) used for 2D linear transformations. It can represent transformations such as translation, rotation, or scaling.
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Transform2D(pub [f32; 6]);
impl Transform2D {
/// Creates an identity transformation with no translation, rotation, or scaling applied.
pub fn identity() -> Self {
Self([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
}
/// Creates a new transformation matrix.
///
/// The parameters are interpreted as matrix elements as follows:
/// [a c x]
/// [b d y]
/// [0 0 1]
pub fn new(a: f32, b: f32, c: f32, d: f32, x: f32, y: f32) -> Self {
Self([a, b, c, d, x, y])
}
/// Creates a translation transformation matrix.
pub fn translation(tx: f32, ty: f32) -> Self {
Self([1.0, 0.0, 0.0, 1.0, tx, ty])
}
/// Creates a rotation transformation matrix.
pub fn rotation(a: f32) -> Self {
let (sin, cos) = a.sin_cos();
Self([cos, sin, -sin, cos, 0.0, 0.0])
}
/// Creates a scaling transformation matrix.
pub fn scaling(sx: f32, sy: f32) -> Self {
Self([sx, 0.0, 0.0, sy, 0.0, 0.0])
}
/// Translates the matrix.
pub fn translate(&mut self, tx: f32, ty: f32) {
let Self([.., x, y]) = self;
*x += tx;
*y += ty;
}
/// Rotates the matrix.
pub fn rotate(&mut self, a: f32) {
let (sin, cos) = a.sin_cos();
let Self([a, b, c, d, x, y]) = self;
[*a, *b] = [*a * cos - *b * sin, *a * sin + *b * cos];
[*c, *d] = [*c * cos - *d * sin, *c * sin + *d * cos];
[*x, *y] = [*x * cos - *y * sin, *x * sin + *y * cos];
}
/// Scales the matrix.
pub fn scale(&mut self, sx: f32, sy: f32) {
let Self([a, b, c, d, x, y]) = self;
*a *= sx;
*b *= sy;
*c *= sx;
*d *= sy;
*x *= sx;
*y *= sy;
}
/// Skews the matrix horizontally.
pub fn skew_x(&mut self, a: f32) {
let tan = a.tan();
let Self([a, b, c, d, x, y]) = self;
*a += *b * tan;
*c += *d * tan;
*x += *y * tan;
}
/// Skews the matrix vertically.
pub fn skew_y(&mut self, a: f32) {
let tan = a.tan();
let Self([a, b, c, d, x, y]) = self;
*b += *a * tan;
*d += *c * tan;
*y += *x * tan;
}
/// Premultiplies the current transformation matrix with another matrix.
#[inline]
pub fn premultiply(&mut self, other: &Self) {
*self = *other * *self;
}
/// Inverts the current transformation matrix.
#[inline]
pub fn invert(&mut self) {
*self = self.inverse()
}
/// Returns the inverse of the current transformation matrix.
pub fn inverse(&self) -> Self {
let &Self([a, b, c, d, x, y]) = self;
let [a, b, c, d, x, y] = [a as f64, b as f64, c as f64, d as f64, x as f64, y as f64];
let det = a * d - c * b;
if det > -1e-6 && det < 1e-6 {
return Self::identity();
}
let invdet = 1.0 / det;
Self([
(d * invdet) as f32,
(-b * invdet) as f32,
(-c * invdet) as f32,
(a * invdet) as f32,
((c * y - d * x) * invdet) as f32,
((b * x - a * y) * invdet) as f32,
])
}
/// Transforms a point using the current transformation matrix.
pub fn transform_point(&self, sx: f32, sy: f32) -> (f32, f32) {
let &Self([a, b, c, d, x, y]) = self;
let dx = sx * a + sy * c + x;
let dy = sx * b + sy * d + y;
(dx, dy)
}
/// Calculates the average scale factor of the current transformation matrix.
pub fn average_scale(&self) -> f32 {
let &Self([a, b, c, d, ..]) = self;
let sx = a.hypot(c);
let sy = b.hypot(d);
(sx + sy) * 0.5
}
/// Returns true if this transform is approximately a pure translation,
/// i.e. no rotation, scaling, or skew.
pub(crate) fn is_pure_translation(&self, epsilon: f32) -> bool {
let &Self([a, b, c, d, ..]) = self;
(a - 1.0).abs() < epsilon && b.abs() < epsilon && c.abs() < epsilon && (d - 1.0).abs() < epsilon
}
/// Converts the current transformation matrix to a 3×4 matrix format.
pub fn to_mat3x4(self) -> [f32; 12] {
let Self([a, b, c, d, x, y]) = self;
[a, b, 0.0, 0.0, c, d, 0.0, 0.0, x, y, 1.0, 0.0]
}
/// Generates a cache key for the current transformation matrix.
pub fn cache_key(&self) -> u64 {
let mut hasher = FnvHasher::default();
for i in 0..6 {
self.0[i].to_bits().hash(&mut hasher);
}
hasher.finish()
}
}
impl Default for Transform2D {
fn default() -> Self {
Self::identity()
}
}
impl Index<usize> for Transform2D {
type Output = f32;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl IndexMut<usize> for Transform2D {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.0[index]
}
}
impl Add for Transform2D {
type Output = Self;
fn add(self, other: Self) -> Self::Output {
let Self([a0, b0, c0, d0, x0, y0]) = self;
let Self([a1, b1, c1, d1, x1, y1]) = other;
Self([a0 + a1, b0 + b1, c0 + c1, d0 + d1, x0 + x1, y0 + y1])
}
}
impl AddAssign for Transform2D {
#[inline]
fn add_assign(&mut self, other: Self) {
*self = *self + other;
}
}
impl Sub for Transform2D {
type Output = Self;
fn sub(self, other: Self) -> Self::Output {
let Self([a0, b0, c0, d0, x0, y0]) = self;
let Self([a1, b1, c1, d1, x1, y1]) = other;
Self([a0 - a1, b0 - b1, c0 - c1, d0 - d1, x0 - x1, y0 - y1])
}
}
impl SubAssign for Transform2D {
#[inline]
fn sub_assign(&mut self, other: Self) {
*self = *self - other;
}
}
impl Mul for Transform2D {
type Output = Self;
#[inline]
fn mul(mut self, other: Self) -> Self::Output {
self *= other;
self
}
}
impl MulAssign for Transform2D {
fn mul_assign(&mut self, other: Self) {
let Self([a0, b0, c0, d0, x0, y0]) = self;
let Self([a1, b1, c1, d1, x1, y1]) = other;
[*a0, *b0] = [*a0 * a1 + *b0 * c1, *a0 * b1 + *b0 * d1];
[*c0, *d0] = [*c0 * a1 + *d0 * c1, *c0 * b1 + *d0 * d1];
[*x0, *y0] = [*x0 * a1 + *y0 * c1 + x1, *x0 * b1 + *y0 * d1 + y1];
}
}
impl Div for Transform2D {
type Output = Self;
#[allow(clippy::suspicious_arithmetic_impl)]
fn div(self, other: Self) -> Self::Output {
self * other.inverse()
}
}
impl DivAssign for Transform2D {
#[inline]
fn div_assign(&mut self, other: Self) {
*self = *self / other;
}
}
#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl Rect {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self { x, y, w, h }
}
pub fn intersect(&self, other: Self) -> Self {
let minx = self.x.max(other.x);
let miny = self.y.max(other.y);
let maxx = (self.x + self.w).min(other.x + other.w);
let maxy = (self.y + self.h).min(other.y + other.h);
Self::new(minx, miny, 0.0f32.max(maxx - minx), 0.0f32.max(maxy - miny))
}
pub fn contains_rect(&self, other: &Self) -> bool {
other.is_empty()
|| (self.x <= other.x
&& other.x + other.w <= self.x + self.w
&& self.y <= other.y
&& other.y + other.h <= self.y + self.h)
}
pub fn intersection(&self, other: &Self) -> Option<Self> {
let x = self.x.max(other.x);
let y = self.y.max(other.y);
let w = (self.x + self.w).min(other.x + other.w) - x;
let h = (self.y + self.h).min(other.y + other.h) - y;
let result = Self { x, y, w, h };
if result.is_empty() {
None
} else {
Some(result)
}
}
pub fn is_empty(&self) -> bool {
self.w <= 0. || self.h <= 0.
}
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Bounds {
pub minx: f32,
pub miny: f32,
pub maxx: f32,
pub maxy: f32,
}
impl Default for Bounds {
fn default() -> Self {
Self {
minx: 1e6,
miny: 1e6,
maxx: -1e6,
maxy: -1e6,
}
}
}
impl Bounds {
pub(crate) fn contains(&self, x: f32, y: f32) -> bool {
(self.minx..=self.maxx).contains(&x) && (self.miny..=self.maxy).contains(&y)
}
}
+146
View File
@@ -0,0 +1,146 @@
use std::collections::BTreeMap;
use crate::{
image::ImageStore,
paint::{GradientStop, MultiStopGradient},
Color, ErrorKind, ImageFlags, ImageId, ImageInfo, ImageSource, Renderer,
};
/// `GradientStore` holds image ids for multi-stop gradients. The actual image/textures
/// are contained by the Canvas's `ImageStore`.
//
// If many gradients are used in a frame, we could combine them into a single texture
// and update the texture immediately prior to giving the renderer the command list.
#[derive(Debug)]
pub struct GradientStore {
this_frame: BTreeMap<MultiStopGradient, ImageId>,
prev_frame: BTreeMap<MultiStopGradient, ImageId>,
}
impl GradientStore {
/// Create a new empty gradient store
pub fn new() -> Self {
Self {
this_frame: BTreeMap::new(),
prev_frame: BTreeMap::new(),
}
}
/// Lookup or add a multi-stop gradient in this gradient store.
pub fn lookup_or_add<R: Renderer>(
&mut self,
colors: &MultiStopGradient,
images: &mut ImageStore<R::Image>,
renderer: &mut R,
) -> Result<ImageId, ErrorKind> {
if let Some(gradient_image_id) = self.prev_frame.remove(colors) {
// See if we already have this texture from the previous frame. If we find
// it then we migrate it to the current frame so we don't release it and
// return the texture id to the caller.
self.this_frame.insert(colors.clone(), gradient_image_id);
Ok(gradient_image_id)
} else if let Some(gradient_image_id) = self.this_frame.get(colors) {
// See if we already used this gradient in this frame, and return the texture
// id if we do.
Ok(*gradient_image_id)
} else {
// We need to allocate a texture and synthesize the gradient image.
let info = ImageInfo::new(ImageFlags::REPEAT_Y, 256, 1, crate::PixelFormat::Rgba8);
let gradient_image_id = images.alloc(renderer, info)?;
let image = linear_gradient_stops(colors);
images.update(renderer, gradient_image_id, ImageSource::Rgba(image.as_ref()), 0, 0)?;
self.this_frame.insert(colors.clone(), gradient_image_id);
Ok(gradient_image_id)
}
}
/// Release the textures that were not used in the most recently rendered frame. This
/// method should be called when all the commands have been submitted.
pub fn release_old_gradients<R: Renderer>(&mut self, images: &mut ImageStore<R::Image>, renderer: &mut R) {
let mut prev_textures = BTreeMap::new();
std::mem::swap(&mut prev_textures, &mut self.prev_frame);
for (_, gradient_image_id) in prev_textures {
images.remove(renderer, gradient_image_id);
}
// Move the "this_frame" textures to "prev_frame". "prev_frame" is already empty.
std::mem::swap(&mut self.this_frame, &mut self.prev_frame);
}
}
#[allow(clippy::many_single_char_names)]
// Gradient filling, adapted from https://github.com/lieff/lvg/blob/master/render/common.c#L147
fn gradient_span(dest: &mut [rgb::RGBA8; 256], color0: Color, color1: Color, offset0: f32, offset1: f32) {
let s0o = offset0.clamp(0.0, 1.0);
let s1o = offset1.clamp(0.0, 1.0);
if s1o < s0o {
return;
}
let s = (s0o * 256.0) as usize;
let e = (s1o * 256.0) as usize;
let mut r = color0.r;
let mut g = color0.g;
let mut b = color0.b;
let mut a = color0.a;
let steps = (e - s) as f32;
let dr = (color1.r - r) / steps;
let dg = (color1.g - g) / steps;
let db = (color1.b - b) / steps;
let da = (color1.a - a) / steps;
#[allow(clippy::needless_range_loop)]
for i in s..e {
// The output must be premultiplied, but we don't premultiply until this point
// so that we can do gradients from transparent colors correctly -- for example
// if we have a stop that is fully transparent red and it transitions to opaque
// blue, we should see some red in the gradient. If we premultiply the stops
// then we won't see any red, because we will have already multiplied it to zero.
// This way we'll get the red contribution.
dest[i] = rgb::RGBA8::new(
(r * a * 255.0) as u8,
(g * a * 255.0) as u8,
(b * a * 255.0) as u8,
(a * 255.0) as u8,
);
r += dr;
g += dg;
b += db;
a += da;
}
}
fn linear_gradient_stops(gradient: &MultiStopGradient) -> imgref::Img<Vec<rgb::RGBA8>> {
let mut dest = [rgb::RGBA8::new(0, 0, 0, 0); 256];
// Fill the gradient up to the first stop.
let first_stop = gradient.get(0);
if first_stop.0 > 0.0 {
let s0 = first_stop.0;
let color0 = first_stop.1;
gradient_span(&mut dest, color0, color0, 0.0, s0);
}
// Iterate over the stops in overlapping pairs and fill out the rest of the
// gradient. If the stop position is > 1.0 then we have exhausted the stops
// and should break. As a special case, if the second stop is > 1.0 then we
// fill the current color to the end of the gradient.
for [GradientStop(s0, color0), GradientStop(s1, color1)] in gradient.pairs() {
// Catch the case where the last stop doesn't go all the way to 1.0 and
// pad it.
if s0 < 1.0 && s1 > 1.0 {
gradient_span(&mut dest, color0, color0, s0, 1.0);
} else {
gradient_span(&mut dest, color0, color1, s0, s1);
}
// If the first stop is >1.0 then we're done.
if s0 > 1.0 {
break;
}
}
imgref::Img::new(dest.to_vec(), 256, 1)
}
+295
View File
@@ -0,0 +1,295 @@
use bitflags::bitflags;
use imgref::*;
use rgb::alt::Gray;
use rgb::*;
use slotmap::{DefaultKey, SlotMap};
#[cfg(feature = "image-loading")]
use ::image::DynamicImage;
#[cfg(feature = "image-loading")]
use std::convert::TryFrom;
use crate::{ErrorKind, Renderer};
/// An image handle.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ImageId(DefaultKey);
/// Specifies the format of an image's pixels.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum PixelFormat {
/// 24-bit RGB image format (8 bits per channel)
Rgb8,
/// 32-bit RGBA image format (8 bits per channel, including alpha)
Rgba8,
/// 8-bit grayscale image format
Gray8,
}
bitflags! {
/// Represents a set of flags that modify the behavior of an image.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ImageFlags: u32 {
/// Generates mipmaps during the creation of the image.
const GENERATE_MIPMAPS = 1;
/// Repeats the image in the X direction when rendered.
const REPEAT_X = 1 << 1;
/// Repeats the image in the Y direction when rendered.
const REPEAT_Y = 1 << 2;
/// Flips (inverses) the image in the Y direction when rendered.
const FLIP_Y = 1 << 3;
/// Indicates that the image data has premultiplied alpha.
const PREMULTIPLIED = 1 << 4;
/// Uses nearest-neighbor interpolation instead of linear interpolation when rendering the image.
const NEAREST = 1 << 5;
}
}
/// Represents the source of an image.
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub enum ImageSource<'a> {
/// Image source with RGB image format (8 bits per channel)
Rgb(ImgRef<'a, RGB8>),
/// Image source with RGBA image format (8 bits per channel, including alpha)
Rgba(ImgRef<'a, RGBA8>),
/// Image source with 8-bit grayscale image format
Gray(ImgRef<'a, Gray<u8>>),
/// Image source referencing a HTML image element (only available on `wasm32` target)
#[cfg(target_arch = "wasm32")]
HtmlImageElement(&'a web_sys::HtmlImageElement),
}
impl ImageSource<'_> {
/// Returns the format of the image source.
pub fn format(&self) -> PixelFormat {
match self {
Self::Rgb(_) => PixelFormat::Rgb8,
Self::Rgba(_) => PixelFormat::Rgba8,
Self::Gray(_) => PixelFormat::Gray8,
#[cfg(target_arch = "wasm32")]
Self::HtmlImageElement(_) => PixelFormat::Rgba8,
}
}
/// Returns the dimensions (width and height) of the image source.
pub fn dimensions(&self) -> Size {
match self {
Self::Rgb(imgref) => Size::new(imgref.width(), imgref.height()),
Self::Rgba(imgref) => Size::new(imgref.width(), imgref.height()),
Self::Gray(imgref) => Size::new(imgref.width(), imgref.height()),
#[cfg(target_arch = "wasm32")]
Self::HtmlImageElement(element) => Size::new(element.width() as usize, element.height() as usize),
}
}
}
impl<'a> From<ImgRef<'a, RGB8>> for ImageSource<'a> {
fn from(src: ImgRef<'a, RGB8>) -> Self {
Self::Rgb(src)
}
}
impl<'a> From<ImgRef<'a, RGBA8>> for ImageSource<'a> {
fn from(src: ImgRef<'a, RGBA8>) -> Self {
Self::Rgba(src)
}
}
impl<'a> From<ImgRef<'a, Gray<u8>>> for ImageSource<'a> {
fn from(src: ImgRef<'a, Gray<u8>>) -> Self {
Self::Gray(src)
}
}
#[cfg(target_arch = "wasm32")]
impl<'a> From<&'a web_sys::HtmlImageElement> for ImageSource<'a> {
fn from(src: &'a web_sys::HtmlImageElement) -> Self {
Self::HtmlImageElement(src)
}
}
#[cfg(feature = "image-loading")]
impl<'a> TryFrom<&'a DynamicImage> for ImageSource<'a> {
type Error = ErrorKind;
fn try_from(src: &'a DynamicImage) -> Result<Self, ErrorKind> {
Ok(match src {
::image::DynamicImage::ImageLuma8(img) => {
let src: Img<&[Gray<u8>]> = Img::new(img.as_pixels(), img.width() as usize, img.height() as usize);
ImageSource::from(src)
}
::image::DynamicImage::ImageRgb8(img) => {
let src = Img::new(img.as_rgb(), img.width() as usize, img.height() as usize);
ImageSource::from(src)
}
::image::DynamicImage::ImageRgba8(img) => {
let src = Img::new(img.as_rgba(), img.width() as usize, img.height() as usize);
ImageSource::from(src)
}
// TODO: if format is not supported maybe we should convert it here,
// But that is an expensive operation on the render thread that will remain hidden from the user
_ => return Err(ErrorKind::UnsupportedImageFormat),
})
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct Size {
pub width: usize,
pub height: usize,
}
impl Size {
pub fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
/// Information about an image.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct ImageInfo {
flags: ImageFlags,
size: Size,
format: PixelFormat,
}
impl ImageInfo {
/// Creates a new `ImageInfo` with the specified flags, width, height, and format.
pub fn new(flags: ImageFlags, width: usize, height: usize, format: PixelFormat) -> Self {
Self {
flags,
size: Size { width, height },
format,
}
}
/// Returns the image flags.
pub fn flags(&self) -> ImageFlags {
self.flags
}
/// Returns the image width in pixels.
pub fn width(&self) -> usize {
self.size.width
}
/// Returns the image height in pixels.
pub fn height(&self) -> usize {
self.size.height
}
/// Returns the image size (width and height) in pixels.
pub fn size(&self) -> Size {
self.size
}
/// Returns the image format.
pub fn format(&self) -> PixelFormat {
self.format
}
/// Sets the image format.
pub fn set_format(&mut self, format: PixelFormat) {
self.format = format;
}
}
#[derive(Debug)]
pub struct ImageStore<T>(SlotMap<DefaultKey, (ImageInfo, T)>);
impl<T> Default for ImageStore<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> ImageStore<T> {
pub fn new() -> Self {
Self(SlotMap::new())
}
pub fn alloc<R: Renderer<Image = T>>(&mut self, renderer: &mut R, info: ImageInfo) -> Result<ImageId, ErrorKind> {
let image = renderer.alloc_image(info)?;
Ok(ImageId(self.0.insert((info, image))))
}
pub fn register_native_texture<R: Renderer<Image = T>>(
&mut self,
renderer: &mut R,
texture: R::NativeTexture,
info: ImageInfo,
) -> Result<ImageId, ErrorKind> {
let image = renderer.create_image_from_native_texture(texture, info)?;
Ok(ImageId(self.0.insert((info, image))))
}
// Reallocates the image without changing the id.
pub fn realloc<R: Renderer<Image = T>>(
&mut self,
renderer: &mut R,
id: ImageId,
info: ImageInfo,
) -> Result<(), ErrorKind> {
if let Some(old) = self.0.get_mut(id.0) {
let new = renderer.alloc_image(info)?;
old.0 = info;
old.1 = new;
Ok(())
} else {
Err(ErrorKind::ImageIdNotFound)
}
}
pub fn get(&self, id: ImageId) -> Option<&T> {
self.0.get(id.0).map(|inner| &inner.1)
}
pub fn get_mut(&mut self, id: ImageId) -> Option<&mut T> {
self.0.get_mut(id.0).map(|inner| &mut inner.1)
}
pub fn update<R: Renderer<Image = T>>(
&mut self,
renderer: &mut R,
id: ImageId,
data: ImageSource,
x: usize,
y: usize,
) -> Result<(), ErrorKind> {
if let Some(image) = self.0.get_mut(id.0) {
renderer.update_image(&mut image.1, data, x, y)?;
Ok(())
} else {
Err(ErrorKind::ImageIdNotFound)
}
}
pub fn info(&self, id: ImageId) -> Option<ImageInfo> {
self.0.get(id.0).map(|inner| inner.0)
}
pub fn remove<R: Renderer<Image = T>>(&mut self, renderer: &mut R, id: ImageId) {
if let Some(image) = self.0.remove(id.0) {
renderer.delete_image(image.1, id);
}
}
pub fn clear<R: Renderer<Image = T>>(&mut self, renderer: &mut R) {
for (idx, image) in self.0.drain() {
renderer.delete_image(image.1, ImageId(idx));
}
}
}
/// Specifies the type of filter to apply to images with `crate::Canvas::filter_image`.
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum ImageFilter {
/// Applies a Gaussian blur filter with the specified standard deviation.
GaussianBlur {
/// The standard deviation of the Gaussian blur filter.
sigma: f32,
},
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+550
View File
@@ -0,0 +1,550 @@
use std::{
cell::{RefCell, RefMut},
f32::consts::PI,
slice,
};
use crate::geometry::{Position, Transform2D, Vector};
#[cfg(feature = "textlayout")]
use rustybuzz::ttf_parser;
mod cache;
pub use cache::{Convexity, PathCache};
// Length proportional to radius of a cubic bezier handle for 90deg arcs.
const KAPPA90: f32 = 0.552_284_8; // 0.552_284_749_3;
/// Specifies whether a shape is solid or a hole when adding it to a path.
///
/// The default value is `Solid`.
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Default)]
pub enum Solidity {
/// The shape is solid (filled).
#[default]
Solid = 1,
/// The shape is a hole (not filled).
Hole = 2,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PackedVerb {
MoveTo,
LineTo,
BezierTo,
Solid,
Hole,
Close,
}
/// A verb describes how to interpret one or more points to continue the countour
/// of a [`Path`].
#[derive(Copy, Clone, Debug)]
pub enum Verb {
/// Terminates the current sub-path and defines the new current point by the
/// given x/y f32 coordinates.
MoveTo(f32, f32),
/// Describes that the contour of the path should continue as a line from the
/// current point to the given x/y f32 coordinates.
LineTo(f32, f32),
/// Describes that the contour of the path should continue as a cubie bezier segment from the
/// current point via two control points (as f32 pairs) to the point in the last f32 pair.
BezierTo(f32, f32, f32, f32, f32, f32),
/// Sets the current sub-path winding to be solid.
Solid,
/// Sets the current sub-path winding to be hole.
Hole,
/// Closes the current sub-path.
Close,
}
impl Verb {
fn num_coordinates(&self) -> usize {
match *self {
Self::MoveTo(..) => 1,
Self::LineTo(..) => 1,
Self::BezierTo(..) => 3,
Self::Solid => 0,
Self::Hole => 0,
Self::Close => 0,
}
}
fn from_packed(packed: &PackedVerb, coords: &[Position]) -> Self {
match *packed {
PackedVerb::MoveTo => Self::MoveTo(coords[0].x, coords[0].y),
PackedVerb::LineTo => Self::LineTo(coords[0].x, coords[0].y),
PackedVerb::BezierTo => Self::BezierTo(
coords[0].x,
coords[0].y,
coords[1].x,
coords[1].y,
coords[2].x,
coords[2].y,
),
PackedVerb::Solid => Self::Solid,
PackedVerb::Hole => Self::Hole,
PackedVerb::Close => Self::Close,
}
}
}
/// A collection of verbs (`move_to()`, `line_to()`, `bezier_to()`, etc.)
/// describing one or more contours.
#[derive(Default, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Path {
verbs: Vec<PackedVerb>,
coords: Vec<Position>,
last_pos: Position,
dist_tol: f32,
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) cache: RefCell<Option<(u64, PathCache)>>,
}
impl Path {
/// Creates a new empty path with a distance tolerance of 0.01.
pub fn new() -> Self {
Self {
dist_tol: 0.01,
..Default::default()
}
}
/// Returns the memory size in bytes used by the path.
pub fn size(&self) -> usize {
std::mem::size_of::<PackedVerb>() * self.verbs.len() + std::mem::size_of::<f32>() * self.coords.len()
}
/// Checks if the path is empty (contains no verbs).
pub fn is_empty(&self) -> bool {
self.verbs.is_empty()
}
/// Sets the distance tolerance used for path operations.
pub fn set_distance_tolerance(&mut self, value: f32) {
self.dist_tol = value;
}
/// Returns an iterator over the path's verbs.
pub fn verbs(&self) -> PathIter<'_> {
PathIter {
verbs: self.verbs.iter(),
coords: &self.coords,
}
}
pub(crate) fn cache<'a>(&'a self, transform: &Transform2D, tess_tol: f32, dist_tol: f32) -> RefMut<'a, PathCache> {
// The path cache saves a flattened and transformed version of the path. If client code calls
// (fill|stroke)_path repeatedly with the same Path under the same transform circumstances then it will be
// retrieved from cache. I'm not sure if transform.cache_key() is actually good enough for this
// and if it will produce the correct cache keys under different float edge cases.
let key = transform.cache_key();
// this shouldn't need a bool once non lexic lifetimes are stable
let needs_rebuild = if let Some((transform_cache_key, _cache)) = &*self.cache.borrow() {
key != *transform_cache_key
} else {
true
};
if needs_rebuild {
let path_cache = PathCache::new(self.verbs(), transform, tess_tol, dist_tol);
*self.cache.borrow_mut() = Some((key, path_cache));
}
RefMut::map(self.cache.borrow_mut(), |cache| &mut cache.as_mut().unwrap().1)
}
// Path funcs
/// Starts a new sub-path with the specified point as the first point.
pub fn move_to(&mut self, x: f32, y: f32) {
self.append(&[PackedVerb::MoveTo], &[Position { x, y }]);
}
/// Adds a line segment from the last point in the path to the specified point.
pub fn line_to(&mut self, x: f32, y: f32) {
self.append(&[PackedVerb::LineTo], &[Position { x, y }]);
}
/// Adds a cubic bezier segment from the last point in the path via two control points to the specified point.
pub fn bezier_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) {
self.append(
&[PackedVerb::BezierTo],
&[
Position { x: c1x, y: c1y },
Position { x: c2x, y: c2y },
Position { x, y },
],
);
}
/// Adds a quadratic bezier segment from the last point in the path via a control point to the specified point.
pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
let pos0 = self.last_pos;
let cpos = Position { x: cx, y: cy };
let pos = Position { x, y };
let pos1 = pos0 + (cpos - pos0) * (2.0 / 3.0);
let pos2 = pos + (cpos - pos) * (2.0 / 3.0);
self.append(&[PackedVerb::BezierTo], &[pos1, pos2, pos]);
}
/// Closes the current sub-path with a line segment.
pub fn close(&mut self) {
self.append(&[PackedVerb::Close], &[]);
}
/// Sets the current sub-path winding, see [`Solidity`].
pub fn solidity(&mut self, solidity: Solidity) {
match solidity {
Solidity::Solid => self.append(&[PackedVerb::Solid], &[]),
Solidity::Hole => self.append(&[PackedVerb::Hole], &[]),
}
}
/// Creates new circle arc shaped sub-path. The arc center is at `cx`,`cy`, the arc radius is `r`,
/// and the arc is drawn from angle `a0` to `a1`, and swept in direction `dir` (Winding)
/// Angles are specified in radians.
pub fn arc(&mut self, cx: f32, cy: f32, r: f32, a0: f32, a1: f32, dir: Solidity) {
let cpos = Position { x: cx, y: cy };
let mut da = a1 - a0;
if dir == Solidity::Hole {
if da.abs() >= PI * 2.0 {
da = PI * 2.0;
} else {
while da < 0.0 {
da += PI * 2.0
}
}
} else if da.abs() >= PI * 2.0 {
da = -PI * 2.0;
} else {
while da > 0.0 {
da -= PI * 2.0
}
}
// Split arc into max 90 degree segments.
let ndivs = ((da.abs() / (PI * 0.5) + 0.5) as i32).clamp(1, 5);
let hda = (da / ndivs as f32) / 2.0;
let mut kappa = (4.0 / 3.0 * (1.0 - hda.cos()) / hda.sin()).abs();
let mut commands = Vec::with_capacity(ndivs as usize);
let mut coords = Vec::with_capacity(ndivs as usize);
if dir == Solidity::Solid {
kappa = -kappa;
}
let (mut ppos, mut ptanpos) = (Position { x: 0.0, y: 0.0 }, Vector::zero());
for i in 0..=ndivs {
let a = a0 + da * (i as f32 / ndivs as f32);
let dpos = Vector::from_angle(a);
let pos = cpos + dpos * r;
let tanpos = -dpos.orthogonal() * r * kappa;
if i == 0 {
let first_move = if self.verbs.is_empty() {
PackedVerb::MoveTo
} else {
PackedVerb::LineTo
};
commands.push(first_move);
coords.extend_from_slice(&[pos]);
} else {
commands.push(PackedVerb::BezierTo);
let pos1 = ppos + ptanpos;
let pos2 = pos - tanpos;
coords.extend_from_slice(&[pos1, pos2, pos]);
}
ppos = pos;
ptanpos = tanpos;
}
self.append(&commands, &coords);
}
/// Adds an arc segment at the corner defined by the last path point and two specified points.
pub fn arc_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) {
if self.verbs.is_empty() {
return;
}
let pos0 = self.last_pos;
let pos1 = Position { x: x1, y: y1 };
let pos2 = Position { x: x2, y: y2 };
// Handle degenerate cases.
if Position::equals(pos0, pos1, self.dist_tol)
|| Position::equals(pos1, pos2, self.dist_tol)
|| Position::segment_distance(pos1, pos0, pos2) < self.dist_tol * self.dist_tol
|| radius < self.dist_tol
{
self.line_to(pos1.x, pos1.y);
}
let mut dpos0 = pos0 - pos1;
let mut dpos1 = pos2 - pos1;
dpos0.normalize();
dpos1.normalize();
let a = dpos0.dot(dpos1).acos();
let d = radius / (a / 2.0).tan();
if d > 10000.0 {
return self.line_to(pos1.x, pos1.y);
}
let (cpos, a0, a1, dir);
if dpos0.cross(dpos1) > 0.0 {
cpos = pos1 + dpos0 * d + dpos0.orthogonal() * radius;
a0 = dpos0.angle();
a1 = (-dpos1).angle();
dir = Solidity::Hole;
} else {
cpos = pos1 + dpos0 * d - dpos0.orthogonal() * radius;
a0 = (-dpos0).angle();
a1 = dpos1.angle();
dir = Solidity::Solid;
}
self.arc(cpos.x, cpos.y, radius, a0 + PI / 2.0, a1 + PI / 2.0, dir);
}
/// Creates a new rectangle shaped sub-path.
pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.append(
&[
PackedVerb::MoveTo,
PackedVerb::LineTo,
PackedVerb::LineTo,
PackedVerb::LineTo,
PackedVerb::Close,
],
&{
let hoffset = Vector::x(w);
let voffset = Vector::y(h);
let tl = Position { x, y };
let tr = tl + hoffset;
let br = tr + voffset;
let bl = tl + voffset;
[tl, bl, br, tr]
},
);
}
/// Creates a new rounded rectangle shaped sub-path.
pub fn rounded_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) {
self.rounded_rect_varying(x, y, w, h, r, r, r, r);
}
/// Creates a new rounded rectangle shaped sub-path with varying radii for each corner.
pub fn rounded_rect_varying(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
rad_top_left: f32,
rad_top_right: f32,
rad_bottom_right: f32,
rad_bottom_left: f32,
) {
if rad_top_left < 0.1 && rad_top_right < 0.1 && rad_bottom_right < 0.1 && rad_bottom_left < 0.1 {
self.rect(x, y, w, h);
} else {
let halfw = w.abs() * 0.5;
let halfh = h.abs() * 0.5;
let rx_bl = rad_bottom_left.min(halfw) * w.signum();
let ry_bl = rad_bottom_left.min(halfh) * h.signum();
let rx_br = rad_bottom_right.min(halfw) * w.signum();
let ry_br = rad_bottom_right.min(halfh) * h.signum();
let rx_tr = rad_top_right.min(halfw) * w.signum();
let ry_tr = rad_top_right.min(halfh) * h.signum();
let rx_tl = rad_top_left.min(halfw) * w.signum();
let ry_tl = rad_top_left.min(halfh) * h.signum();
self.append(
&[
PackedVerb::MoveTo,
PackedVerb::LineTo,
PackedVerb::BezierTo,
PackedVerb::LineTo,
PackedVerb::BezierTo,
PackedVerb::LineTo,
PackedVerb::BezierTo,
PackedVerb::LineTo,
PackedVerb::BezierTo,
PackedVerb::Close,
],
&[
Position { x, y: y + ry_tl },
Position { x, y: y + h - ry_bl },
//
Position {
x,
y: y + h - ry_bl * (1.0 - KAPPA90),
},
Position {
x: x + rx_bl * (1.0 - KAPPA90),
y: y + h,
},
Position { x: x + rx_bl, y: y + h },
//
Position {
x: x + w - rx_br,
y: y + h,
},
//
Position {
x: x + w - rx_br * (1.0 - KAPPA90),
y: y + h,
},
Position {
x: x + w,
y: y + h - ry_br * (1.0 - KAPPA90),
},
Position {
x: x + w,
y: y + h - ry_br,
},
//
Position { x: x + w, y: y + ry_tr },
//
Position {
x: x + w,
y: y + ry_tr * (1.0 - KAPPA90),
},
Position {
x: x + w - rx_tr * (1.0 - KAPPA90),
y,
},
Position { x: x + w - rx_tr, y },
//
Position { x: x + rx_tl, y },
//
Position {
x: x + rx_tl * (1.0 - KAPPA90),
y,
},
Position {
x,
y: y + ry_tl * (1.0 - KAPPA90),
},
Position { x, y: y + ry_tl },
],
);
}
}
/// Creates a new ellipse shaped sub-path.
pub fn ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32) {
self.append(
&[
PackedVerb::MoveTo,
PackedVerb::BezierTo,
PackedVerb::BezierTo,
PackedVerb::BezierTo,
PackedVerb::BezierTo,
PackedVerb::Close,
],
&{
let cpos = Position { x: cx, y: cy };
let hoffset = Vector::x(rx);
let voffset = Vector::y(ry);
[
cpos - hoffset,
cpos - hoffset + voffset * KAPPA90,
cpos - hoffset * KAPPA90 + voffset,
cpos + voffset,
cpos + hoffset * KAPPA90 + voffset,
cpos + hoffset + voffset * KAPPA90,
cpos + hoffset,
cpos + hoffset - voffset * KAPPA90,
cpos + hoffset * KAPPA90 - voffset,
cpos - voffset,
cpos - hoffset * KAPPA90 - voffset,
cpos - hoffset - voffset * KAPPA90,
cpos - hoffset,
]
},
);
}
/// Creates a new circle shaped sub-path.
pub fn circle(&mut self, cx: f32, cy: f32, r: f32) {
self.ellipse(cx, cy, r, r);
}
/// Appends a slice of verbs and coordinates to the path.
fn append(&mut self, verbs: &[PackedVerb], coords: &[Position]) {
if !coords.is_empty() {
self.last_pos = coords[coords.len() - 1];
}
self.verbs.extend_from_slice(verbs);
self.coords.extend_from_slice(coords);
}
}
/// An iterator over the verbs and coordinates of a path.
#[derive(Debug)]
pub struct PathIter<'a> {
verbs: slice::Iter<'a, PackedVerb>,
coords: &'a [Position],
}
impl Iterator for PathIter<'_> {
type Item = Verb;
fn next(&mut self) -> Option<Self::Item> {
if let Some(verb) = self.verbs.next() {
let verb = Verb::from_packed(verb, self.coords);
let num_coords = verb.num_coordinates();
self.coords = &self.coords[num_coords..];
Some(verb)
} else {
None
}
}
}
#[cfg(feature = "textlayout")]
impl ttf_parser::OutlineBuilder for Path {
fn move_to(&mut self, x: f32, y: f32) {
self.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.line_to(x, y);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.quad_to(x1, y1, x, y);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.bezier_to(x1, y1, x2, y2, x, y);
}
fn close(&mut self) {
self.close();
}
}
File diff suppressed because it is too large. Load diff
+257
View File
@@ -0,0 +1,257 @@
//! Module containing renderer implementations.
use imgref::ImgVec;
use rgb::RGBA8;
use crate::{
geometry::Position, paint::GlyphTexture, Color, CompositeOperationState, ErrorKind, FillRule, ImageFilter, ImageId,
ImageInfo, ImageSource, ImageStore,
};
mod opengl;
pub use opengl::OpenGl;
#[cfg(feature = "wgpu")]
mod wgpu;
#[cfg(feature = "wgpu")]
pub use wgpu::{WGPURenderOutput, WGPURenderer};
mod void;
pub use void::Void;
mod params;
pub(crate) use params::Params;
/// Represents a drawable object.
#[derive(Copy, Clone, Default, Debug)]
pub struct Drawable {
pub(crate) fill_verts: Option<(usize, usize)>,
pub(crate) stroke_verts: Option<(usize, usize)>,
}
/// Defines different types of commands that can be executed by the renderer.
#[derive(Debug)]
pub enum CommandType {
/// Set the render target (screen or image).
SetRenderTarget(RenderTarget),
/// Clear a rectangle with the specified color.
ClearRect {
/// Color to fill the rectangle with.
color: Color,
},
/// Fill a convex shape.
ConvexFill {
/// Rendering parameters for the fill operation.
params: Params,
},
/// Fill a concave shape.
ConcaveFill {
/// Rendering parameters for the stencil operation.
stencil_params: Params,
/// Rendering parameters for the fill operation.
fill_params: Params,
},
/// Stroke a shape.
Stroke {
/// Rendering parameters for the stroke operation.
params: Params,
},
/// Stroke a shape using stencil.
StencilStroke {
/// Rendering parameters for the first stroke operation.
params1: Params,
/// Rendering parameters for the second stroke operation.
params2: Params,
},
/// Render triangles.
Triangles {
/// Rendering parameters for the triangle operation.
params: Params,
},
/// Render a filtered image.
RenderFilteredImage {
/// ID of the target image.
target_image: ImageId,
/// Image filter to apply.
filter: ImageFilter,
},
}
/// Represents a command that can be executed by the renderer.
#[derive(Debug)]
pub struct Command {
pub(crate) cmd_type: CommandType,
pub(crate) drawables: Vec<Drawable>,
pub(crate) triangles_verts: Option<(usize, usize)>,
pub(crate) image: Option<ImageId>,
pub(crate) glyph_texture: GlyphTexture,
pub(crate) fill_rule: FillRule,
pub(crate) composite_operation: CompositeOperationState,
}
impl Command {
/// Creates a new command with the specified command type.
pub fn new(flavor: CommandType) -> Self {
Self {
cmd_type: flavor,
drawables: Vec::new(),
triangles_verts: None,
image: None,
glyph_texture: GlyphTexture::default(),
fill_rule: FillRule::default(),
composite_operation: CompositeOperationState::default(),
}
}
}
/// Represents different render targets (screen or image).
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum RenderTarget {
/// Render to the screen.
Screen,
/// Render to a specific image.
Image(ImageId),
}
/// The main renderer trait that the [Canvas](../struct.Canvas.html) draws to.
pub trait Renderer {
/// Associated image type.
type Image;
/// Associated native texture type.
type NativeTexture;
/// Associated render output type.
type RenderOutput;
/// Associated type to hold commands created via `flush_to_output`.
type CommandBuffer;
/// Set the size of the renderer.
fn set_size(&mut self, width: u32, height: u32, dpi: f32);
/// Render the specified commands.
fn render(
&mut self,
output: impl Into<Self::RenderOutput>,
images: &mut ImageStore<Self::Image>,
verts: &[Vertex],
commands: Vec<Command>,
) -> Self::CommandBuffer;
/// Allocate a new image with the specified image info.
fn alloc_image(&mut self, info: ImageInfo) -> Result<Self::Image, ErrorKind>;
/// Create a new image from a native texture.
fn create_image_from_native_texture(
&mut self,
native_texture: Self::NativeTexture,
info: ImageInfo,
) -> Result<Self::Image, ErrorKind>;
/// Update an image with new data.
fn update_image(&mut self, image: &mut Self::Image, data: ImageSource, x: usize, y: usize)
-> Result<(), ErrorKind>;
/// Get the native texture associated with an image (default implementation returns an error).
#[allow(unused_variables)]
fn get_native_texture(&self, image: &Self::Image) -> Result<Self::NativeTexture, ErrorKind> {
Err(ErrorKind::UnsupportedImageFormat)
}
/// Delete an image.
fn delete_image(&mut self, image: Self::Image, image_id: ImageId);
/// Take a screenshot of the current render target.
fn screenshot(&mut self) -> Result<ImgVec<RGBA8>, ErrorKind>;
}
/// Marker trait for renderers that don't have a surface.
pub trait SurfacelessRenderer: Renderer {
/// Render the specified commands.
fn render_surfaceless(&mut self, images: &mut ImageStore<Self::Image>, verts: &[Vertex], commands: Vec<Command>);
}
use bytemuck::{Pod, Zeroable};
/// Vertex struct for specifying triangle geometry.
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default, Pod, Zeroable)]
#[repr(C)]
pub struct Vertex {
/// X-coordinate of the vertex.
pub x: f32,
/// Y-coordinate of the vertex.
pub y: f32,
/// U-coordinate of the vertex (for texture mapping).
pub u: f32,
/// V-coordinate of the vertex (for texture mapping).
pub v: f32,
}
impl Vertex {
pub(crate) fn pos(position: Position, u: f32, v: f32) -> Self {
let Position { x, y } = position;
Self { x, y, u, v }
}
/// Create a new vertex with the specified coordinates.
pub fn new(x: f32, y: f32, u: f32, v: f32) -> Self {
Self { x, y, u, v }
}
/// Set the coordinates of the vertex.
pub fn set(&mut self, x: f32, y: f32, u: f32, v: f32) {
*self = Self { x, y, u, v };
}
}
/// Represents different types of shaders used by the renderer.
///
/// The default value is `FillGradient`.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)]
pub enum ShaderType {
/// Fill gradient shader.
#[default]
FillGradient,
/// Fill image shader.
FillImage,
/// Stencil shader.
Stencil,
/// Fill image gradient shader.
FillImageGradient,
/// Filter image shader.
FilterImage,
/// Fill color shader.
FillColor,
/// Texture copy unclipped shader.
TextureCopyUnclipped,
/// Fill color shader without clipping, used for `clear_rect()`
FillColorUnclipped,
/// Fill conic gradient shader.
FillGradientConic,
/// Fill image conic gradient shader.
FillImageGradientConic,
}
impl ShaderType {
/// Convert the shader type to a u8 value.
pub fn to_u8(self) -> u8 {
match self {
Self::FillGradient => 0,
Self::FillImage => 1,
Self::Stencil => 2,
Self::FillImageGradient => 3,
Self::FilterImage => 4,
Self::FillColor => 5,
Self::TextureCopyUnclipped => 6,
Self::FillColorUnclipped => 7,
Self::FillGradientConic => 8,
Self::FillImageGradientConic => 9,
}
}
/// Convert the shader type to a f32 value.
pub fn to_f32(self) -> f32 {
self.to_u8() as f32
}
}
+909
View File
@@ -0,0 +1,909 @@
use std::{collections::HashMap, mem, rc::Rc};
#[cfg(not(target_arch = "wasm32"))]
use std::ffi::c_void;
#[cfg(all(feature = "glutin", not(target_arch = "wasm32")))]
use glutin::display::GlDisplay;
use fnv::FnvHashMap;
use imgref::ImgVec;
use rgb::RGBA8;
use crate::{
renderer::{GlyphTexture, ImageId, Vertex},
BlendFactor, Color, CompositeOperationState, ErrorKind, FillRule, ImageFilter, ImageInfo, ImageSource, ImageStore,
Scissor, Transform2D,
};
use glow::HasContext;
use super::{Command, CommandType, Params, RenderTarget, Renderer, ShaderType, SurfacelessRenderer};
mod program;
use program::MainProgram;
mod gl_texture;
use gl_texture::GlTexture;
mod framebuffer;
use framebuffer::Framebuffer;
mod uniform_array;
use uniform_array::UniformArray;
/// Represents an OpenGL renderer.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
pub struct OpenGl {
debug: bool,
antialias: bool,
is_opengles_2_0: bool,
view: [f32; 2],
screen_view: [f32; 2],
// All types of the vertex/fragment shader, indexed by shader_type when has_glyph_texture is true
main_programs_with_glyph_texture: [Option<MainProgram>; 10],
// Same shader programs but with has_glyph_texture being false
main_programs_without_glyph_texture: [Option<MainProgram>; 10],
current_program: u8,
current_program_needs_glyph_texture: bool,
vert_arr: Option<<glow::Context as glow::HasContext>::VertexArray>,
vert_buff: Option<<glow::Context as glow::HasContext>::Buffer>,
framebuffers: FnvHashMap<ImageId, Result<Framebuffer, ErrorKind>>,
context: Rc<glow::Context>,
screen_target: Option<Framebuffer>,
current_render_target: RenderTarget,
}
impl OpenGl {
/// Creates a new OpenGL renderer from a function loader.
///
/// # Safety
/// This function is unsafe because it requires a function loader that can load OpenGL functions
/// and create a valid OpenGL context.
#[cfg(not(target_arch = "wasm32"))]
pub unsafe fn new_from_function<F>(load_fn: F) -> Result<Self, ErrorKind>
where
F: FnMut(&str) -> *const c_void,
{
let context = glow::Context::from_loader_function(load_fn);
let version = context.get_parameter_string(glow::VERSION);
let is_opengles_2_0 = version.starts_with("OpenGL ES 2.");
Self::new_from_context(context, is_opengles_2_0)
}
/// Creates a new OpenGL renderer from a function loader that takes C-style strings.
///
/// # Safety
/// This function is unsafe because it requires a function loader that can load OpenGL functions
/// and create a valid OpenGL context.
#[cfg(not(target_arch = "wasm32"))]
pub unsafe fn new_from_function_cstr<F>(load_fn: F) -> Result<Self, ErrorKind>
where
F: FnMut(&std::ffi::CStr) -> *const c_void,
{
let context = glow::Context::from_loader_function_cstr(load_fn);
let version = context.get_parameter_string(glow::VERSION);
let is_opengles_2_0 = version.starts_with("OpenGL ES 2.");
Self::new_from_context(context, is_opengles_2_0)
}
/// Creates a new OpenGL renderer from a Glutin display.
#[cfg(all(feature = "glutin", not(target_arch = "wasm32")))]
pub fn new_from_glutin_display(display: &impl GlDisplay) -> Result<Self, ErrorKind> {
unsafe { OpenGl::new_from_function_cstr(|s| display.get_proc_address(s).cast()) }
}
/// Creates a new OpenGL renderer from an HTML canvas element in a WASM32 target.
#[cfg(target_arch = "wasm32")]
pub fn new_from_html_canvas(canvas: &web_sys::HtmlCanvasElement) -> Result<Self, ErrorKind> {
let attrs = web_sys::WebGlContextAttributes::new();
attrs.set_stencil(true);
attrs.set_antialias(false);
use wasm_bindgen::JsCast;
let webgl2_context = match canvas.get_context_with_context_options("webgl2", &attrs) {
Ok(Some(context)) => context.dyn_into::<web_sys::WebGl2RenderingContext>().unwrap(),
_ => {
return Err(ErrorKind::GeneralError(
"Canvas::getContext failed to retrieve WebGL 2 context".to_owned(),
))
}
};
let context = glow::Context::from_webgl2_context(webgl2_context);
Self::new_from_context(context, true)
}
fn new_from_context(context: glow::Context, is_opengles_2_0: bool) -> Result<Self, ErrorKind> {
let debug = cfg!(debug_assertions);
let antialias = true;
let context = Rc::new(context);
let generate_shader_program_variants = |with_glyph_texture| -> Result<_, ErrorKind> {
Ok([
Some(MainProgram::new(
&context,
antialias,
ShaderType::FillGradient,
with_glyph_texture,
)?),
Some(MainProgram::new(
&context,
antialias,
ShaderType::FillImage,
with_glyph_texture,
)?),
if with_glyph_texture {
// No stencil fill with glyph texture
None
} else {
Some(MainProgram::new(&context, antialias, ShaderType::Stencil, false)?)
},
Some(MainProgram::new(
&context,
antialias,
ShaderType::FillImageGradient,
with_glyph_texture,
)?),
if with_glyph_texture {
// Image filter is unrelated to glyph rendering
None
} else {
Some(MainProgram::new(&context, antialias, ShaderType::FilterImage, false)?)
},
Some(MainProgram::new(
&context,
antialias,
ShaderType::FillColor,
with_glyph_texture,
)?),
if with_glyph_texture {
// Texture blitting is unrelated to glyph rendering
None
} else {
Some(MainProgram::new(
&context,
antialias,
ShaderType::TextureCopyUnclipped,
false,
)?)
},
None,
Some(MainProgram::new(
&context,
antialias,
ShaderType::FillGradientConic,
with_glyph_texture,
)?),
Some(MainProgram::new(
&context,
antialias,
ShaderType::FillImageGradientConic,
with_glyph_texture,
)?),
])
};
let main_programs_with_glyph_texture = generate_shader_program_variants(true)?;
let main_programs_without_glyph_texture = generate_shader_program_variants(false)?;
let mut opengl = Self {
debug,
antialias,
is_opengles_2_0: false,
view: [0.0, 0.0],
screen_view: [0.0, 0.0],
main_programs_with_glyph_texture,
main_programs_without_glyph_texture,
current_program: 0,
current_program_needs_glyph_texture: true,
vert_arr: None,
vert_buff: None,
framebuffers: HashMap::default(),
context,
screen_target: None,
current_render_target: RenderTarget::Screen,
};
unsafe {
opengl.is_opengles_2_0 = is_opengles_2_0;
opengl.vert_arr = opengl.context.create_vertex_array().ok();
opengl.vert_buff = opengl.context.create_buffer().ok();
}
Ok(opengl)
}
/// Checks if the renderer is using OpenGL ES.
pub fn is_opengles(&self) -> bool {
self.is_opengles_2_0
}
fn check_error(&self, label: &str) {
if !self.debug {
return;
}
let err = unsafe { self.context.get_error() };
if err == glow::NO_ERROR {
return;
}
let message = match err {
glow::INVALID_ENUM => "Invalid enum",
glow::INVALID_VALUE => "Invalid value",
glow::INVALID_OPERATION => "Invalid operation",
glow::OUT_OF_MEMORY => "Out of memory",
glow::INVALID_FRAMEBUFFER_OPERATION => "Invalid framebuffer operation",
_ => "Unknown error",
};
log::error!("({err}) Error on {label} - {message}");
}
fn gl_factor(factor: BlendFactor) -> u32 {
match factor {
BlendFactor::Zero => glow::ZERO,
BlendFactor::One => glow::ONE,
BlendFactor::SrcColor => glow::SRC_COLOR,
BlendFactor::OneMinusSrcColor => glow::ONE_MINUS_SRC_COLOR,
BlendFactor::DstColor => glow::DST_COLOR,
BlendFactor::OneMinusDstColor => glow::ONE_MINUS_DST_COLOR,
BlendFactor::SrcAlpha => glow::SRC_ALPHA,
BlendFactor::OneMinusSrcAlpha => glow::ONE_MINUS_SRC_ALPHA,
BlendFactor::DstAlpha => glow::DST_ALPHA,
BlendFactor::OneMinusDstAlpha => glow::ONE_MINUS_DST_ALPHA,
BlendFactor::SrcAlphaSaturate => glow::SRC_ALPHA_SATURATE,
}
}
fn set_composite_operation(&self, blend_state: CompositeOperationState) {
unsafe {
self.context.blend_func_separate(
Self::gl_factor(blend_state.src_rgb),
Self::gl_factor(blend_state.dst_rgb),
Self::gl_factor(blend_state.src_alpha),
Self::gl_factor(blend_state.dst_alpha),
);
}
}
fn convex_fill(&mut self, images: &ImageStore<GlTexture>, cmd: &Command, gpu_paint: &Params) {
self.set_uniforms(images, gpu_paint, cmd.image, cmd.glyph_texture);
for drawable in &cmd.drawables {
if let Some((start, count)) = drawable.fill_verts {
unsafe {
self.context.draw_arrays(glow::TRIANGLES, start as i32, count as i32);
}
}
if let Some((start, count)) = drawable.stroke_verts {
unsafe {
self.context
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
}
}
}
self.check_error("convex_fill");
}
fn concave_fill(
&mut self,
images: &ImageStore<GlTexture>,
cmd: &Command,
stencil_paint: &Params,
fill_paint: &Params,
) {
unsafe {
self.context.enable(glow::STENCIL_TEST);
self.context.stencil_mask(0xff);
self.context.stencil_func(glow::ALWAYS, 0, 0xff);
self.context.color_mask(false, false, false, false);
//glow::DepthMask(glow::FALSE);
}
self.set_uniforms(images, stencil_paint, None, GlyphTexture::None);
unsafe {
self.context
.stencil_op_separate(glow::FRONT, glow::KEEP, glow::KEEP, glow::INCR_WRAP);
self.context
.stencil_op_separate(glow::BACK, glow::KEEP, glow::KEEP, glow::DECR_WRAP);
self.context.disable(glow::CULL_FACE);
}
for drawable in &cmd.drawables {
if let Some((start, count)) = drawable.fill_verts {
unsafe {
self.context.draw_arrays(glow::TRIANGLES, start as i32, count as i32);
}
}
}
unsafe {
self.context.enable(glow::CULL_FACE);
// Draw anti-aliased pixels
self.context.color_mask(true, true, true, true);
//glow::DepthMask(glow::TRUE);
}
self.set_uniforms(images, fill_paint, cmd.image, cmd.glyph_texture);
if self.antialias {
unsafe {
match cmd.fill_rule {
FillRule::NonZero => self.context.stencil_func(glow::EQUAL, 0x0, 0xff),
FillRule::EvenOdd => self.context.stencil_func(glow::EQUAL, 0x0, 0x1),
}
self.context.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
}
// draw fringes
for drawable in &cmd.drawables {
if let Some((start, count)) = drawable.stroke_verts {
unsafe {
self.context
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
}
}
}
}
unsafe {
match cmd.fill_rule {
FillRule::NonZero => self.context.stencil_func(glow::NOTEQUAL, 0x0, 0xff),
FillRule::EvenOdd => self.context.stencil_func(glow::NOTEQUAL, 0x0, 0x1),
}
self.context.stencil_op(glow::ZERO, glow::ZERO, glow::ZERO);
if let Some((start, count)) = cmd.triangles_verts {
self.context
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
}
self.context.disable(glow::STENCIL_TEST);
}
self.check_error("concave_fill");
}
fn stroke(&mut self, images: &ImageStore<GlTexture>, cmd: &Command, paint: &Params) {
self.set_uniforms(images, paint, cmd.image, cmd.glyph_texture);
for drawable in &cmd.drawables {
if let Some((start, count)) = drawable.stroke_verts {
unsafe {
self.context
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
}
}
}
self.check_error("stroke");
}
fn stencil_stroke(&mut self, images: &ImageStore<GlTexture>, cmd: &Command, paint1: &Params, paint2: &Params) {
unsafe {
self.context.enable(glow::STENCIL_TEST);
self.context.stencil_mask(0xff);
// Fill the stroke base without overlap
self.context.stencil_func(glow::EQUAL, 0x0, 0xff);
self.context.stencil_op(glow::KEEP, glow::KEEP, glow::INCR);
}
self.set_uniforms(images, paint2, cmd.image, cmd.glyph_texture);
for drawable in &cmd.drawables {
if let Some((start, count)) = drawable.stroke_verts {
unsafe {
self.context
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
}
}
}
// Draw anti-aliased pixels.
self.set_uniforms(images, paint1, cmd.image, cmd.glyph_texture);
unsafe {
self.context.stencil_func(glow::EQUAL, 0x0, 0xff);
self.context.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
}
for drawable in &cmd.drawables {
if let Some((start, count)) = drawable.stroke_verts {
unsafe {
self.context
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
}
}
}
unsafe {
// Clear stencil buffer.
self.context.color_mask(false, false, false, false);
self.context.stencil_func(glow::ALWAYS, 0x0, 0xff);
self.context.stencil_op(glow::ZERO, glow::ZERO, glow::ZERO);
}
for drawable in &cmd.drawables {
if let Some((start, count)) = drawable.stroke_verts {
unsafe {
self.context
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
}
}
}
unsafe {
self.context.color_mask(true, true, true, true);
self.context.disable(glow::STENCIL_TEST);
}
self.check_error("stencil_stroke");
}
fn triangles(&mut self, images: &ImageStore<GlTexture>, cmd: &Command, paint: &Params) {
self.set_uniforms(images, paint, cmd.image, cmd.glyph_texture);
if let Some((start, count)) = cmd.triangles_verts {
unsafe {
self.context.draw_arrays(glow::TRIANGLES, start as i32, count as i32);
}
}
self.check_error("triangles");
}
fn set_uniforms(
&mut self,
images: &ImageStore<GlTexture>,
paint: &Params,
image_tex: Option<ImageId>,
glyph_tex: GlyphTexture,
) {
self.select_main_program(paint);
let arr = UniformArray::from(paint);
self.main_program().set_config(arr.as_slice());
self.check_error("set_uniforms uniforms");
let tex = image_tex.and_then(|id| images.get(id)).map(GlTexture::id);
unsafe {
self.context.active_texture(glow::TEXTURE0);
self.context.bind_texture(glow::TEXTURE_2D, tex);
}
let glyphtex = glyph_tex.image_id().and_then(|id| images.get(id).map(GlTexture::id));
unsafe {
self.context.active_texture(glow::TEXTURE0 + 1);
self.context.bind_texture(glow::TEXTURE_2D, glyphtex);
}
self.check_error("set_uniforms texture");
}
fn clear_rect(&self, x: u32, y: u32, width: u32, height: u32, color: Color) {
unsafe {
self.context.enable(glow::SCISSOR_TEST);
self.context.scissor(
x as i32,
self.view[1] as i32 - (height as i32 + y as i32),
width as i32,
height as i32,
);
self.context.clear_color(color.r, color.g, color.b, color.a);
self.context.clear(glow::COLOR_BUFFER_BIT | glow::STENCIL_BUFFER_BIT);
self.context.disable(glow::SCISSOR_TEST);
}
}
fn set_target(&mut self, images: &ImageStore<GlTexture>, target: RenderTarget) {
self.current_render_target = target;
match (target, &self.screen_target) {
(RenderTarget::Screen, None) => unsafe {
Framebuffer::unbind(&self.context);
self.view = self.screen_view;
self.context.viewport(0, 0, self.view[0] as i32, self.view[1] as i32);
},
(RenderTarget::Screen, Some(framebuffer)) => {
framebuffer.bind();
self.view = self.screen_view;
unsafe {
self.context.viewport(0, 0, self.view[0] as i32, self.view[1] as i32);
}
}
(RenderTarget::Image(id), _) => {
let context = self.context.clone();
if let Some(texture) = images.get(id) {
if let Ok(fb) = self
.framebuffers
.entry(id)
.or_insert_with(|| Framebuffer::new(&context, texture))
{
fb.bind();
self.view[0] = texture.info().width() as f32;
self.view[1] = texture.info().height() as f32;
unsafe {
self.context
.viewport(0, 0, texture.info().width() as i32, texture.info().height() as i32);
}
}
}
}
}
}
/// Make the "Screen" `RenderTarget` actually render to a framebuffer object. This is useful when
/// embedding femtovg into another program where final composition is handled by an external task.
/// The given `framebuffer_object` must refer to a Framebuffer Object created on the current OpenGL
/// Context, and must have a depth & stencil attachment.
///
/// Pass `None` to clear any previous Framebuffer Object ID that was passed and target rendering to
/// the default target (normally the window).
pub fn set_screen_target(&mut self, framebuffer_object: Option<<glow::Context as glow::HasContext>::Framebuffer>) {
match framebuffer_object {
Some(fbo_id) => self.screen_target = Some(Framebuffer::from_external(&self.context, fbo_id)),
None => self.screen_target = None,
}
}
fn render_filtered_image(
&mut self,
images: &mut ImageStore<GlTexture>,
cmd: Command,
target_image: ImageId,
filter: ImageFilter,
) {
match filter {
ImageFilter::GaussianBlur { sigma } => self.render_gaussian_blur(images, cmd, target_image, sigma),
}
}
fn render_gaussian_blur(
&mut self,
images: &mut ImageStore<GlTexture>,
mut cmd: Command,
target_image: ImageId,
sigma: f32,
) {
let original_render_target = self.current_render_target;
// The filtering happens in two passes, first a horizontal blur and then the vertial blur. The
// first pass therefore renders into an intermediate, temporarily allocated texture.
let source_image_info = images.get(cmd.image.unwrap()).unwrap().info();
let image_paint = crate::Paint::image(
cmd.image.unwrap(),
0.,
0.,
source_image_info.width() as _,
source_image_info.height() as _,
0.,
1.,
);
let mut blur_params = Params::new(
images,
&Transform2D::default(),
&image_paint.flavor,
&GlyphTexture::default(),
&Scissor::default(),
0.,
0.,
0.,
);
blur_params.shader_type = ShaderType::FilterImage;
let gauss_coeff_x = 1. / ((2. * std::f32::consts::PI).sqrt() * sigma);
let gauss_coeff_y = f32::exp(-0.5 / (sigma * sigma));
let gauss_coeff_z = gauss_coeff_y * gauss_coeff_y;
blur_params.image_blur_filter_coeff[0] = gauss_coeff_x;
blur_params.image_blur_filter_coeff[1] = gauss_coeff_y;
blur_params.image_blur_filter_coeff[2] = gauss_coeff_z;
blur_params.image_blur_filter_direction = [1.0, 0.0];
// GLES 2.0 does not allow non-constant loop indices, so limit the standard devitation to allow for a upper fixed limit
// on the number of iterations in the fragment shader.
blur_params.image_blur_filter_sigma = sigma.min(8.);
let horizontal_blur_buffer = images.alloc(self, source_image_info).unwrap();
self.set_target(images, RenderTarget::Image(horizontal_blur_buffer));
self.main_program().set_view(self.view);
self.clear_rect(
0,
0,
source_image_info.width() as _,
source_image_info.height() as _,
Color::rgbaf(0., 0., 0., 0.),
);
self.triangles(images, &cmd, &blur_params);
self.set_target(images, RenderTarget::Image(target_image));
self.main_program().set_view(self.view);
self.clear_rect(
0,
0,
source_image_info.width() as _,
source_image_info.height() as _,
Color::rgbaf(0., 0., 0., 0.),
);
blur_params.image_blur_filter_direction = [0.0, 1.0];
cmd.image = Some(horizontal_blur_buffer);
self.triangles(images, &cmd, &blur_params);
images.remove(self, horizontal_blur_buffer);
// restore previous render target and view
self.set_target(images, original_render_target);
self.main_program().set_view(self.view);
}
fn main_program(&self) -> &MainProgram {
let programs = if self.current_program_needs_glyph_texture {
&self.main_programs_with_glyph_texture
} else {
&self.main_programs_without_glyph_texture
};
programs[self.current_program as usize]
.as_ref()
.expect("internal error: invalid shader program selected for given paint")
}
fn select_main_program(&mut self, params: &Params) {
let program_index = params.shader_type.to_u8();
if program_index != self.current_program
|| params.uses_glyph_texture() != self.current_program_needs_glyph_texture
{
unsafe {
self.context.active_texture(glow::TEXTURE0);
self.context.bind_texture(glow::TEXTURE_2D, None);
self.context.active_texture(glow::TEXTURE0 + 1);
self.context.bind_texture(glow::TEXTURE_2D, None);
}
self.main_program().unbind();
self.current_program = program_index;
self.current_program_needs_glyph_texture = params.uses_glyph_texture();
let program = self.main_program();
program.bind();
// Bind the two uniform samplers to texture units
program.set_tex(0);
program.set_glyphtex(1);
program.set_view(self.view);
}
}
}
impl Renderer for OpenGl {
type Image = GlTexture;
type NativeTexture = <glow::Context as glow::HasContext>::Texture;
type RenderOutput = ();
type CommandBuffer = ();
fn set_size(&mut self, width: u32, height: u32, _dpi: f32) {
self.view[0] = width as f32;
self.view[1] = height as f32;
self.screen_view = self.view;
unsafe {
self.context.viewport(0, 0, width as i32, height as i32);
}
}
fn get_native_texture(&self, image: &Self::Image) -> Result<Self::NativeTexture, ErrorKind> {
Ok(image.id())
}
fn render(
&mut self,
_output: impl Into<Self::RenderOutput>,
images: &mut ImageStore<Self::Image>,
verts: &[Vertex],
commands: Vec<Command>,
) {
self.current_program = 0;
self.main_program().bind();
unsafe {
self.context.enable(glow::CULL_FACE);
self.context.cull_face(glow::BACK);
self.context.front_face(glow::CCW);
self.context.enable(glow::BLEND);
self.context.disable(glow::DEPTH_TEST);
self.context.disable(glow::SCISSOR_TEST);
self.context.color_mask(true, true, true, true);
self.context.stencil_mask(0xffff_ffff);
self.context.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
self.context.stencil_func(glow::ALWAYS, 0, 0xffff_ffff);
self.context.active_texture(glow::TEXTURE0);
self.context.bind_texture(glow::TEXTURE_2D, None);
self.context.active_texture(glow::TEXTURE0 + 1);
self.context.bind_texture(glow::TEXTURE_2D, None);
self.context.bind_vertex_array(self.vert_arr);
let vertex_size = mem::size_of::<Vertex>();
self.context.bind_buffer(glow::ARRAY_BUFFER, self.vert_buff);
self.context
.buffer_data_u8_slice(glow::ARRAY_BUFFER, verts.align_to().1, glow::STREAM_DRAW);
self.context.enable_vertex_attrib_array(0);
self.context.enable_vertex_attrib_array(1);
self.context
.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, vertex_size as i32, 0);
self.context.vertex_attrib_pointer_f32(
1,
2,
glow::FLOAT,
false,
vertex_size as i32,
2 * mem::size_of::<f32>() as i32,
);
}
self.check_error("render prepare");
for cmd in commands {
self.set_composite_operation(cmd.composite_operation);
match cmd.cmd_type {
CommandType::ConvexFill { ref params } => self.convex_fill(images, &cmd, params),
CommandType::ConcaveFill {
ref stencil_params,
ref fill_params,
} => self.concave_fill(images, &cmd, stencil_params, fill_params),
CommandType::Stroke { ref params } => self.stroke(images, &cmd, params),
CommandType::StencilStroke {
ref params1,
ref params2,
} => self.stencil_stroke(images, &cmd, params1, params2),
CommandType::Triangles { ref params } => self.triangles(images, &cmd, params),
CommandType::ClearRect { color } => {
if let Some((start, _)) = cmd.triangles_verts {
let x = verts[start].x as _;
let y = verts[start].y as _;
let width = verts[start + 1].x as u32 - x;
let height = verts[start + 1].y as u32 - y;
self.clear_rect(x, y, width, height, color);
}
}
CommandType::SetRenderTarget(target) => {
self.set_target(images, target);
self.main_program().set_view(self.view);
}
CommandType::RenderFilteredImage { target_image, filter } => {
self.render_filtered_image(images, cmd, target_image, filter)
}
}
}
unsafe {
self.context.disable_vertex_attrib_array(0);
self.context.disable_vertex_attrib_array(1);
self.context.bind_vertex_array(None);
self.context.disable(glow::CULL_FACE);
self.context.bind_buffer(glow::ARRAY_BUFFER, None);
self.context.bind_texture(glow::TEXTURE_2D, None);
}
self.main_program().unbind();
self.check_error("render done");
}
fn alloc_image(&mut self, info: ImageInfo) -> Result<Self::Image, ErrorKind> {
Self::Image::new(&self.context, info, self.is_opengles_2_0)
}
fn create_image_from_native_texture(
&mut self,
native_texture: Self::NativeTexture,
info: ImageInfo,
) -> Result<Self::Image, ErrorKind> {
Ok(Self::Image::new_from_native_texture(native_texture, info))
}
fn update_image(
&mut self,
image: &mut Self::Image,
data: ImageSource,
x: usize,
y: usize,
) -> Result<(), ErrorKind> {
image.update(&self.context, data, x, y, self.is_opengles_2_0)
}
fn delete_image(&mut self, image: Self::Image, image_id: ImageId) {
self.framebuffers.remove(&image_id);
image.delete(&self.context);
}
fn screenshot(&mut self) -> Result<ImgVec<RGBA8>, ErrorKind> {
//let mut image = image::RgbaImage::new(self.view[0] as u32, self.view[1] as u32);
let w = self.view[0] as usize;
let h = self.view[1] as usize;
let mut image = ImgVec::new(
vec![
RGBA8 {
r: 255,
g: 255,
b: 255,
a: 255
};
w * h
],
w,
h,
);
unsafe {
self.context.read_pixels(
0,
0,
self.view[0] as i32,
self.view[1] as i32,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelPackData::Slice(Some(image.buf_mut().align_to_mut().1)),
);
}
let mut flipped = Vec::with_capacity(w * h);
for row in image.rows().rev() {
flipped.extend_from_slice(row);
}
Ok(ImgVec::new(flipped, w, h))
}
}
impl SurfacelessRenderer for OpenGl {
fn render_surfaceless(&mut self, images: &mut ImageStore<Self::Image>, verts: &[Vertex], commands: Vec<Command>) {
self.render((), images, verts, commands)
}
}
impl Drop for OpenGl {
fn drop(&mut self) {
if let Some(vert_arr) = self.vert_arr {
unsafe {
self.context.delete_vertex_array(vert_arr);
}
}
if let Some(vert_buff) = self.vert_buff {
unsafe {
self.context.delete_buffer(vert_buff);
}
}
}
}
@@ -0,0 +1,137 @@
use std::rc::Rc;
use glow::HasContext;
use crate::ErrorKind;
use super::GlTexture;
#[derive(Debug)]
pub struct Framebuffer {
context: Rc<glow::Context>,
fbo: <glow::Context as glow::HasContext>::Framebuffer,
stencil_rbo: Option<<glow::Context as glow::HasContext>::Renderbuffer>,
}
impl Framebuffer {
pub fn from_external(context: &Rc<glow::Context>, fbo: <glow::Context as glow::HasContext>::Framebuffer) -> Self {
Self {
context: context.clone(),
fbo,
stencil_rbo: None,
}
}
pub fn new(context: &Rc<glow::Context>, texture: &GlTexture) -> Result<Self, ErrorKind> {
let fbo = unsafe { context.create_framebuffer().unwrap() };
unsafe {
context.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
}
let width = texture.info().width() as u32;
let height = texture.info().height() as u32;
unsafe {
context.framebuffer_texture_2d(
glow::FRAMEBUFFER,
glow::COLOR_ATTACHMENT0,
glow::TEXTURE_2D,
Some(texture.id()),
0,
);
};
let stencil_rbo = unsafe { context.create_renderbuffer().unwrap() };
unsafe {
context.bind_renderbuffer(glow::RENDERBUFFER, Some(stencil_rbo));
context.renderbuffer_storage(glow::RENDERBUFFER, glow::STENCIL_INDEX8, width as i32, height as i32);
context.bind_renderbuffer(glow::RENDERBUFFER, None);
context.framebuffer_renderbuffer(
glow::FRAMEBUFFER,
glow::STENCIL_ATTACHMENT,
glow::RENDERBUFFER,
Some(stencil_rbo),
);
let status = context.check_framebuffer_status(glow::FRAMEBUFFER);
if status != glow::FRAMEBUFFER_COMPLETE {
let reason = match status {
glow::FRAMEBUFFER_INCOMPLETE_ATTACHMENT => {
format!("({status}) Framebuffer incomplete attachment")
}
//glow::FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER => format!("({}) Framebuffer incomplete draw buffer", status),
//glow::FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS => format!("({}) Framebuffer incomplete layer targets", status),
//FIXME: will be in next glow release: glow::FRAMEBUFFER_INCOMPLETE_DIMENSIONS => format!("({}) Framebuffer incomplete dimensions", status),
glow::FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT => {
format!("({status}) Framebuffer incomplete missing attachment")
}
glow::FRAMEBUFFER_INCOMPLETE_MULTISAMPLE => {
format!("({status}) Framebuffer incomplete multisample")
}
//glow::FRAMEBUFFER_INCOMPLETE_READ_BUFFER => format!("({}) Framebuffer incomplete read buffer", status),
glow::FRAMEBUFFER_UNSUPPORTED => format!("({status}) Framebuffer unsupported"),
_ => format!("({status}) Framebuffer not complete!"),
};
return Err(ErrorKind::RenderTargetError(reason));
}
context.bind_framebuffer(glow::FRAMEBUFFER, None);
}
Ok(Self {
context: context.clone(),
fbo,
stencil_rbo: Some(stencil_rbo),
})
}
pub fn bind(&self) {
unsafe {
self.context.bind_framebuffer(glow::FRAMEBUFFER, Some(self.fbo));
}
}
pub fn unbind(context: &Rc<glow::Context>) {
unsafe {
context.bind_framebuffer(glow::FRAMEBUFFER, None);
}
}
// pub fn blit_to_texture(&self, texture: &GlTexture) {
// let dest_fbo = Self::new(texture);
// unsafe {
// glow::BindFramebuffer(glow::READ_FRAMEBUFFER, self.fbo);
// glow::BindFramebuffer(glow::DRAW_FRAMEBUFFER, dest_fbo.fbo);
// glow::BlitFramebuffer(
// 0,
// 0,
// self.width as i32,
// self.height as i32,
// 0,
// 0,
// dest_fbo.width as i32,
// dest_fbo.height as i32,
// glow::COLOR_BUFFER_BIT,
// glow::NEAREST
// );
// glow::BindFramebuffer(glow::READ_FRAMEBUFFER, 0);
// glow::BindFramebuffer(glow::DRAW_FRAMEBUFFER, 0);
// }
// }
}
impl Drop for Framebuffer {
fn drop(&mut self) {
unsafe {
self.context.delete_framebuffer(self.fbo);
if let Some(stencil_rbo) = self.stencil_rbo {
self.context.delete_renderbuffer(stencil_rbo);
}
}
}
}
@@ -0,0 +1,290 @@
use std::rc::Rc;
use glow::HasContext;
use crate::{ErrorKind, ImageFlags, ImageInfo, ImageSource, PixelFormat};
#[derive(Debug)]
pub struct GlTexture {
id: <glow::Context as glow::HasContext>::Texture,
info: ImageInfo,
owned: bool,
}
impl GlTexture {
pub fn new_from_native_texture(texture: <glow::Context as glow::HasContext>::Texture, info: ImageInfo) -> Self {
Self {
id: texture,
info,
owned: false,
}
}
pub fn new(context: &Rc<glow::Context>, info: ImageInfo, opengles_2_0: bool) -> Result<Self, ErrorKind> {
//let size = src.dimensions();
let id = unsafe {
let id = context.create_texture().unwrap();
context.bind_texture(glow::TEXTURE_2D, Some(id));
context.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
if !opengles_2_0 {
context.pixel_store_i32(glow::UNPACK_ROW_LENGTH, info.width() as i32);
context.pixel_store_i32(glow::UNPACK_SKIP_PIXELS, 0);
context.pixel_store_i32(glow::UNPACK_SKIP_ROWS, 0);
}
id
};
let texture = Self { id, info, owned: true };
match info.format() {
PixelFormat::Gray8 => unsafe {
let internal_format = if opengles_2_0 { glow::LUMINANCE } else { glow::R8 };
let format = if opengles_2_0 { internal_format } else { glow::RED };
context.tex_image_2d(
glow::TEXTURE_2D,
0,
internal_format as i32,
texture.info.width() as i32,
texture.info.height() as i32,
0,
format,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(None), //data.buf().as_ptr() as *const GLvoid
);
},
PixelFormat::Rgb8 => unsafe {
context.tex_image_2d(
glow::TEXTURE_2D,
0,
glow::RGB as i32,
texture.info.width() as i32,
texture.info.height() as i32,
0,
glow::RGB,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(None),
//data.buf().as_ptr() as *const GLvoid
);
},
PixelFormat::Rgba8 => unsafe {
context.tex_image_2d(
glow::TEXTURE_2D,
0,
glow::RGBA as i32,
texture.info.width() as i32,
texture.info.height() as i32,
0,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(None),
//data.buf().as_ptr() as *const GLvoid
);
},
}
let flags = texture.info.flags();
if flags.contains(ImageFlags::GENERATE_MIPMAPS) {
if flags.contains(ImageFlags::NEAREST) {
unsafe {
context.tex_parameter_i32(
glow::TEXTURE_2D,
glow::TEXTURE_MIN_FILTER,
glow::NEAREST_MIPMAP_NEAREST as i32,
);
}
} else {
unsafe {
context.tex_parameter_i32(
glow::TEXTURE_2D,
glow::TEXTURE_MIN_FILTER,
glow::LINEAR_MIPMAP_LINEAR as i32,
);
}
}
} else if flags.contains(ImageFlags::NEAREST) {
unsafe {
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32);
}
} else {
unsafe {
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
}
}
if flags.contains(ImageFlags::NEAREST) {
unsafe {
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32);
}
} else {
unsafe {
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
}
}
if flags.contains(ImageFlags::REPEAT_X) {
unsafe {
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32);
}
} else {
unsafe {
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
}
}
if flags.contains(ImageFlags::REPEAT_Y) {
unsafe {
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32);
}
} else {
unsafe {
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
}
}
unsafe {
context.pixel_store_i32(glow::UNPACK_ALIGNMENT, 4);
if !opengles_2_0 {
context.pixel_store_i32(glow::UNPACK_ROW_LENGTH, 0);
context.pixel_store_i32(glow::UNPACK_SKIP_PIXELS, 0);
context.pixel_store_i32(glow::UNPACK_SKIP_ROWS, 0);
}
}
if flags.contains(ImageFlags::GENERATE_MIPMAPS) {
unsafe {
context.generate_mipmap(glow::TEXTURE_2D);
//glow::TexParameteri(glow::TEXTURE_2D, glow::GENERATE_MIPMAP, glow::TRUE);
}
}
unsafe {
context.bind_texture(glow::TEXTURE_2D, None);
}
Ok(texture)
}
pub fn id(&self) -> <glow::Context as glow::HasContext>::Texture {
self.id
}
pub fn update(
&mut self,
context: &Rc<glow::Context>,
src: ImageSource,
x: usize,
y: usize,
opengles_2_0: bool,
) -> Result<(), ErrorKind> {
let size = src.dimensions();
if x + size.width > self.info.width() {
return Err(ErrorKind::ImageUpdateOutOfBounds);
}
if y + size.height > self.info.height() {
return Err(ErrorKind::ImageUpdateOutOfBounds);
}
if self.info.format() != src.format() {
return Err(ErrorKind::ImageUpdateWithDifferentFormat);
}
unsafe {
context.bind_texture(glow::TEXTURE_2D, Some(self.id));
context.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
if !opengles_2_0 {
context.pixel_store_i32(glow::UNPACK_ROW_LENGTH, size.width as i32);
}
}
match src {
ImageSource::Gray(data) => unsafe {
let format = if opengles_2_0 { glow::LUMINANCE } else { glow::R8 };
context.tex_sub_image_2d(
glow::TEXTURE_2D,
0,
x as i32,
y as i32,
size.width as i32,
size.height as i32,
format,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(Some(data.buf().align_to().1)),
);
},
ImageSource::Rgb(data) => unsafe {
context.tex_sub_image_2d(
glow::TEXTURE_2D,
0,
x as i32,
y as i32,
size.width as i32,
size.height as i32,
glow::RGB,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(Some(data.buf().align_to().1)),
);
},
ImageSource::Rgba(data) => unsafe {
context.tex_sub_image_2d(
glow::TEXTURE_2D,
0,
x as i32,
y as i32,
size.width as i32,
size.height as i32,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(Some(data.buf().align_to().1)),
);
},
#[cfg(target_arch = "wasm32")]
ImageSource::HtmlImageElement(image_element) => unsafe {
context.tex_sub_image_2d_with_html_image(
glow::TEXTURE_2D,
0,
x as i32,
y as i32,
glow::RGBA,
glow::UNSIGNED_BYTE,
image_element,
)
},
}
if self.info.flags().contains(ImageFlags::GENERATE_MIPMAPS) {
unsafe {
context.generate_mipmap(glow::TEXTURE_2D);
//glow::TexParameteri(glow::TEXTURE_2D, glow::GENERATE_MIPMAP, glow::TRUE);
}
}
unsafe {
context.pixel_store_i32(glow::UNPACK_ALIGNMENT, 4);
if !opengles_2_0 {
context.pixel_store_i32(glow::UNPACK_ROW_LENGTH, 0);
}
//glow::PixelStorei(glow::UNPACK_SKIP_PIXELS, 0);
//glow::PixelStorei(glow::UNPACK_SKIP_ROWS, 0);
context.bind_texture(glow::TEXTURE_2D, None);
}
Ok(())
}
pub fn delete(self, context: &Rc<glow::Context>) {
if self.owned {
unsafe {
context.delete_texture(self.id);
}
}
}
pub fn info(&self) -> ImageInfo {
self.info
}
}
@@ -0,0 +1,225 @@
precision highp float;
#define UNIFORMARRAY_SIZE 14
#define TAU 6.28318530717958647692528676655900577
uniform vec4 frag[UNIFORMARRAY_SIZE];
#define scissorMat mat3(frag[0].xyz, frag[1].xyz, frag[2].xyz)
#define paintMat mat3(frag[3].xyz, frag[4].xyz, frag[5].xyz)
#define innerCol frag[6]
#define outerCol frag[7]
#define scissorExt frag[8].xy
#define scissorScale frag[8].zw
#define extent frag[9].xy
#define radius frag[9].z
#define feather frag[9].w
#define strokeMult frag[10].x
#define strokeThr frag[10].y
#define texType int(frag[10].z)
#define shaderType int(frag[10].w)
#define glyphTextureType int(frag[11].x)
#define imageBlurFilterDirection frag[11].yz
#define imageBlurFilterSigma frag[11].w
#define imageBlurFilterCoeff frag[12].xyz
uniform sampler2D tex;
uniform sampler2D glyphtex;
uniform vec2 viewSize;
varying vec2 ftcoord;
varying vec2 fpos;
#define SHADER_TYPE_FillGradient 0
#define SHADER_TYPE_FillImage 1
#define SHADER_TYPE_Stencil 2
#define SHADER_TYPE_FillImageGradient 3
#define SHADER_TYPE_FilterImage 4
#define SHADER_TYPE_FillColor 5
#define SHADER_TYPE_TextureCopyUnclipped 6
#define SHADER_TYPE_FillGradientConic 8
#define SHADER_TYPE_FillImageGradientConic 9
float sdroundrect(vec2 pt, vec2 ext, float rad) {
vec2 ext2 = ext - vec2(rad,rad);
vec2 d = abs(pt) - ext2;
return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rad;
}
// Scissoring
float scissorMask(vec2 p) {
vec2 sc = (abs((scissorMat * vec3(p,1.0)).xy) - scissorExt);
sc = vec2(0.5,0.5) - sc * scissorScale;
return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0);
}
#ifdef EDGE_AA
// Stroke - from [0..1] to clipped pyramid, where the slope is 1px.
float strokeMask() {
return min(1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * min(1.0, ftcoord.y);
// Using this smoothstep preduces maybe better results when combined with fringe_width of 2, but it may look blurrier
// maybe this should be controlled via flag
//return smoothstep(0.0, 1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * smoothstep(0.0, 1.0, ftcoord.y);
}
#endif
vec4 renderGradient() {
// Calculate gradient color using box gradient
vec2 pt = (paintMat * vec3(fpos, 1.0)).xy;
float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0);
return mix(innerCol,outerCol,d);
}
// Image-based Gradient; sample a texture using the gradient position.
vec4 renderImageGradient() {
// Calculate gradient color using box gradient
vec2 pt = (paintMat * vec3(fpos, 1.0)).xy;
float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0);
return texture2D(tex, vec2(d, 0.0));//mix(innerCol,outerCol,d);
}
float conicAngleFraction() {
vec2 pt = (paintMat * vec3(fpos, 1.0)).xy;
// atan returns a value between -pi and pi.
// normally you'd use atan(pt.y,pt.x) but its switched
// around here to be clockwise and start from the top.
return (-atan(pt.x,pt.y) / TAU) + 0.5;
}
vec4 renderGradientConic() {
float d = conicAngleFraction();
return mix(innerCol,outerCol,d);
}
vec4 renderImageGradientConic() {
float d = conicAngleFraction();
return texture2D(tex, vec2(d, 0.0));
}
vec4 renderImage() {
// Calculate color from texture
vec2 pt = (paintMat * vec3(fpos, 1.0)).xy / extent;
vec4 color = texture2D(tex, pt);
if (texType == 1) color = vec4(color.xyz * color.w, color.w);
if (texType == 2) color = vec4(color.x);
// Apply color tint and alpha.
color *= innerCol;
return color;
}
vec4 renderPlainTextureCopy() {
vec4 color = texture2D(tex, ftcoord);
if (texType == 1) color = vec4(color.xyz * color.w, color.w);
if (texType == 2) color = vec4(color.x);
// Apply color tint and alpha.
color *= innerCol;
return color;
}
vec4 renderFilteredImage() {
float sampleCount = ceil(1.5 * imageBlurFilterSigma);
vec3 gaussian_coeff = imageBlurFilterCoeff;
vec4 color_sum = texture2D(tex, fpos.xy / extent) * gaussian_coeff.x;
float coefficient_sum = gaussian_coeff.x;
gaussian_coeff.xy *= gaussian_coeff.yz;
for (float i = 1.0; i <= 12.0; i += 1.) {
// Work around GLES 2.0 limitation of only allowing constant loop indices
// by breaking here. Sigma has an upper bound of 8, imposed on the Rust side.
if (i >= sampleCount) {
break;
}
color_sum += texture2D(tex, (fpos.xy - i * imageBlurFilterDirection) / extent) * gaussian_coeff.x;
color_sum += texture2D(tex, (fpos.xy + i * imageBlurFilterDirection) / extent) * gaussian_coeff.x;
coefficient_sum += 2.0 * gaussian_coeff.x;
// Compute the coefficients incrementally:
// https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-40-incremental-computation-gaussian
gaussian_coeff.xy *= gaussian_coeff.yz;
}
vec4 color = color_sum / coefficient_sum;
if (texType == 1) color = vec4(color.xyz * color.w, color.w);
if (texType == 2) color = vec4(color.x);
return color;
}
void main(void) {
vec4 result;
#ifdef EDGE_AA
float strokeAlpha = 1.0;
#if SELECT_SHADER != 6
strokeAlpha = strokeMask();
if (strokeAlpha < strokeThr) discard;
#endif
#else
float strokeAlpha = 1.0;
#endif
#if SELECT_SHADER == SHADER_TYPE_FillGradient
// Gradient
result = renderGradient();
#elif SELECT_SHADER == SHADER_TYPE_FillImageGradient
// Image-based Gradient; sample a texture using the gradient position.
result = renderImageGradient();
#elif SELECT_SHADER == SHADER_TYPE_FillImage
// Image
result = renderImage();
#elif SELECT_SHADER == SHADER_TYPE_FillColor
// Plain color fill
result = innerCol;
#elif SELECT_SHADER == SHADER_TYPE_TextureCopyUnclipped
// Plain texture copy, unclipped
gl_FragColor = renderPlainTextureCopy();
return;
#elif SELECT_SHADER == SHADER_TYPE_Stencil
// Stencil fill
result = vec4(1,1,1,1);
#elif SELECT_SHADER == SHADER_TYPE_FilterImage
// Filter Image
result = renderFilteredImage();
#elif SELECT_SHADER == SHADER_TYPE_FillGradientConic
result = renderGradientConic();
#elif SELECT_SHADER == SHADER_TYPE_FillImageGradientConic
result = renderImageGradientConic();
#else
#error A shader variant must be selected with the SELECT_SHADER pre-processor variable
#endif
float scissor = scissorMask(fpos);
#ifdef ENABLE_GLYPH_TEXTURE
// Textured tris
vec4 mask = texture2D(glyphtex, ftcoord);
if (glyphTextureType == 1) {
mask = vec4(mask.x);
} else {
result = vec4(1, 1, 1, 1);
mask = vec4(mask.xyz * mask.w, mask.w);
}
mask *= scissor;
result *= mask;
#else
#if SELECT_SHADER != SHADER_TYPE_Stencil && SELECT_SHADER != SHADER_TYPE_FilterImage
// Not stencil fill
// Combine alpha
result *= strokeAlpha * scissor;
#endif
#endif
gl_FragColor = result;
}
@@ -0,0 +1,15 @@
uniform vec2 viewSize;
attribute vec2 vertex;
attribute vec2 tcoord;
varying vec2 ftcoord;
varying vec2 fpos;
void main(void) {
ftcoord = tcoord;
fpos = vertex;
gl_Position = vec4(2.0 * vertex.x / viewSize.x - 1.0, 1.0 - 2.0 * vertex.y / viewSize.y, 0, 1);
}
@@ -0,0 +1,220 @@
use std::rc::Rc;
use glow::HasContext;
use crate::{renderer::ShaderType, ErrorKind};
const GLSL_VERSION: &str = "#version 100";
pub struct Shader {
context: Rc<glow::Context>,
id: <glow::Context as glow::HasContext>::Shader,
}
impl Shader {
pub fn new(context: &Rc<glow::Context>, src: &str, kind: u32) -> Result<Self, ErrorKind> {
let id = unsafe { context.create_shader(kind).unwrap() };
// Compile
unsafe {
context.shader_source(id, src);
context.compile_shader(id);
}
// Validate
let success = unsafe { context.get_shader_compile_status(id) };
if !success {
let error = unsafe { context.get_shader_info_log(id) };
let name = match kind {
glow::VERTEX_SHADER => "Vertex stage",
glow::FRAGMENT_SHADER => "Fragment stage",
_ => "Shader stage",
};
return Err(ErrorKind::ShaderCompileError(format!("{name}: {error}")));
}
Ok(Self {
context: context.clone(),
id,
})
}
pub fn id(&self) -> <glow::Context as glow::HasContext>::Shader {
self.id
}
}
impl Drop for Shader {
fn drop(&mut self) {
unsafe {
self.context.delete_shader(self.id);
}
}
}
#[derive(Debug)]
pub struct Program {
context: Rc<glow::Context>,
id: <glow::Context as glow::HasContext>::Program,
}
impl Program {
pub fn new(context: &Rc<glow::Context>, shaders: &[Shader], attrib_locations: &[&str]) -> Result<Self, ErrorKind> {
let program = Self {
context: context.clone(),
id: unsafe { context.create_program().unwrap() },
};
// Attach stages
for shader in shaders {
unsafe {
context.attach_shader(program.id, shader.id());
}
}
for (i, loc) in attrib_locations.iter().enumerate() {
unsafe {
context.bind_attrib_location(program.id, i as u32, loc);
}
}
unsafe {
context.link_program(program.id);
}
// Check for error
let success = unsafe { context.get_program_link_status(program.id) };
if !success {
let error = unsafe { context.get_program_info_log(program.id) };
return Err(ErrorKind::ShaderLinkError(error));
}
// Detach stages
for shader in shaders {
unsafe {
context.detach_shader(program.id, shader.id());
}
}
Ok(program)
}
pub(crate) fn bind(&self) {
unsafe {
self.context.use_program(Some(self.id));
}
}
pub(crate) fn unbind(&self) {
unsafe {
self.context.use_program(None);
}
}
fn uniform_location(&self, name: &str) -> Option<<glow::Context as glow::HasContext>::UniformLocation> {
unsafe { self.context.get_uniform_location(self.id, name) }
}
}
impl Drop for Program {
fn drop(&mut self) {
unsafe {
self.context.delete_program(self.id);
}
}
}
#[derive(Debug)]
pub struct MainProgram {
context: Rc<glow::Context>,
program: Program,
loc_viewsize: <glow::Context as glow::HasContext>::UniformLocation,
loc_tex: Option<<glow::Context as glow::HasContext>::UniformLocation>,
loc_glyphtex: Option<<glow::Context as glow::HasContext>::UniformLocation>,
loc_frag: Option<<glow::Context as glow::HasContext>::UniformLocation>,
}
impl MainProgram {
pub(crate) fn new(
context: &Rc<glow::Context>,
antialias: bool,
shader_type: ShaderType,
with_glyph_texture: bool,
) -> Result<Self, ErrorKind> {
let shader_defs = if antialias { "#define EDGE_AA 1" } else { "" };
let select_shader_type = format!(
"#define SELECT_SHADER {}\n{}",
shader_type.to_u8(),
if with_glyph_texture {
"#define ENABLE_GLYPH_TEXTURE"
} else {
""
}
);
let vert_shader_src = format!("{}\n{}\n{}", GLSL_VERSION, shader_defs, include_str!("main-vs.glsl"));
let frag_shader_src = format!(
"{}\n{}\n{}\n{}",
GLSL_VERSION,
shader_defs,
select_shader_type,
include_str!("main-fs.glsl")
);
let vert_shader = Shader::new(context, &vert_shader_src, glow::VERTEX_SHADER)?;
let frag_shader = Shader::new(context, &frag_shader_src, glow::FRAGMENT_SHADER)?;
let program = Program::new(context, &[vert_shader, frag_shader], &["vertex", "tcoord"])?;
let loc_viewsize = program.uniform_location("viewSize").unwrap();
let loc_tex = program.uniform_location("tex");
let loc_glyphtex = program.uniform_location("glyphtex");
let loc_frag = program.uniform_location("frag");
Ok(Self {
context: context.clone(),
program,
loc_viewsize,
loc_tex,
loc_glyphtex,
loc_frag,
})
}
pub(crate) fn set_tex(&self, tex: i32) {
unsafe {
self.context.uniform_1_i32(self.loc_tex.as_ref(), tex);
}
}
pub(crate) fn set_glyphtex(&self, tex: i32) {
unsafe {
self.context.uniform_1_i32(self.loc_glyphtex.as_ref(), tex);
}
}
pub(crate) fn set_view(&self, view: [f32; 2]) {
unsafe {
self.context.uniform_2_f32_slice(Some(&self.loc_viewsize), &view);
}
}
pub(crate) fn set_config(&self, config: &[f32]) {
unsafe {
self.context.uniform_4_f32_slice(self.loc_frag.as_ref(), config);
}
}
pub(crate) fn bind(&self) {
self.program.bind();
}
pub(crate) fn unbind(&self) {
self.program.unbind();
}
}
@@ -0,0 +1,115 @@
use super::Params;
const UNIFORMARRAY_SIZE: usize = 14;
pub struct UniformArray([f32; UNIFORMARRAY_SIZE * 4]);
impl Default for UniformArray {
fn default() -> Self {
Self([
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
])
}
}
impl UniformArray {
pub fn as_slice(&self) -> &[f32] {
&self.0
}
pub fn set_scissor_mat(&mut self, mat: [f32; 12]) {
self.0[0..12].copy_from_slice(&mat);
}
pub fn set_paint_mat(&mut self, mat: [f32; 12]) {
self.0[12..24].copy_from_slice(&mat);
}
pub fn set_inner_col(&mut self, col: [f32; 4]) {
self.0[24..28].copy_from_slice(&col);
}
pub fn set_outer_col(&mut self, col: [f32; 4]) {
self.0[28..32].copy_from_slice(&col);
}
pub fn set_scissor_ext(&mut self, ext: [f32; 2]) {
self.0[32..34].copy_from_slice(&ext);
}
pub fn set_scissor_scale(&mut self, scale: [f32; 2]) {
self.0[34..36].copy_from_slice(&scale);
}
pub fn set_extent(&mut self, ext: [f32; 2]) {
self.0[36..38].copy_from_slice(&ext);
}
pub fn set_radius(&mut self, radius: f32) {
self.0[38] = radius;
}
pub fn set_feather(&mut self, feather: f32) {
self.0[39] = feather;
}
pub fn set_stroke_mult(&mut self, stroke_mult: f32) {
self.0[40] = stroke_mult;
}
pub fn set_stroke_thr(&mut self, stroke_thr: f32) {
self.0[41] = stroke_thr;
}
pub fn set_tex_type(&mut self, tex_type: f32) {
self.0[42] = tex_type;
}
pub fn set_shader_type(&mut self, shader_type: f32) {
self.0[43] = shader_type;
}
pub fn set_glyph_texture_type(&mut self, glyph_texture_type: f32) {
self.0[44] = glyph_texture_type;
}
pub fn set_image_blur_filter_direction(&mut self, direction: [f32; 2]) {
self.0[45..47].copy_from_slice(&direction);
}
pub fn set_image_blur_filter_sigma(&mut self, sigma: f32) {
self.0[47] = sigma;
}
pub fn set_image_blur_filter_coeff(&mut self, coeff: [f32; 3]) {
self.0[48..51].copy_from_slice(&coeff);
}
}
impl From<&Params> for UniformArray {
fn from(params: &Params) -> Self {
let mut arr = Self::default();
arr.set_scissor_mat(params.scissor_mat);
arr.set_paint_mat(params.paint_mat);
arr.set_inner_col(params.inner_col);
arr.set_outer_col(params.outer_col);
arr.set_scissor_ext(params.scissor_ext);
arr.set_scissor_scale(params.scissor_scale);
arr.set_extent(params.extent);
arr.set_radius(params.radius);
arr.set_feather(params.feather);
arr.set_stroke_mult(params.stroke_mult);
arr.set_stroke_thr(params.stroke_thr);
arr.set_shader_type(params.shader_type.to_f32());
arr.set_tex_type(params.tex_type);
arr.set_glyph_texture_type(params.glyph_texture_type as f32);
arr.set_image_blur_filter_direction(params.image_blur_filter_direction);
arr.set_image_blur_filter_sigma(params.image_blur_filter_sigma);
arr.set_image_blur_filter_coeff(params.image_blur_filter_coeff);
arr
}
}
+257
View File
@@ -0,0 +1,257 @@
use crate::{
geometry::Position,
paint::{GlyphTexture, GradientColors},
ImageFlags, ImageStore, PaintFlavor, PixelFormat, Scissor, Transform2D,
};
use super::ShaderType;
#[derive(Copy, Clone, Debug, Default)]
pub struct Params {
pub(crate) scissor_mat: [f32; 12],
pub(crate) paint_mat: [f32; 12],
pub(crate) inner_col: [f32; 4],
pub(crate) outer_col: [f32; 4],
pub(crate) scissor_ext: [f32; 2],
pub(crate) scissor_scale: [f32; 2],
pub(crate) extent: [f32; 2],
pub(crate) radius: f32,
pub(crate) feather: f32,
pub(crate) stroke_mult: f32,
pub(crate) stroke_thr: f32,
pub(crate) tex_type: f32,
pub(crate) shader_type: ShaderType,
pub(crate) glyph_texture_type: u8, // 0 -> no glyph rendering, 1 -> alpha mask, 2 -> color texture
pub(crate) image_blur_filter_direction: [f32; 2],
pub(crate) image_blur_filter_sigma: f32,
pub(crate) image_blur_filter_coeff: [f32; 3],
}
impl Params {
pub(crate) fn new<T>(
images: &ImageStore<T>,
global_transform: &Transform2D,
paint_flavor: &PaintFlavor,
glyph_texture: &GlyphTexture,
scissor: &Scissor,
stroke_width: f32,
fringe_width: f32,
stroke_thr: f32,
) -> Self {
let mut params = Self::default();
// Scissor
let (scissor_ext, scissor_scale) = if let Some(ext) = scissor.extent {
if ext[0] < -0.5 || ext[1] < -0.5 {
([1.0, 1.0], [1.0, 1.0])
} else {
params.scissor_mat = scissor.transform.inverse().to_mat3x4();
let Transform2D([a, b, c, d, ..]) = scissor.transform;
let scissor_scale = [a.hypot(c) / fringe_width, b.hypot(d) / fringe_width];
(ext, scissor_scale)
}
} else {
([1.0, 1.0], [1.0, 1.0])
};
params.scissor_ext = scissor_ext;
params.scissor_scale = scissor_scale;
params.stroke_mult = (stroke_width * 0.5 + fringe_width * 0.5) / fringe_width;
params.stroke_thr = stroke_thr;
params.glyph_texture_type = match glyph_texture {
GlyphTexture::None => 0,
GlyphTexture::AlphaMask(_) => 1,
GlyphTexture::ColorTexture(_) => 2,
};
let inv_transform;
match &paint_flavor {
PaintFlavor::Color(color) => {
let color = color.premultiplied().to_array();
params.inner_col = color;
params.outer_col = color;
params.shader_type = ShaderType::FillColor;
inv_transform = global_transform.inverse();
}
&PaintFlavor::Image {
id,
center: Position { x: cx, y: cy },
width,
height,
angle,
tint,
} => {
let Some(image_info) = images.info(*id) else {
return params;
};
params.extent[0] = *width;
params.extent[1] = *height;
let color = tint;
params.inner_col = color.premultiplied().to_array();
params.outer_col = color.premultiplied().to_array();
let mut transform = Transform2D::rotation(*angle);
transform.translate(*cx, *cy);
transform *= *global_transform;
if image_info.flags().contains(ImageFlags::FLIP_Y) {
let mut m1 = Transform2D::translation(0.0, height * 0.5);
m1 *= transform;
let mut m2 = Transform2D::scaling(1.0, -1.0);
m2 *= m1;
let mut m1 = Transform2D::translation(0.0, -height * 0.5);
m1 *= m2;
inv_transform = m1.inverse();
} else {
inv_transform = transform.inverse();
}
params.shader_type = ShaderType::FillImage;
params.tex_type = match image_info.format() {
PixelFormat::Rgba8 => {
if image_info.flags().contains(ImageFlags::PREMULTIPLIED) {
0.0
} else {
1.0
}
}
PixelFormat::Gray8 => 2.0,
PixelFormat::Rgb8 => 0.0,
};
}
PaintFlavor::LinearGradient {
start: Position { x: start_x, y: start_y },
end: Position { x: end_x, y: end_y },
colors,
} => {
let large = 1e5f32;
let mut dx = end_x - start_x;
let mut dy = end_y - start_y;
let d = dx.hypot(dy);
if d > 0.0001 {
dx /= d;
dy /= d;
} else {
dx = 0.0;
dy = 1.0;
}
let mut transform = Transform2D([dy, -dx, dx, dy, start_x - dx * large, start_y - dy * large]);
transform *= *global_transform;
inv_transform = transform.inverse();
params.extent[0] = large;
params.extent[1] = large + d * 0.5;
params.feather = 1.0f32.max(d);
match colors {
GradientColors::TwoStop { start_color, end_color } => {
params.inner_col = start_color.premultiplied().to_array();
params.outer_col = end_color.premultiplied().to_array();
params.shader_type = ShaderType::FillGradient;
}
GradientColors::MultiStop { .. } => {
params.shader_type = ShaderType::FillImageGradient;
}
}
}
&PaintFlavor::BoxGradient {
pos: Position { x, y },
width,
height,
radius,
feather,
colors,
} => {
let mut transform = Transform2D::translation(x + width * 0.5, y + height * 0.5);
transform *= *global_transform;
inv_transform = transform.inverse();
params.extent[0] = width * 0.5;
params.extent[1] = height * 0.5;
params.radius = *radius;
params.feather = *feather;
match colors {
GradientColors::TwoStop { start_color, end_color } => {
params.inner_col = start_color.premultiplied().to_array();
params.outer_col = end_color.premultiplied().to_array();
params.shader_type = ShaderType::FillGradient;
}
GradientColors::MultiStop { .. } => {
params.shader_type = ShaderType::FillImageGradient;
}
}
}
&PaintFlavor::RadialGradient {
center: Position { x: cx, y: cy },
in_radius,
out_radius,
colors,
} => {
let r = (in_radius + out_radius) * 0.5;
let f = out_radius - in_radius;
let mut transform = Transform2D::translation(*cx, *cy);
transform *= *global_transform;
inv_transform = transform.inverse();
params.extent[0] = r;
params.extent[1] = r;
params.radius = r;
params.feather = 1.0f32.max(f);
match colors {
GradientColors::TwoStop { start_color, end_color } => {
params.inner_col = start_color.premultiplied().to_array();
params.outer_col = end_color.premultiplied().to_array();
params.shader_type = ShaderType::FillGradient;
}
GradientColors::MultiStop { .. } => {
params.shader_type = ShaderType::FillImageGradient;
}
}
}
&PaintFlavor::ConicGradient {
center: Position { x: cx, y: cy },
colors,
} => {
let mut transform = Transform2D::translation(*cx, *cy);
transform *= *global_transform;
inv_transform = transform.inverse();
match colors {
GradientColors::TwoStop { start_color, end_color } => {
params.inner_col = start_color.premultiplied().to_array();
params.outer_col = end_color.premultiplied().to_array();
params.shader_type = ShaderType::FillGradientConic;
}
GradientColors::MultiStop { .. } => {
params.shader_type = ShaderType::FillImageGradientConic;
}
}
}
}
params.paint_mat = inv_transform.to_mat3x4();
params
}
pub(crate) fn uses_glyph_texture(self) -> bool {
self.glyph_texture_type != 0
}
}
+73
View File
@@ -0,0 +1,73 @@
#![allow(unused_variables)]
use imgref::ImgVec;
use rgb::RGBA8;
use crate::{ErrorKind, ImageInfo, ImageSource, ImageStore};
use super::{Command, ImageId, Renderer, Vertex};
/// Void renderer used for testing
#[derive(Debug)]
pub struct Void;
impl Renderer for Void {
type Image = VoidImage;
type NativeTexture = ();
type RenderOutput = ();
type CommandBuffer = ();
fn set_size(&mut self, width: u32, height: u32, dpi: f32) {}
fn render(
&mut self,
_output: impl Into<Self::RenderOutput>,
images: &mut ImageStore<VoidImage>,
verts: &[Vertex],
commands: Vec<Command>,
) {
}
fn alloc_image(&mut self, info: ImageInfo) -> Result<Self::Image, ErrorKind> {
Ok(VoidImage { info })
}
fn create_image_from_native_texture(
&mut self,
_native_texture: Self::NativeTexture,
_info: ImageInfo,
) -> Result<Self::Image, ErrorKind> {
Err(ErrorKind::UnsupportedImageFormat)
}
fn update_image(
&mut self,
image: &mut Self::Image,
data: ImageSource,
x: usize,
y: usize,
) -> Result<(), ErrorKind> {
let size = data.dimensions();
if x + size.width > image.info.width() {
return Err(ErrorKind::ImageUpdateOutOfBounds);
}
if y + size.height > image.info.height() {
return Err(ErrorKind::ImageUpdateOutOfBounds);
}
Ok(())
}
fn delete_image(&mut self, image: Self::Image, _image_id: ImageId) {}
fn screenshot(&mut self) -> Result<ImgVec<RGBA8>, ErrorKind> {
Ok(ImgVec::new(Vec::new(), 0, 0))
}
}
#[derive(Debug)]
pub struct VoidImage {
info: ImageInfo,
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,280 @@
struct Params {
scissor_mat: mat3x4<f32>,
paint_mat: mat3x4<f32>,
inner_col: vec4<f32>,
outer_col: vec4<f32>,
scissor_ext: vec2<f32>,
scissor_scale: vec2<f32>,
extent: vec2<f32>,
radius: f32,
feather: f32,
stroke_mult: f32,
stroke_thr: f32,
tex_type: f32,
shader_type: f32,
glyph_texture_type: f32, // 0 -> no glyph rendering, 1 -> alpha mask, 2 -> color texture
image_blur_filter_sigma: f32,
image_blur_filter_direction: vec2<f32>,
image_blur_filter_coeff: vec3<f32>,
}
const SHADER_TYPE_FillGradient: i32 = 0;
const SHADER_TYPE_FillImage: i32 = 1;
const SHADER_TYPE_Stencil: i32 = 2;
const SHADER_TYPE_FillImageGradient: i32 = 3;
const SHADER_TYPE_FilterImage: i32 = 4;
const SHADER_TYPE_FillColor: i32 = 5;
const SHADER_TYPE_TextureCopyUnclipped: i32 = 6;
const SHADER_TYPE_FillColorUnclipped: i32 = 7;
const SHADER_TYPE_FillGradientConic: i32 = 8;
const SHADER_TYPE_FillImageGradientConic: i32 = 9;
const TAU: f32 = 6.28318530717958647692528676655900577;
struct ViewSize {
x: f32,
y: f32,
pad: vec2<f32>,
}
@group(0)
@binding(0)
var<uniform> viewSize: ViewSize;
@group(1)
@binding(0)
var<uniform> params: Params;
struct Vertex {
vertex: vec2<f32>,
tcoord: vec2<f32>,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) ftcoord: vec2<f32>,
@location(1) fpos: vec2<f32>,
};
@vertex
fn vs_main(
@location(0) vertex: vec2<f32>,
@location(1) tcoord: vec2<f32>,
) -> VertexOutput {
var result: VertexOutput;
result.ftcoord = tcoord;
result.fpos = vertex;
result.position = vec4<f32>(2.0 * vertex.x / viewSize.x - 1.0, 1.0 - 2.0 * vertex.y / viewSize.y, 0, 1);
return result;
}
@vertex
fn vs_main_texture(
@location(0) vertex: vec2<f32>,
@location(1) tcoord: vec2<f32>,
) -> VertexOutput {
var result: VertexOutput;
result.ftcoord = tcoord;
result.fpos = vertex;
result.position = vec4<f32>(2.0 * vertex.x / viewSize.x - 1.0, 2.0 * vertex.y / viewSize.y - 1.0, 0, 1);
return result;
}
@group(1)
@binding(1)
var image_texture: texture_2d<f32>;
@group(1)
@binding(2)
var image_sampler: sampler;
@group(1)
@binding(3)
var glyph_texture: texture_2d<f32>;
@group(1)
@binding(4)
var glyph_sampler: sampler;
@fragment
fn fs_main(vertex: VertexOutput) -> @location(0) vec4<f32> {
var result: vec4<f32>;
let shader_type_int: i32 = i32(params.shader_type);
var strokeAlpha: f32 = 1.0;
if (shader_type_int != SHADER_TYPE_TextureCopyUnclipped && shader_type_int != SHADER_TYPE_FillColorUnclipped && shader_type_int != SHADER_TYPE_FilterImage) {
strokeAlpha = strokeMask(vertex, params);
if (strokeAlpha < params.stroke_thr) {
discard;
}
}
switch (shader_type_int) {
case SHADER_TYPE_FillGradient: {
// Gradient
result = renderGradient(vertex, params);
}
case SHADER_TYPE_FillImageGradient: {
// Image-based Gradient; sample a texture using the gradient position.
result = renderImageGradient(vertex, params);
}
case SHADER_TYPE_FillImage: {
// Image
result = renderImage(vertex, params);
}
case SHADER_TYPE_FillColor: {
// Plain color fill
result = params.inner_col;
}
case SHADER_TYPE_TextureCopyUnclipped: {
// Plain texture copy, unclipped
return renderPlainTextureCopy(vertex, params);
}
case SHADER_TYPE_Stencil: {
// Stencil fill
result = vec4<f32>(1,1,1,1);
}
case SHADER_TYPE_FilterImage: {
// Filter Image
return renderFilteredImage(vertex, params);
}
case SHADER_TYPE_FillColorUnclipped: {
// Plain color fill
return params.inner_col;
}
case SHADER_TYPE_FillGradientConic: {
let d = conicAngleFraction(vertex, params);
return mix(params.inner_col,params.outer_col,d);
}
case SHADER_TYPE_FillImageGradientConic: {
let d = conicAngleFraction(vertex, params);
return textureSample(image_texture, image_sampler, vec2<f32>(d, 0.0));
}
default: {
result = vec4<f32>(0.0, 0.0, 1.0, 1.0);
}
}
var scissor: f32 = scissorMask(vertex.fpos, params);
if (params.glyph_texture_type != 0.0) {
// Textured tris
var mask: vec4<f32> = textureSample(glyph_texture, glyph_sampler, vertex.ftcoord);
if (params.glyph_texture_type == 1) {
mask = vec4<f32>(mask.x);
} else {
result = vec4<f32>(1, 1, 1, 1);
mask = vec4<f32>(mask.xyz * mask.w, mask.w);
}
mask *= scissor;
result *= mask;
} else if (shader_type_int != SHADER_TYPE_Stencil && shader_type_int != SHADER_TYPE_FilterImage) {
// Not stencil fill
// Combine alpha
result *= strokeAlpha * scissor;
}
return result;
}
fn conicAngleFraction(vertex: VertexOutput, params: Params) -> f32 {
let pt: vec2<f32> = (params.paint_mat * vec3<f32>(vertex.fpos, 1.0)).xy;
return (-atan2(pt.x,pt.y) / TAU) + 0.5;
}
fn sdroundrect(pt: vec2<f32>, ext: vec2<f32>, rad: f32) -> f32 {
let ext2: vec2<f32> = ext - vec2<f32>(rad,rad);
let d: vec2<f32> = abs(pt) - ext2;
return min(max(d.x,d.y),0.0) + length(max(d, vec2<f32>(0.0, 0.0))) - rad;
}
// Scissoring
fn scissorMask(p: vec2<f32>, params: Params) -> f32 {
var sc: vec2<f32> = (abs((params.scissor_mat * vec3<f32>(p,1.0)).xy) - params.scissor_ext);
sc = vec2(0.5,0.5) - sc * params.scissor_scale;
return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0);
}
// Stroke - from [0..1] to clipped pyramid, where the slope is 1px.
fn strokeMask(vertex: VertexOutput, params: Params) -> f32 {
return min(1.0, (1.0-abs(vertex.ftcoord.x*2.0-1.0))*params.stroke_mult) * min(1.0, vertex.ftcoord.y);
// Using this smoothstep preduces maybe better results when combined with fringe_width of 2, but it may look blurrier
// maybe this should be controlled via flag
//return smoothstep(0.0, 1.0, (1.0-abs(vertex.ftcoord.x*2.0-1.0))*params.stroke_mult) * smoothstep(0.0, 1.0, vertex.ftcoord.y);
}
fn renderGradient(vertex: VertexOutput, params: Params) -> vec4<f32> {
// Calculate gradient color using box gradient
let pt: vec2<f32> = (params.paint_mat * vec3<f32>(vertex.fpos, 1.0)).xy;
let d: f32 = clamp((sdroundrect(pt, params.extent, params.radius) + params.feather*0.5) / params.feather, 0.0, 1.0);
return mix(params.inner_col,params.outer_col,d);
}
// Image-based Gradient; sample a texture using the gradient position.
fn renderImageGradient(vertex: VertexOutput, params: Params) -> vec4<f32> {
// Calculate gradient color using box gradient
let pt: vec2<f32> = (params.paint_mat * vec3<f32>(vertex.fpos, 1.0)).xy;
let d: f32 = clamp((sdroundrect(pt, params.extent, params.radius) + params.feather*0.5) / params.feather, 0.0, 1.0);
return textureSample(image_texture, image_sampler, vec2<f32>(d, 0.0));//mix(innerCol,outerCol,d);
}
fn renderImage(vertex: VertexOutput, params: Params) -> vec4<f32> {
// Calculate color from texture
let pt: vec2<f32> = (params.paint_mat * vec3<f32>(vertex.fpos, 1.0)).xy / params.extent;
var color: vec4<f32> = textureSample(image_texture, image_sampler, pt);
if (params.tex_type == 1) { color = vec4(color.xyz * color.w, color.w); }
if (params.tex_type == 2) { color = vec4(color.x); }
// Apply color tint and alpha.
color *= params.inner_col;
return color;
}
fn renderPlainTextureCopy(vertex: VertexOutput, params: Params) -> vec4<f32> {
var color: vec4<f32> = textureSample(image_texture, image_sampler, vertex.ftcoord);
if (params.tex_type == 1) { color = vec4(color.xyz * color.w, color.w); }
if (params.tex_type == 2) { color = vec4(color.x); }
// Apply color tint and alpha.
color *= params.inner_col;
return color;
}
fn renderFilteredImage(vertex: VertexOutput, params: Params) -> vec4<f32> {
let sampleCount: f32 = ceil(1.5 * params.image_blur_filter_sigma);
var gaussian_coeff: vec3<f32> = params.image_blur_filter_coeff;
var color_sum: vec4<f32> = textureSample(image_texture, image_sampler, vertex.fpos.xy / params.extent) * gaussian_coeff.x;
var coefficient_sum: f32 = gaussian_coeff.x;
gaussian_coeff.x *= gaussian_coeff.y;
gaussian_coeff.y *= gaussian_coeff.z;
for (var i: f32 = 1.0; i <= 12.0; i += 1.) {
// Work around GLES 2.0 limitation of only allowing constant loop indices
// by breaking here. Sigma has an upper bound of 8, imposed on the Rust side.
if (i >= sampleCount) {
break;
}
color_sum += textureSample(image_texture, image_sampler, (vertex.fpos.xy - i * params.image_blur_filter_direction) / params.extent) * gaussian_coeff.x;
color_sum += textureSample(image_texture, image_sampler, (vertex.fpos.xy + i * params.image_blur_filter_direction) / params.extent) * gaussian_coeff.x;
coefficient_sum += 2.0 * gaussian_coeff.x;
// Compute the coefficients incrementally:
// https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-40-incremental-computation-gaussian
gaussian_coeff.x *= gaussian_coeff.y;
gaussian_coeff.y *= gaussian_coeff.z;
}
var color: vec4<f32> = color_sum / coefficient_sum;
if (params.tex_type == 1) { color = vec4<f32>(color.xyz * color.w, color.w); }
if (params.tex_type == 2) { color = vec4<f32>(color.x); }
return color;
}
File diff suppressed because it is too large. Load diff
+166
View File
@@ -0,0 +1,166 @@
#![allow(dead_code)]
#[derive(Debug)]
struct Node {
x: usize,
y: usize,
width: usize,
}
/// Represents an atlas for packing rectangles.
#[derive(Debug)]
pub struct Atlas {
width: usize,
height: usize,
nodes: Vec<Node>,
}
impl Atlas {
/// Creates a new atlas with the specified width and height.
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
nodes: vec![Node { x: 0, y: 0, width }],
}
}
/// Returns the size (width and height) of the atlas.
pub fn size(&self) -> (usize, usize) {
(self.width, self.height)
}
/// Expands the atlas to the specified width and height.
pub fn expand(&mut self, width: usize, height: usize) {
// Insert node for empty space
if width > self.width {
self.insert_node(self.nodes.len(), self.width, 0, width - self.width);
}
self.width = width;
self.height = height;
}
/// Resets the atlas to the specified width and height.
pub fn reset(&mut self, width: usize, height: usize) {
*self = Self::new(width, height);
}
/// Adds a rectangle with the specified width and height to the atlas.
///
/// Returns the position (x, y) of the added rectangle, or `None` if the rectangle cannot be added.
pub fn add_rect(&mut self, rect_width: usize, rect_height: usize) -> Option<(usize, usize)> {
let mut besth = self.height;
let mut bestw = self.width;
let mut besti = None;
let mut bestx = 0;
let mut besty = 0;
// Bottom left fit heuristic.
for i in 0..self.nodes.len() {
if let Some(y) = self.rect_fits(i, rect_width, rect_height) {
if y + rect_height < besth || (y + rect_height == besth && self.nodes[i].width < bestw) {
besti = Some(i);
bestw = self.nodes[i].width;
besth = y + rect_height;
bestx = self.nodes[i].x;
besty = y;
}
}
}
if let Some(besti) = besti {
// Perform the actual packing.
self.add_skyline_level(besti, bestx, besty, rect_width, rect_height);
return Some((bestx, besty));
}
None
}
fn insert_node(&mut self, idx: usize, x: usize, y: usize, width: usize) {
self.nodes.insert(idx, Node { x, y, width });
}
fn remove_node(&mut self, idx: usize) {
self.nodes.remove(idx);
}
fn add_skyline_level(&mut self, idx: usize, x: usize, y: usize, width: usize, height: usize) {
// Insert new node
self.insert_node(idx, x, y + height, width);
// Delete skyline segments that fall under the shadow of the new segment.
let mut i = idx + 1;
while i < self.nodes.len() {
if self.nodes[i].x < self.nodes[i - 1].x + self.nodes[i - 1].width {
let shrink = self.nodes[i - 1].x + self.nodes[i - 1].width - self.nodes[i].x;
self.nodes[i].x += shrink;
let new_width = self.nodes[i].width as isize - shrink as isize;
if new_width <= 0 {
self.remove_node(i);
i -= 1;
} else {
self.nodes[i].width = new_width as usize;
break;
}
} else {
break;
}
i += 1;
}
// Merge same height skyline segments that are next to each other.
let mut i = 0isize;
while i < self.nodes.len() as isize - 1 {
let index = i as usize;
if self.nodes[index].y == self.nodes[index + 1].y {
self.nodes[index].width += self.nodes[index + 1].width;
self.remove_node(index + 1);
i -= 1;
}
i += 1;
}
}
fn rect_fits(&self, mut idx: usize, width: usize, height: usize) -> Option<usize> {
// Checks if there is enough space at the location of skyline span 'i',
// and return the max height of all skyline spans under that at that location,
// (think tetris block being dropped at that position). Or -1 if no space found.
let x = self.nodes[idx].x;
let mut y = self.nodes[idx].y;
if x + width > self.width {
return None;
}
let mut space_left = width as isize;
while space_left > 0 {
if idx == self.nodes.len() {
return None;
}
y = y.max(self.nodes[idx].y);
if y + height > self.height {
return None;
}
space_left -= self.nodes[idx].width as isize;
idx += 1;
}
Some(y)
}
}
+665
View File
@@ -0,0 +1,665 @@
use fnv::FnvHashMap;
use std::cell::{Ref, RefCell};
use std::collections::hash_map::Entry;
use std::fmt;
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
use std::rc::Rc;
#[cfg(feature = "textlayout")]
use ttf_parser::{Face as TtfFont, GlyphId};
use crate::{paint::FontVariations, ErrorKind, Path};
/// Abstraction over the parsed font face, so callers don't need cfg blocks.
/// With `textlayout`, this wraps a `ttf_parser::Face`.
/// Otherwise, this is a zero-sized type.
#[cfg(feature = "textlayout")]
pub(crate) struct FontFaceRef<'a>(pub(crate) ttf_parser::Face<'a>);
#[cfg(not(feature = "textlayout"))]
pub(crate) struct FontFaceRef<'a>(std::marker::PhantomData<&'a ()>);
/// Information about a font variation axis (e.g. weight, width, optical size).
///
/// Returned by [`Canvas::font_variation_axes`](crate::Canvas::font_variation_axes)
/// in the order they appear in the font's OpenType `fvar` table. The position
/// of each axis in that vector determines the index of the corresponding
/// normalized coordinate (`i16` in F2DOT14 format) when calling
/// [`Canvas::fill_glyph_run`](crate::Canvas::fill_glyph_run) or
/// [`Canvas::stroke_glyph_run`](crate::Canvas::stroke_glyph_run).
#[derive(Clone, Debug)]
pub struct VariationAxisInfo {
/// Four-byte axis tag (e.g. `*b"wght"` for weight).
pub tag: [u8; 4],
/// Minimum value for this axis.
pub min_value: f32,
/// Default value for this axis.
pub def_value: f32,
/// Maximum value for this axis.
pub max_value: f32,
/// Name table ID for this axis's name.
pub name_id: u16,
/// Whether this axis is hidden from the user.
pub hidden: bool,
}
#[derive(Clone, Debug)]
pub struct GlyphMetrics {
pub width: f32,
pub height: f32,
pub bearing_x: f32,
pub bearing_y: f32,
}
#[derive(Debug)]
pub struct Glyph {
pub path: Option<Path>, // None means render as image
pub metrics: GlyphMetrics,
}
pub enum GlyphRendering<'a> {
RenderAsPath(Ref<'a, Path>),
#[cfg(feature = "image-loading")]
RenderAsImage(image::DynamicImage),
}
#[derive(Copy, Clone, Default, Debug)]
struct FontFlags(u8);
// TODO: underline, strikeout, subscript, superscript metrics
impl FontFlags {
fn new(regular: bool, italic: bool, bold: bool, oblique: bool, variable: bool) -> Self {
let mut flags = 0;
if regular {
flags |= 0x1;
}
if italic {
flags |= 0x2;
}
if bold {
flags |= 0x4;
}
if oblique {
flags |= 0x8;
}
if variable {
flags |= 0x10;
}
Self(flags)
}
#[inline]
fn regular(&self) -> bool {
self.0 & 0x1 > 0
}
#[inline]
fn italic(&self) -> bool {
self.0 & 0x2 > 0
}
#[inline]
fn bold(&self) -> bool {
self.0 & 0x4 > 0
}
#[inline]
fn oblique(&self) -> bool {
self.0 & 0x8 > 0
}
#[inline]
fn variable(&self) -> bool {
self.0 & 0x10 > 0
}
}
/// Information about a font.
#[derive(Copy, Clone, Default, Debug)]
pub struct FontMetrics {
ascender: f32,
descender: f32,
height: f32,
flags: FontFlags,
weight: u16,
width: u16,
}
impl FontMetrics {
fn scale(&mut self, scale: f32) {
self.ascender *= scale;
self.descender *= scale;
self.height *= scale;
}
/// Returns the distance from the baseline to the top of the highest glyph.
pub fn ascender(&self) -> f32 {
self.ascender
}
/// Returns the distance from the baseline to the bottom of the lowest descenders on the glyphs.
pub fn descender(&self) -> f32 {
self.descender
}
/// Returns the height of the font.
pub fn height(&self) -> f32 {
self.height.round()
}
/// Returns if the font is regular.
pub fn regular(&self) -> bool {
self.flags.regular()
}
/// Returns if the font is italic.
pub fn italic(&self) -> bool {
self.flags.italic()
}
/// Returns if the font is bold.
pub fn bold(&self) -> bool {
self.flags.bold()
}
/// Returns if the font is oblique.
pub fn oblique(&self) -> bool {
self.flags.oblique()
}
/// Returns if the font is a variable font.
pub fn variable(&self) -> bool {
self.flags.variable()
}
/// Returns the weight of the font.
pub fn weight(&self) -> u16 {
self.weight
}
/// Returns the width of the font.
pub fn width(&self) -> u16 {
self.width
}
}
impl fmt::Debug for Font {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Font")
.field("data", &format_args!(".."))
.field("face_index", &self.face_index)
.field("units_per_em", &self.units_per_em)
.field("metrics", &self.metrics)
.field("glyphs", &self.glyphs)
.finish()
}
}
/// Key for the glyph cache: (glyph_id, normalized_coords_hash).
/// The hash covers all normalized variation coordinates, or 0 for defaults.
type GlyphCacheKey = (u16, u64);
pub struct Font {
data: Box<dyn AsRef<[u8]>>,
face_index: u32,
units_per_em: u16,
metrics: FontMetrics,
glyphs: RefCell<FnvHashMap<GlyphCacheKey, Glyph>>,
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
swash_scale_context: Rc<RefCell<swash::scale::ScaleContext>>,
}
impl Font {
#[cfg(feature = "textlayout")]
pub fn new_with_data<T: AsRef<[u8]> + 'static>(
data: T,
face_index: u32,
_text_context: &super::TextContextImpl,
) -> Result<Self, ErrorKind> {
let ttf_font = TtfFont::parse(data.as_ref(), face_index).map_err(|_| ErrorKind::FontParseError)?;
let units_per_em = ttf_font.units_per_em();
let metrics = FontMetrics {
ascender: ttf_font.ascender() as f32,
descender: ttf_font.descender() as f32,
height: ttf_font.height() as f32,
flags: FontFlags::new(
ttf_font.is_regular(),
ttf_font.is_italic(),
ttf_font.is_bold(),
ttf_font.is_oblique(),
ttf_font.is_variable(),
),
width: ttf_font.width().to_number(),
weight: ttf_font.weight().to_number(),
};
Ok(Self {
data: Box::new(data),
face_index,
units_per_em,
metrics,
glyphs: RefCell::default(),
})
}
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
pub fn new_with_data<T: AsRef<[u8]> + 'static>(
data: T,
face_index: u32,
text_context: &super::TextContextImpl,
) -> Result<Self, ErrorKind> {
let font_ref =
swash::FontRef::from_index(data.as_ref(), face_index as usize).ok_or(ErrorKind::FontParseError)?;
let swash_metrics = font_ref.metrics(&[]);
let attrs = font_ref.attributes();
let units_per_em = swash_metrics.units_per_em;
let weight = attrs.weight().0;
let stretch = attrs.stretch();
let style = attrs.style();
let is_bold = weight >= 700;
let is_italic = matches!(style, swash::Style::Italic);
let is_oblique = matches!(style, swash::Style::Oblique(_));
let is_variable = font_ref.variations().next().is_some();
let is_regular = weight == 400 && matches!(style, swash::Style::Normal) && stretch == swash::Stretch::NORMAL;
// Map swash::Stretch to usWidthClass values as per
// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
let width: u16 = match stretch {
swash::Stretch::ULTRA_CONDENSED => 1,
swash::Stretch::EXTRA_CONDENSED => 2,
swash::Stretch::CONDENSED => 3,
swash::Stretch::SEMI_CONDENSED => 4,
swash::Stretch::NORMAL => 5,
swash::Stretch::SEMI_EXPANDED => 6,
swash::Stretch::EXPANDED => 7,
swash::Stretch::EXTRA_EXPANDED => 8,
swash::Stretch::ULTRA_EXPANDED => 9,
_ => 5,
};
let metrics = FontMetrics {
ascender: swash_metrics.ascent,
descender: -swash_metrics.descent,
// swash ascent and descent are both positive (distance from baseline),
// unlike ttf-parser where descent is negative, so this is a sum not a difference.
height: swash_metrics.ascent + swash_metrics.descent + swash_metrics.leading,
flags: FontFlags::new(is_regular, is_italic, is_bold, is_oblique, is_variable),
weight,
width,
};
Ok(Self {
data: Box::new(data),
face_index,
units_per_em,
metrics,
glyphs: RefCell::default(),
swash_scale_context: text_context.swash_scale_context(),
})
}
#[cfg(not(any(feature = "textlayout", feature = "swash")))]
pub fn new_with_data<T: AsRef<[u8]> + 'static>(
_data: T,
_face_index: u32,
_text_context: &super::TextContextImpl,
) -> Result<Self, ErrorKind> {
Err(ErrorKind::FontParseError)
}
#[allow(dead_code)]
pub fn data(&self) -> &[u8] {
(*self.data).as_ref()
}
#[allow(dead_code)]
pub fn face_index(&self) -> u32 {
self.face_index
}
#[cfg(feature = "textlayout")]
pub(crate) fn face_ref(&self) -> FontFaceRef<'_> {
FontFaceRef(ttf_parser::Face::parse(self.data.as_ref().as_ref(), self.face_index).unwrap())
}
#[cfg(not(feature = "textlayout"))]
pub(crate) fn face_ref(&self) -> FontFaceRef<'_> {
FontFaceRef(std::marker::PhantomData)
}
#[cfg(feature = "swash")]
pub(crate) fn swash_font_ref(&self) -> Option<swash::FontRef<'_>> {
swash::FontRef::from_index(self.data.as_ref().as_ref(), self.face_index as usize)
}
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
fn swash_scale_context(&self) -> &RefCell<swash::scale::ScaleContext> {
&self.swash_scale_context
}
#[cfg(feature = "textlayout")]
pub(crate) fn face_ref_with_variations(&self, variations: &FontVariations) -> FontFaceRef<'_> {
let mut face = self.face_ref();
for (tag, value) in variations.iter() {
face.0.set_variation(ttf_parser::Tag(tag), value);
}
face
}
#[cfg(not(feature = "textlayout"))]
#[allow(dead_code)]
pub(crate) fn face_ref_with_variations(&self, _variations: &FontVariations) -> FontFaceRef<'_> {
// Without ttf-parser, face is a phantom type. Variations are applied
// through the swash scaler in the glyph() method instead.
self.face_ref()
}
#[cfg(feature = "textlayout")]
pub(crate) fn face_ref_with_normalized_coords(&self, coords: &[i16]) -> FontFaceRef<'_> {
let mut face = self.face_ref();
// Round-trip: convert normalized coords back to design-space values
// so ttf-parser can re-normalize them (including avar remapping).
// Coords are zipped with axes in order — the i-th coord maps to the i-th axis.
for (axis, coord) in face.0.variation_axes().into_iter().zip(coords.iter()) {
let value = *coord as f32 / 16384.0;
let design_value = if value >= 0.0 {
axis.def_value + value * (axis.max_value - axis.def_value)
} else {
axis.def_value + value * (axis.def_value - axis.min_value)
};
face.0.set_variation(axis.tag, design_value);
}
face
}
#[cfg(not(feature = "textlayout"))]
pub(crate) fn face_ref_with_normalized_coords(&self, _coords: &[i16]) -> FontFaceRef<'_> {
self.face_ref()
}
pub fn metrics(&self, size: f32) -> FontMetrics {
let mut metrics = self.metrics;
metrics.scale(self.scale(size));
metrics
}
#[cfg(feature = "textlayout")]
pub fn metrics_with_variations(&self, size: f32, variations: &FontVariations) -> FontMetrics {
if variations.is_empty() {
return self.metrics(size);
}
let face = self.face_ref_with_variations(variations);
let mut metrics = FontMetrics {
ascender: face.0.ascender() as f32,
descender: face.0.descender() as f32,
height: face.0.height() as f32,
..self.metrics
};
metrics.scale(self.scale(size));
metrics
}
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
pub fn metrics_with_variations(&self, size: f32, variations: &FontVariations) -> FontMetrics {
if variations.is_empty() {
return self.metrics(size);
}
let Some(font_ref) = self.swash_font_ref() else {
return self.metrics(size);
};
let settings: Vec<swash::Setting<f32>> = variations
.iter()
.map(|(tag, value)| swash::Setting {
tag: swash::tag_from_bytes(&tag.to_be_bytes()),
value,
})
.collect();
let coords: Vec<swash::NormalizedCoord> = font_ref.variations().normalized_coords(settings).collect();
let swash_metrics = font_ref.metrics(&coords);
let mut metrics = FontMetrics {
ascender: swash_metrics.ascent,
descender: -swash_metrics.descent,
height: swash_metrics.ascent + swash_metrics.descent + swash_metrics.leading,
..self.metrics
};
metrics.scale(self.scale(size));
metrics
}
#[cfg(not(any(feature = "textlayout", feature = "swash")))]
pub fn metrics_with_variations(&self, size: f32, _variations: &FontVariations) -> FontMetrics {
self.metrics(size)
}
pub fn scale(&self, size: f32) -> f32 {
size / self.units_per_em as f32
}
#[cfg(feature = "textlayout")]
pub fn variation_axes(&self) -> Vec<VariationAxisInfo> {
let face = self.face_ref();
let mut axes = Vec::new();
if let Some(table) = face.0.tables().fvar {
for axis in table.axes {
axes.push(VariationAxisInfo {
tag: axis.tag.to_bytes(),
min_value: axis.min_value,
def_value: axis.def_value,
max_value: axis.max_value,
name_id: axis.name_id,
hidden: axis.hidden,
});
}
}
axes
}
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
pub fn variation_axes(&self) -> Vec<VariationAxisInfo> {
let Some(font_ref) = self.swash_font_ref() else {
return Vec::new();
};
font_ref
.variations()
.map(|v| {
let name_id = match v.name_id() {
swash::StringId::Other(id) => id,
_ => 0,
};
VariationAxisInfo {
tag: v.tag().to_be_bytes(),
min_value: v.min_value(),
def_value: v.default_value(),
max_value: v.max_value(),
name_id,
hidden: v.is_hidden(),
}
})
.collect()
}
#[cfg(not(any(feature = "textlayout", feature = "swash")))]
pub fn variation_axes(&self) -> Vec<VariationAxisInfo> {
Vec::new()
}
#[cfg(feature = "textlayout")]
pub(crate) fn normalize_variations(&self, variations: &FontVariations) -> Vec<i16> {
let face = self.face_ref_with_variations(variations);
face.0.variation_coordinates().iter().map(|c| c.get()).collect()
}
#[allow(dead_code)]
fn hash_normalized_coords(coords: &[i16]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = fnv::FnvHasher::default();
coords.hash(&mut hasher);
hasher.finish()
}
#[cfg(feature = "textlayout")]
pub(crate) fn glyph(
&self,
face: &FontFaceRef<'_>,
codepoint: u16,
normalized_coords: &[i16],
) -> Option<Ref<'_, Glyph>> {
let cache_key: GlyphCacheKey = (codepoint, Self::hash_normalized_coords(normalized_coords));
if let Entry::Vacant(entry) = self.glyphs.borrow_mut().entry(cache_key) {
let mut path = Path::new();
let id = GlyphId(codepoint);
let maybe_glyph = if let Some(image) = face
.0
.glyph_raster_image(id, u16::MAX)
.filter(|img| img.format == ttf_parser::RasterImageFormat::PNG)
{
let scale = if image.pixels_per_em != 0 {
self.units_per_em as f32 / image.pixels_per_em as f32
} else {
1.0
};
Some(Glyph {
path: None,
metrics: GlyphMetrics {
width: image.width as f32 * scale,
height: image.height as f32 * scale,
bearing_x: image.x as f32 * scale,
bearing_y: (image.y as f32 + image.height as f32) * scale,
},
})
} else {
face.0.outline_glyph(id, &mut path).map(|bbox| Glyph {
path: Some(path),
metrics: GlyphMetrics {
width: bbox.width() as f32,
height: bbox.height() as f32,
bearing_x: bbox.x_min as f32,
bearing_y: bbox.y_max as f32,
},
})
};
if let Some(glyph) = maybe_glyph {
entry.insert(glyph);
}
}
Ref::filter_map(self.glyphs.borrow(), |glyphs| glyphs.get(&cache_key)).ok()
}
#[cfg(not(any(feature = "textlayout", feature = "swash")))]
pub(crate) fn glyph(
&self,
_face: &FontFaceRef<'_>,
_codepoint: u16,
_normalized_coords: &[i16],
) -> Option<Ref<'_, Glyph>> {
None
}
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
pub(crate) fn glyph(
&self,
_face: &FontFaceRef<'_>,
codepoint: u16,
normalized_coords: &[i16],
) -> Option<Ref<'_, Glyph>> {
let cache_key: GlyphCacheKey = (codepoint, Self::hash_normalized_coords(normalized_coords));
if let Entry::Vacant(entry) = self.glyphs.borrow_mut().entry(cache_key) {
let font_ref = self.swash_font_ref()?;
let mut scale_context = self.swash_scale_context().borrow_mut();
let scaler_builder = scale_context
.builder(font_ref)
.size(self.units_per_em as f32)
.hint(false)
.normalized_coords(normalized_coords);
let mut scaler = scaler_builder.build();
let maybe_glyph = if let Some(outline) = scaler.scale_outline(codepoint) {
use swash::zeno::{Command, PathData};
let bounds = outline.bounds();
let mut path = Path::new();
for cmd in outline.path().commands() {
match cmd {
Command::MoveTo(p) => path.move_to(p.x, p.y),
Command::LineTo(p) => path.line_to(p.x, p.y),
Command::QuadTo(c, p) => path.quad_to(c.x, c.y, p.x, p.y),
Command::CurveTo(c1, c2, p) => path.bezier_to(c1.x, c1.y, c2.x, c2.y, p.x, p.y),
Command::Close => path.close(),
}
}
Some(Glyph {
path: Some(path),
metrics: GlyphMetrics {
width: bounds.width(),
height: bounds.height(),
bearing_x: bounds.min.x,
bearing_y: bounds.max.y,
},
})
} else {
None
};
if let Some(glyph) = maybe_glyph {
entry.insert(glyph);
}
}
Ref::filter_map(self.glyphs.borrow(), |glyphs| glyphs.get(&cache_key)).ok()
}
#[cfg(feature = "textlayout")]
pub(crate) fn glyph_rendering_representation(
&self,
face: &FontFaceRef<'_>,
codepoint: u16,
#[allow(unused_variables)] pixels_per_em: u16,
normalized_coords: &[i16],
) -> Option<GlyphRendering<'_>> {
#[cfg(feature = "image-loading")]
if let Some(image) =
face.0
.glyph_raster_image(GlyphId(codepoint), pixels_per_em)
.and_then(|raster_glyph_image| {
image::load_from_memory_with_format(raster_glyph_image.data, image::ImageFormat::Png).ok()
})
{
return Some(GlyphRendering::RenderAsImage(image));
}
self.glyph(face, codepoint, normalized_coords).and_then(|glyph| {
Ref::filter_map(glyph, |glyph| glyph.path.as_ref())
.ok()
.map(GlyphRendering::RenderAsPath)
})
}
#[cfg(not(feature = "textlayout"))]
pub(crate) fn glyph_rendering_representation(
&self,
_face: &FontFaceRef<'_>,
codepoint: u16,
#[allow(unused_variables)] _pixels_per_em: u16,
normalized_coords: &[i16],
) -> Option<GlyphRendering<'_>> {
self.glyph(_face, codepoint, normalized_coords).and_then(|glyph| {
Ref::filter_map(glyph, |glyph| glyph.path.as_ref())
.ok()
.map(GlyphRendering::RenderAsPath)
})
}
}
+558
View File
@@ -0,0 +1,558 @@
use std::{
hash::{Hash, Hasher},
ops::Range,
};
use fnv::FnvHasher;
use lru::LruCache;
use crate::{
paint::{FontVariations, TextSettings},
Align, Baseline, ErrorKind, FontId, Paint,
};
use unicode_bidi::BidiInfo;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Copy, Clone, Debug)]
pub struct ShapedGlyph {
pub x: f32,
pub y: f32,
pub c: char,
pub byte_index: usize,
pub font_id: FontId,
pub glyph_id: u16,
pub width: f32,
pub height: f32,
pub advance_x: f32,
pub advance_y: f32,
pub offset_x: f32,
pub offset_y: f32,
}
#[derive(Clone, Debug, Default)]
pub(super) struct ShapedWord {
glyphs: Vec<ShapedGlyph>,
width: f32,
}
#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub(super) struct ShapingId {
size: u32,
word_hash: u64,
font_ids: [Option<FontId>; 8],
variation_hash: u64,
}
impl ShapingId {
fn new(
font_size: f32,
font_ids: [Option<FontId>; 8],
word: &str,
max_width: Option<f32>,
variations: &FontVariations,
) -> Self {
let mut hasher = FnvHasher::default();
word.hash(&mut hasher);
if let Some(max_width) = max_width {
(max_width.trunc() as i32).hash(&mut hasher);
}
Self {
size: (font_size * 10.0).trunc() as u32,
word_hash: hasher.finish(),
font_ids,
variation_hash: variations.hash(),
}
}
}
pub(super) type ShapedWordsCache<H> = LruCache<ShapingId, Result<ShapedWord, ErrorKind>, H>;
pub(super) type ShapingRunCache<H> = LruCache<ShapingId, TextMetrics, H>;
impl super::TextContext {
/// Returns information on how the provided text will be drawn with the specified paint.
pub fn measure_text<S: AsRef<str>>(
&self,
x: f32,
y: f32,
text: S,
paint: &Paint,
) -> Result<TextMetrics, ErrorKind> {
self.0.borrow_mut().measure_text(x, y, text, &paint.text)
}
/// Returns the maximum index-th byte of text that will fit inside `max_width`.
///
/// The retuned index will always lie at the start and/or end of a UTF-8 code point sequence or at the start or end of the text
pub fn break_text<S: AsRef<str>>(&self, max_width: f32, text: S, paint: &Paint) -> Result<usize, ErrorKind> {
self.0.borrow_mut().break_text(max_width, text, &paint.text)
}
/// Returnes a list of ranges representing each line of text that will fit inside `max_width`
pub fn break_text_vec<S: AsRef<str>>(
&self,
max_width: f32,
text: S,
paint: &Paint,
) -> Result<Vec<Range<usize>>, ErrorKind> {
self.0.borrow_mut().break_text_vec(max_width, text, &paint.text)
}
/// Adjusts the capacity of the shaping run cache. This is a cache for measurements of whole
/// strings.
pub fn resize_shaping_run_cache(&self, capacity: std::num::NonZeroUsize) {
self.0.borrow_mut().resize_shaping_run_cache(capacity)
}
/// Adjusts the capacity of the shaped words cache. This is a cache for measurements of
/// individual words. Words are separated by
/// [UAX#29 word boundaries](http://www.unicode.org/reports/tr29/#Word_Boundaries).
pub fn resize_shaped_words_cache(&self, capacity: std::num::NonZeroUsize) {
self.0.borrow_mut().resize_shaped_words_cache(capacity)
}
}
impl super::TextContextImpl {
pub fn resize_shaping_run_cache(&mut self, capacity: std::num::NonZeroUsize) {
self.shaping_run_cache.resize(capacity);
}
pub fn resize_shaped_words_cache(&mut self, capacity: std::num::NonZeroUsize) {
self.shaped_words_cache.resize(capacity);
}
pub fn measure_text<S: AsRef<str>>(
&mut self,
x: f32,
y: f32,
text: S,
text_settings: &TextSettings,
) -> Result<TextMetrics, ErrorKind> {
shape(x, y, self, text_settings, text.as_ref(), None)
}
pub fn break_text<S: AsRef<str>>(
&mut self,
max_width: f32,
text: S,
text_settings: &TextSettings,
) -> Result<usize, ErrorKind> {
let layout = shape(0.0, 0.0, self, text_settings, text.as_ref(), Some(max_width))?;
Ok(layout.final_byte_index)
}
pub fn break_text_vec<S: AsRef<str>>(
&mut self,
max_width: f32,
text: S,
text_settings: &TextSettings,
) -> Result<Vec<Range<usize>>, ErrorKind> {
let text = text.as_ref();
let mut res = Vec::new();
let mut start = 0;
while start < text.len() {
let Ok(index) = self.break_text(max_width, &text[start..], text_settings) else {
break;
};
if index == 0 {
break;
}
let index = start + index;
res.push(start..index);
start += &text[start..index].len();
}
Ok(res)
}
}
/// Represents the result of a text shaping run.
#[derive(Clone, Default, Debug)]
pub struct TextMetrics {
/// X-coordinate of the starting position for the shaped text.
pub x: f32,
/// Y-coordinate of the starting position for the shaped text.
pub y: f32,
width: f32,
height: f32,
/// Vector of shaped glyphs resulting from the text shaping run.
pub glyphs: Vec<ShapedGlyph>,
pub(crate) final_byte_index: usize,
}
impl TextMetrics {
pub(crate) fn scale(&mut self, scale: f32) {
self.x *= scale;
self.y *= scale;
self.width *= scale;
self.height *= scale;
for glyph in &mut self.glyphs {
glyph.x *= scale;
glyph.y *= scale;
glyph.width *= scale;
glyph.height *= scale;
}
}
/// width of the glyphs as drawn
pub fn width(&self) -> f32 {
self.width
}
/// height of the glyphs as drawn
pub fn height(&self) -> f32 {
self.height
}
}
// Shaper
pub fn shape(
x: f32,
y: f32,
context: &mut super::TextContextImpl,
text_settings: &TextSettings,
text: &str,
max_width: Option<f32>,
) -> Result<TextMetrics, ErrorKind> {
let variations = &text_settings.font_variations;
let id = ShapingId::new(
text_settings.font_size,
text_settings.font_ids,
text,
max_width,
variations,
);
if !context.shaping_run_cache.contains(&id) {
let metrics = shape_run(
context,
text_settings.font_size,
text_settings.font_ids,
text_settings.letter_spacing,
text,
max_width,
variations,
);
context.shaping_run_cache.put(id, metrics);
}
if let Some(mut metrics) = context.shaping_run_cache.get(&id).cloned() {
layout(x, y, context, &mut metrics, text_settings)?;
return Ok(metrics);
}
Err(ErrorKind::UnknownError)
}
fn shape_run(
context: &mut super::TextContextImpl,
font_size: f32,
font_ids: [Option<FontId>; 8],
letter_spacing: f32,
text: &str,
max_width: Option<f32>,
variations: &FontVariations,
) -> TextMetrics {
let mut result = TextMetrics {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
glyphs: Vec::with_capacity(text.len()),
final_byte_index: 0,
};
let bidi_info = BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
// this controls whether we should break within words
let mut first_word_in_paragraph = true;
let Some(paragraph) = bidi_info.paragraphs.first() else {
return result;
};
let line = paragraph.range.clone();
let (levels, runs) = bidi_info.visual_runs(paragraph, line);
for run in runs {
let sub_text = &text[run.clone()];
if sub_text.is_empty() {
continue;
}
let hb_direction = if levels[run.start].is_rtl() {
rustybuzz::Direction::RightToLeft
} else {
rustybuzz::Direction::LeftToRight
};
let mut words = Vec::new();
let mut word_break_reached = false;
let mut byte_index = run.start;
for mut word_txt in sub_text.split_word_bounds() {
let id = ShapingId::new(font_size, font_ids, word_txt, max_width, variations);
if !context.shaped_words_cache.contains(&id) {
let word = shape_word(
word_txt,
hb_direction,
context,
font_size,
&font_ids,
letter_spacing,
variations,
);
context.shaped_words_cache.put(id, word);
}
if let Some(Ok(word)) = context.shaped_words_cache.get(&id) {
let mut word = word.clone();
if let Some(max_width) = max_width {
if result.width + word.width >= max_width {
word_break_reached = true;
if first_word_in_paragraph {
// search for the largest prefix of the word that can fit
let mut bytes_included = 0;
let mut subword_width = 0.0;
let target_width = max_width - result.width;
for glyph in word.glyphs {
bytes_included = glyph.byte_index;
let glyph_width = glyph.advance_x + letter_spacing;
// nuance: we want to include the first glyph even if it breaks
// the bounds. this is to allow pathologically small bounds to
// at least complete rendering
if subword_width + glyph_width >= target_width && bytes_included != 0 {
break;
}
subword_width += glyph_width;
}
if bytes_included == 0 {
// just in case - never mind!
break;
}
let subword_txt = &word_txt[..bytes_included];
let id = ShapingId::new(font_size, font_ids, subword_txt, Some(max_width), variations);
if !context.shaped_words_cache.contains(&id) {
let subword = shape_word(
subword_txt,
hb_direction,
context,
font_size,
&font_ids,
letter_spacing,
variations,
);
context.shaped_words_cache.put(id, subword);
}
if let Some(Ok(subword)) = context.shaped_words_cache.get(&id) {
// replace the outer variables so we can continue normally
word = subword.clone();
word_txt = subword_txt;
} else {
break;
}
} else if word.glyphs.iter().all(|g| g.c.is_whitespace()) {
// the last word we've broken in the middle of is whitespace.
// include this word for now, but we will discard its metrics in a moment.
} else {
// we are not breaking up words - discard this word
break;
}
}
}
// if we have broken in the middle of whitespace, do not include this word in metrics
if !word_break_reached || !word.glyphs.iter().all(|g| g.c.is_whitespace()) {
result.width += word.width;
}
for glyph in &mut word.glyphs {
glyph.byte_index += byte_index;
debug_assert!(text.get(glyph.byte_index..).is_some());
}
words.push(word);
first_word_in_paragraph = false;
}
byte_index += word_txt.len();
if word_break_reached {
break;
}
}
if levels[run.start].is_rtl() {
words.reverse();
}
for word in words {
result.glyphs.extend(word.glyphs.clone());
}
result.final_byte_index = byte_index;
if word_break_reached {
break;
}
}
result
}
fn shape_word(
word: &str,
hb_direction: rustybuzz::Direction,
context: &mut super::TextContextImpl,
font_size: f32,
font_ids: &[Option<FontId>; 8],
letter_spacing: f32,
variations: &FontVariations,
) -> Result<ShapedWord, ErrorKind> {
// find_font will call the closure with each font matching the provided style
// until a font capable of shaping the word is found
context.find_font(font_ids, |(font_id, font)| {
let font_face = font.face_ref_with_variations(variations);
let normalized_coords = font.normalize_variations(variations);
let face = rustybuzz::Face::from_face(font_face.0.clone());
// Call harfbuzz
let output = {
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(word);
buffer.set_direction(hb_direction);
rustybuzz::shape(&face, &[], buffer)
};
let positions = output.glyph_positions();
let infos = output.glyph_infos();
let mut shaped_word = ShapedWord {
glyphs: Vec::with_capacity(positions.len()),
width: 0.0,
};
let mut has_missing = false;
for (position, (info, c)) in positions.iter().zip(infos.iter().zip(word.chars())) {
if info.glyph_id == 0 {
has_missing = true;
}
let scale = font.scale(font_size);
let mut g = ShapedGlyph {
x: 0.0,
y: 0.0,
c,
byte_index: info.cluster as usize,
font_id,
glyph_id: info
.glyph_id
.try_into()
.expect("rustybuzz guarantees the output glyph id is u16"),
width: 0.0,
height: 0.0,
advance_x: position.x_advance as f32 * scale,
advance_y: position.y_advance as f32 * scale,
offset_x: position.x_offset as f32 * scale,
offset_y: position.y_offset as f32 * scale,
};
if let Some(glyph) = font.glyph(&font_face, g.glyph_id, &normalized_coords) {
g.width = glyph.metrics.width * scale;
g.height = glyph.metrics.height * scale;
}
shaped_word.width += g.advance_x + letter_spacing;
shaped_word.glyphs.push(g);
}
(has_missing, shaped_word)
})
}
// Calculates the x,y coordinates for each glyph based on their advances. Calculates total width and height of the shaped text run
fn layout(
x: f32,
y: f32,
context: &mut super::TextContextImpl,
res: &mut TextMetrics,
text_settings: &TextSettings,
) -> Result<(), ErrorKind> {
let mut cursor_x = x;
let mut cursor_y = y;
// Horizontal alignment
match text_settings.text_align {
Align::Center => cursor_x -= res.width / 2.0,
Align::Right => cursor_x -= res.width,
Align::Left => (),
}
res.x = cursor_x;
let mut min_y = cursor_y;
let mut max_y = cursor_y;
let mut ascender: f32 = 0.;
let mut descender: f32 = 0.;
for glyph in &mut res.glyphs {
let font = context.font_mut(glyph.font_id).ok_or(ErrorKind::NoFontFound)?;
let metrics = font.metrics(text_settings.font_size);
ascender = ascender.max(metrics.ascender());
descender = descender.min(metrics.descender());
}
let primary_metrics = context.find_font(&text_settings.font_ids, |(_, font)| {
(false, font.metrics(text_settings.font_size))
})?;
if ascender.abs() < f32::EPSILON {
ascender = primary_metrics.ascender();
}
if descender.abs() < f32::EPSILON {
descender = primary_metrics.descender();
}
// Baseline alignment
let alignment_offset_y = match text_settings.text_baseline {
Baseline::Top => ascender,
Baseline::Middle => ascender.midpoint(descender),
Baseline::Alphabetic => 0.0,
Baseline::Bottom => descender,
};
for glyph in &mut res.glyphs {
glyph.x = cursor_x + glyph.offset_x;
glyph.y = (cursor_y + alignment_offset_y).round() + glyph.offset_y;
min_y = min_y.min(glyph.y);
max_y = max_y.max(glyph.y + glyph.height);
cursor_x += glyph.advance_x + text_settings.letter_spacing;
cursor_y += glyph.advance_y;
}
res.y = min_y;
res.height = max_y - min_y;
Ok(())
}
+450
View File
@@ -0,0 +1,450 @@
use femtovg::{renderer::Void, Baseline, Canvas, Color, FillRule, Paint, Path, Solidity};
#[test]
fn path_with_single_move_to() {
let mut canvas = Canvas::new(Void).unwrap();
let mut path = Path::new();
path.move_to(10.0, 10.0);
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
}
#[test]
fn path_with_two_lines() {
let mut canvas = Canvas::new(Void).unwrap();
let mut path = Path::new();
path.line_to(10.0, 10.0);
path.line_to(10.0, 10.0);
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
}
#[test]
fn path_with_close_points() {
let mut canvas = Canvas::new(Void).unwrap();
let mut path = Path::new();
path.move_to(10.0, 10.0);
path.line_to(10.0001, 10.0);
path.line_to(10.0001, 10.000001);
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
}
#[test]
fn path_with_points_at_limits() {
let mut canvas = Canvas::new(Void).unwrap();
let mut path = Path::new();
path.move_to(10.0, 10.0);
path.line_to(f32::MAX, f32::MAX);
path.quad_to(10.0, 10.0, -f32::MAX, f32::MAX);
path.bezier_to(10.0, 10.0, f32::MAX, 5000.0, -f32::MAX, -f32::MAX);
path.rounded_rect_varying(
-f32::MAX,
-f32::MAX,
f32::MAX,
f32::MAX,
f32::MAX,
f32::MAX,
f32::MAX,
f32::MAX,
);
path.close();
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
}
#[test]
fn path_with_points_around_zero() {
let mut canvas = Canvas::new(Void).unwrap();
let mut path = Path::new();
path.move_to(0.0, 0.0);
path.line_to(0.0, 0.0);
path.line_to(0.0001, 0.0000003);
path.quad_to(0.002, 0.0001, -0.002, 0.0001);
path.bezier_to(0.0001, 0.002, -0.002, 0.0001, -0.002, 0.0001);
path.rounded_rect_varying(
-f32::MAX,
-f32::MAX,
f32::MAX,
f32::MAX,
f32::MAX,
0.0001,
0.0001,
0.0001,
);
path.close();
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
}
#[test]
fn degenerate_stroke() {
let mut canvas = Canvas::new(Void).unwrap();
let mut path = Path::new();
path.move_to(0.5, 0.5);
path.line_to(2., 2.);
path.line_to(2., 2.);
path.line_to(4., 2.);
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
}
#[test]
fn degenerate_arc_to() {
let mut canvas = Canvas::new(Void).unwrap();
let mut path = Path::new();
path.move_to(10.0, 10.0);
path.arc_to(10.0, 10.0001, 10.0, 10.0001, 2.0);
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
}
#[test]
fn degenerate_arc() {
let mut canvas = Canvas::new(Void).unwrap();
let mut path = Path::new();
path.move_to(10.0, 10.0);
path.arc(10.0, 10.0, 10.0, 0.0, f32::MAX, Solidity::Hole);
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
}
#[test]
fn path_contains_point() {
let mut canvas = Canvas::new(Void).unwrap();
// without setting size contains_point will early out on the bounds check and report false
canvas.set_size(100, 100, 1.0);
// Star - cancave & self crossing
let mut path = Path::new();
path.move_to(50.0, 0.0);
path.line_to(21.0, 90.0);
path.line_to(98.0, 35.0);
path.line_to(2.0, 35.0);
path.line_to(79.0, 90.0);
path.close();
// Center of the star should be hollow for even-odd rule
assert!(!canvas.contains_point(&path, 50.0, 45.0, FillRule::EvenOdd));
assert!(canvas.contains_point(&path, 50.0, 5.0, FillRule::EvenOdd));
// Center of the star should be fill for NonZero rule
assert!(canvas.contains_point(&path, 50.0, 45.0, FillRule::NonZero));
assert!(canvas.contains_point(&path, 50.0, 5.0, FillRule::NonZero));
}
#[test]
fn text_location_respects_scale() {
let mut canvas = Canvas::new(Void).unwrap();
canvas
.add_font("examples/assets/RobotoFlex-VariableFont.ttf")
.expect("Font not found");
let paint = Paint::color(Color::black()).with_text_baseline(Baseline::Top);
canvas.scale(5.0, 5.0);
let res = canvas.measure_text(100.0, 100.0, "Hello", &paint).unwrap();
assert_eq!(res.x, 100.0);
assert_eq!(res.y, 100.0);
}
#[test]
fn text_measure_without_canvas() {
let text_context = femtovg::TextContext::default();
let font_id = text_context
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
.expect("Font not found");
let test_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
let metrics = text_context
.measure_text(0., 0., "Hello World", &test_paint)
.expect("text shaping failed unexpectedly");
assert_eq!(metrics.width().ceil(), 83.);
assert_eq!(metrics.height().ceil(), 13.);
}
#[test]
fn font_measure_without_canvas() {
let text_context = femtovg::TextContext::default();
let font_id = text_context
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
.expect("Font not found");
let test_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
let metrics = text_context
.measure_font(&test_paint)
.expect("font measuring failed unexpectedly");
assert_eq!(metrics.ascender().ceil(), 15.);
}
#[test]
fn break_text_without_canvas() {
let text_context = femtovg::TextContext::default();
let font_id = text_context
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
.expect("Font not found");
let test_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
let text = "Multiple Lines Broken";
let breaks = text_context
.break_text_vec(60., text, &test_paint)
.expect("text shaping failed unexpectedly");
assert_eq!(
breaks
.iter()
.map(|range| &text[range.start..range.end])
.collect::<Vec<_>>(),
vec!["Multiple ", "Lines ", "Broken"]
);
}
#[test]
fn variable_font_weight_affects_measurement() {
let text_context = femtovg::TextContext::default();
let font_id = text_context
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
.expect("Font not found");
// Verify the font has a wght axis
let axes = text_context.font_variation_axes(font_id).unwrap();
let wght_axis = axes.iter().find(|a| &a.tag == b"wght");
assert!(wght_axis.is_some(), "Font should have a wght axis");
let light_paint = femtovg::Paint::default()
.with_font(&[font_id])
.with_font_size(16.)
.with_font_weight(femtovg::Paint::FONT_WEIGHT_LIGHT);
let bold_paint = femtovg::Paint::default()
.with_font(&[font_id])
.with_font_size(16.)
.with_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
let light_metrics = text_context
.measure_text(0., 0., "Hello World", &light_paint)
.expect("text shaping failed");
let bold_metrics = text_context
.measure_text(0., 0., "Hello World", &bold_paint)
.expect("text shaping failed");
// Bold text should be wider than light text
assert!(
bold_metrics.width() > light_metrics.width(),
"Bold ({}) should be wider than light ({})",
bold_metrics.width(),
light_metrics.width()
);
}
#[test]
fn font_variation_generic_api_matches_named_weight() {
let text_context = femtovg::TextContext::default();
let font_id = text_context
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
.expect("Font not found");
// Using the generic variation API for wght should produce the same measurement
// as the named font_weight API
let named_paint = femtovg::Paint::default()
.with_font(&[font_id])
.with_font_size(16.)
.with_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
let generic_paint = femtovg::Paint::default()
.with_font(&[font_id])
.with_font_size(16.)
.with_font_variation(b"wght", 700.0);
let named_metrics = text_context
.measure_text(0., 0., "Hello World", &named_paint)
.expect("text shaping failed");
let generic_metrics = text_context
.measure_text(0., 0., "Hello World", &generic_paint)
.expect("text shaping failed");
assert!(
(named_metrics.width() - generic_metrics.width()).abs() < f32::EPSILON,
"Named weight API ({}) and generic variation API ({}) should produce identical measurements",
named_metrics.width(),
generic_metrics.width()
);
}
#[test]
fn font_variation_italic_and_slant_api() {
// Test the Paint API for italic and slant methods
let mut paint = femtovg::Paint::default();
// Initially no italic set
assert!(paint.font_italic().is_none());
assert!(paint.font_slant().is_none());
// Set italic — also sets slnt as fallback
paint.set_font_italic(true);
assert_eq!(paint.font_italic(), Some(true));
assert_eq!(paint.font_slant(), Some(-12.0));
paint.set_font_italic(false);
assert_eq!(paint.font_italic(), Some(false));
assert_eq!(paint.font_slant(), Some(0.0));
// clear_font_italic removes both ital and slnt
paint.clear_font_italic();
assert!(paint.font_italic().is_none());
assert!(paint.font_slant().is_none());
// Explicit slant overrides the italic fallback
paint.set_font_italic(true);
paint.set_font_slant(-5.0);
assert_eq!(paint.font_italic(), Some(true));
assert_eq!(paint.font_slant(), Some(-5.0));
paint.clear_font_variations();
// Set slant independently
paint.set_font_slant(-12.0);
assert_eq!(paint.font_slant(), Some(-12.0));
paint.clear_font_slant();
assert!(paint.font_slant().is_none());
// Multiple variations at once
paint.set_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
paint.set_font_italic(true);
assert_eq!(paint.font_weight(), Some(700.0));
assert_eq!(paint.font_italic(), Some(true));
assert_eq!(paint.font_slant(), Some(-12.0));
// Clear all
paint.clear_font_variations();
assert!(paint.font_weight().is_none());
assert!(paint.font_italic().is_none());
assert!(paint.font_slant().is_none());
}
#[test]
fn font_variation_hash_stability() {
// Setting the same variations in different order should produce the same hash
let mut paint_a = femtovg::Paint::default();
paint_a.set_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
paint_a.set_font_italic(true);
let mut paint_b = femtovg::Paint::default();
paint_b.set_font_italic(true);
paint_b.set_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
// Both paints should have the same variation hash (verified via the generic API)
assert_eq!(paint_a.font_weight(), paint_b.font_weight());
assert_eq!(paint_a.font_italic(), paint_b.font_italic());
}
#[test]
fn variable_font_slant_affects_measurement() {
let text_context = femtovg::TextContext::default();
let font_id = text_context
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
.expect("Font not found");
// Verify the font has a slnt axis
let axes = text_context.font_variation_axes(font_id).unwrap();
let slnt_axis = axes.iter().find(|a| &a.tag == b"slnt");
assert!(slnt_axis.is_some(), "Font should have a slnt axis");
let upright_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
let slanted_paint = femtovg::Paint::default()
.with_font(&[font_id])
.with_font_size(16.)
.with_font_slant(-10.0);
let upright_metrics = text_context
.measure_text(0., 0., "Hello World", &upright_paint)
.expect("text shaping failed");
let slanted_metrics = text_context
.measure_text(0., 0., "Hello World", &slanted_paint)
.expect("text shaping failed");
// Slanted text should produce different measurements than upright text
// (the widths may differ slightly due to slant-adjusted glyph metrics)
assert!(
(upright_metrics.width() - slanted_metrics.width()).abs() > 0.0
|| upright_metrics.glyphs.len() == slanted_metrics.glyphs.len(),
"Slanted text should shape successfully and produce valid metrics"
);
}
#[test]
fn font_italic_falls_back_to_slnt_axis() {
// Roboto Flex has no `ital` axis but has `slnt`. Calling set_font_italic
// should still produce slanted text via the slnt fallback.
let text_context = femtovg::TextContext::default();
let font_id = text_context
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
.expect("Font not found");
let axes = text_context.font_variation_axes(font_id).unwrap();
assert!(
axes.iter().find(|a| &a.tag == b"ital").is_none(),
"Font should NOT have an ital axis"
);
assert!(
axes.iter().find(|a| &a.tag == b"slnt").is_some(),
"Font should have a slnt axis"
);
let upright_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
let italic_paint = femtovg::Paint::default()
.with_font(&[font_id])
.with_font_size(16.)
.with_font_italic(true);
let upright_metrics = text_context
.measure_text(0., 0., "Hello World", &upright_paint)
.expect("text shaping failed");
let italic_metrics = text_context
.measure_text(0., 0., "Hello World", &italic_paint)
.expect("text shaping failed");
// The slnt axis changes glyph outlines (tilts them) but doesn't change advance
// widths, so we verify that shaping succeeds and produces the same glyph count.
// The visual difference (slanted glyphs) is only visible in rendering.
assert_eq!(
upright_metrics.glyphs.len(),
italic_metrics.glyphs.len(),
"Italic fallback via slnt should shape the same number of glyphs"
);
assert!(italic_metrics.width() > 0.0, "Italic text should have positive width");
}
+85
View File
@@ -0,0 +1,85 @@
use femtovg::Transform2D;
#[test]
fn test_multiplication() {
let a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
let expected = Transform2D::new(25.0, 28.0, 57.0, 64.0, 100.0, 112.0);
let result = a * b;
assert_eq!(result, expected);
}
#[test]
fn test_multiplication_assignment() {
let mut a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
let expected = Transform2D::new(25.0, 28.0, 57.0, 64.0, 100.0, 112.0);
a *= b;
assert_eq!(a, expected);
}
#[test]
fn test_premultiply() {
let a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
let mut premultiplied = a;
premultiplied.premultiply(&b);
assert_eq!(premultiplied, b * a);
}
#[test]
fn test_division() {
let a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
let expected = Transform2D::new(-2.0, 3.0, -3.0, 4.0, -3.0, 3.0);
let result = b / a;
assert_eq!(result, expected);
}
#[test]
fn test_division_assignment() {
let a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let mut b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
let expected = Transform2D::new(-2.0, 3.0, -3.0, 4.0, -3.0, 3.0);
b /= a;
assert_eq!(b, expected);
}
#[test]
fn test_translation() {
let transform = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let (tx, ty) = (2.0, 5.0);
let mut translated = transform;
translated.translate(tx, ty);
assert_eq!(translated, transform * Transform2D::translation(tx, ty))
}
#[test]
fn test_rotation() {
let transform = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let a = std::f32::consts::TAU * 0.75;
let mut scaled = transform;
scaled.rotate(a);
assert_eq!(scaled, transform * Transform2D::rotation(a))
}
#[test]
fn test_scaling() {
let transform = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let (sx, sy) = (2.0, 0.5);
let mut scaled = transform;
scaled.scale(sx, sy);
assert_eq!(scaled, transform * Transform2D::scaling(sx, sy))
}