fix(text): always draw the first line under overflow: elide
Vendor i-slint-core 1.16.1 with a one-rule patch in sharedparley's
line culling: with overflow: elide, every line whose bottom edge
exceeded max_height was dropped — INCLUDING the first one, eliding the
text to literally nothing. CJK fallback fonts report line metrics
taller than snug fixed-height boxes (Hiragino at 14px measures ~21px
inside TrackRow's 20px title line), so on macOS every CJK track title
rendered blank while Latin titles fit; Linux survived because Noto's
metrics fit the box. Root-caused with a headless render matrix on the
Mac Mini: the verbatim TrackRow structure reproduced the blank title,
removing elide rendered it, and the patched core renders all variants.
The first line still receives the ellipsis treatment as the last
visible line.
This is the real #543 mechanism. The earlier femtovg render_glyph
retry (3a1c367d) addressed a different, hypothesized layer and stays
as harmless defense in depth.
Fixes #543
cc
This commit is contained in:
82 files changed
+51634
-2
No files matched your search
Generated
-2
@@ -2983,8 +2983,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "i-slint-core"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e4a57afd78d8a1f599a78781dc1413af68545b2bc5ba5e143ecf022c75d5f3"
|
||||
dependencies = [
|
||||
"auto_enums",
|
||||
"bitflags 2.11.0",
|
||||
|
||||
@@ -121,6 +121,14 @@ 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" }
|
||||
# With `overflow: elide`, sharedparley culled EVERY line whose bottom edge
|
||||
# exceeded max_height — including the FIRST one, eliding the text to nothing.
|
||||
# CJK fallback fonts (Hiragino on macOS) report line metrics taller than
|
||||
# snug fixed-height boxes (TrackRow's 20px title line at 14px), so every
|
||||
# CJK track title rendered blank on macOS while Latin fit (#543; verified
|
||||
# with a headless repro matrix on the Mac Mini). The patch always draws the
|
||||
# first line — it still gets the ellipsis treatment as the last visible one.
|
||||
i-slint-core = { path = "vendor/i-slint-core-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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "e9c1ca295f9356af71f1e251c287de18406b46f6"
|
||||
},
|
||||
"path_in_vcs": "internal/core"
|
||||
}
|
||||
+3376
File diff suppressed because it is too large.
Load diff
+390
@@ -0,0 +1,390 @@
|
||||
# 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 = "2024"
|
||||
rust-version = "1.88"
|
||||
name = "i-slint-core"
|
||||
version = "1.16.1"
|
||||
authors = ["Slint Developers <info@slint.dev>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Internal Slint Runtime Library."
|
||||
homepage = "https://slint.dev"
|
||||
documentation = "https://slint.dev/docs"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"gui",
|
||||
"toolkit",
|
||||
"graphics",
|
||||
"design",
|
||||
"ui",
|
||||
]
|
||||
categories = [
|
||||
"gui",
|
||||
"development-tools",
|
||||
"no-std",
|
||||
]
|
||||
license = "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0"
|
||||
repository = "https://github.com/slint-ui/slint"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
32-bit-color = []
|
||||
box-shadow-cache = []
|
||||
default = [
|
||||
"std",
|
||||
"unicode",
|
||||
"shared-parley",
|
||||
]
|
||||
ffi = ["dep:static_assertions"]
|
||||
image-decoders = [
|
||||
"dep:image",
|
||||
"dep:clru",
|
||||
]
|
||||
image-default-formats = ["image?/default-formats"]
|
||||
libm = [
|
||||
"num-traits/libm",
|
||||
"euclid/libm",
|
||||
]
|
||||
path = [
|
||||
"dep:auto_enums",
|
||||
"dep:lyon_path",
|
||||
"dep:lyon_geom",
|
||||
"dep:lyon_algorithms",
|
||||
]
|
||||
raw-window-handle-06 = ["dep:raw-window-handle-06"]
|
||||
rtti = []
|
||||
shared-fontique = ["i-slint-common/shared-fontique"]
|
||||
shared-parley = [
|
||||
"shared-fontique",
|
||||
"dep:parley",
|
||||
"dep:skrifa",
|
||||
"dep:icu_normalizer",
|
||||
]
|
||||
shared-swash = ["dep:swash"]
|
||||
std = [
|
||||
"euclid/std",
|
||||
"once_cell/std",
|
||||
"dep:scoped-tls-hkt",
|
||||
"dep:web-time",
|
||||
"dep:lyon_extra",
|
||||
"image-decoders",
|
||||
"svg",
|
||||
"path",
|
||||
"raw-window-handle-06?/std",
|
||||
"chrono/std",
|
||||
"chrono/wasmbind",
|
||||
"chrono/clock",
|
||||
"dep:sys-locale",
|
||||
"shared-fontique",
|
||||
"i-slint-common/color-parsing",
|
||||
"i-slint-common/markdown",
|
||||
"taffy/std",
|
||||
]
|
||||
svg = ["dep:resvg"]
|
||||
testing = []
|
||||
tr = ["dep:tr"]
|
||||
unicode = [
|
||||
"unicode-script",
|
||||
"unicode-linebreak",
|
||||
]
|
||||
unsafe-single-threaded = []
|
||||
unstable-wgpu-27 = ["wgpu-27"]
|
||||
unstable-wgpu-28 = ["wgpu-28"]
|
||||
wgpu-27 = ["dep:wgpu-27"]
|
||||
wgpu-28 = ["dep:wgpu-28"]
|
||||
|
||||
[lib]
|
||||
name = "i_slint_core"
|
||||
path = "lib.rs"
|
||||
|
||||
[dependencies.auto_enums]
|
||||
version = "0.8.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "2.4.2"
|
||||
|
||||
[dependencies.cfg-if]
|
||||
version = "1"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["alloc"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.clru]
|
||||
version = "0.6.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.const-field-offset]
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies.derive_more]
|
||||
version = "2.0.0"
|
||||
features = [
|
||||
"deref",
|
||||
"deref_mut",
|
||||
"into",
|
||||
"from",
|
||||
"add",
|
||||
"add_assign",
|
||||
"mul",
|
||||
"not",
|
||||
"display",
|
||||
"error",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.euclid]
|
||||
version = "0.22.1"
|
||||
default-features = false
|
||||
|
||||
[dependencies.i-slint-common]
|
||||
version = "=1.16.1"
|
||||
features = ["default"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.i-slint-core-macros]
|
||||
version = "=1.16.1"
|
||||
features = ["default"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.icu_normalizer]
|
||||
version = "2"
|
||||
features = ["compiled_data"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.image]
|
||||
version = "0.25"
|
||||
features = [
|
||||
"png",
|
||||
"jpeg",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.lyon_algorithms]
|
||||
version = "1.0"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.lyon_extra]
|
||||
version = "1.0.1"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.lyon_geom]
|
||||
version = "1.0"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.lyon_path]
|
||||
version = "1.0"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.num-traits]
|
||||
version = "0.2"
|
||||
default-features = false
|
||||
|
||||
[dependencies.once_cell]
|
||||
version = "1.5"
|
||||
features = ["critical-section"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.parley]
|
||||
version = "0.8.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.pin-project]
|
||||
version = "1"
|
||||
|
||||
[dependencies.pin-weak]
|
||||
version = "1.1"
|
||||
default-features = false
|
||||
|
||||
[dependencies.portable-atomic]
|
||||
version = "1"
|
||||
features = ["critical-section"]
|
||||
|
||||
[dependencies.raw-window-handle-06]
|
||||
version = "0.6"
|
||||
features = ["alloc"]
|
||||
optional = true
|
||||
package = "raw-window-handle"
|
||||
|
||||
[dependencies.resvg]
|
||||
version = "0.47.0"
|
||||
features = [
|
||||
"text",
|
||||
"raster-images",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.rgb]
|
||||
version = "0.8.27"
|
||||
|
||||
[dependencies.scoped-tls-hkt]
|
||||
version = "0.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.scopeguard]
|
||||
version = "1.1.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.163"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.skrifa]
|
||||
version = "0.40"
|
||||
optional = true
|
||||
|
||||
[dependencies.slab]
|
||||
version = "0.4.3"
|
||||
default-features = false
|
||||
|
||||
[dependencies.static_assertions]
|
||||
version = "1.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.strum]
|
||||
version = "0.28"
|
||||
features = ["derive"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.swash]
|
||||
version = "0.2.7"
|
||||
features = ["scale"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.sys-locale]
|
||||
version = "0.3.2"
|
||||
features = ["js"]
|
||||
optional = true
|
||||
|
||||
[dependencies.taffy]
|
||||
version = "0.9"
|
||||
features = [
|
||||
"flexbox",
|
||||
"taffy_tree",
|
||||
"alloc",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.tr]
|
||||
version = "0.1"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.unicode-linebreak]
|
||||
version = "0.1.5"
|
||||
optional = true
|
||||
|
||||
[dependencies.unicode-script]
|
||||
version = "0.5.7"
|
||||
optional = true
|
||||
|
||||
[dependencies.unicode-segmentation]
|
||||
version = "1.12.0"
|
||||
|
||||
[dependencies.vtable]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.wgpu-27]
|
||||
version = "27"
|
||||
optional = true
|
||||
default-features = false
|
||||
package = "wgpu"
|
||||
|
||||
[dependencies.wgpu-28]
|
||||
version = "28"
|
||||
optional = true
|
||||
default-features = false
|
||||
package = "wgpu"
|
||||
|
||||
[dev-dependencies.async-compat]
|
||||
version = "0.2.4"
|
||||
|
||||
[dev-dependencies.fontique]
|
||||
version = "0.8.0"
|
||||
|
||||
[dev-dependencies.rustybuzz]
|
||||
version = "0.20.0"
|
||||
|
||||
[dev-dependencies.serde_json]
|
||||
version = "1.0.96"
|
||||
|
||||
[dev-dependencies.tempfile]
|
||||
version = "3.12.0"
|
||||
|
||||
[dev-dependencies.tiny-skia]
|
||||
version = "0.12.0"
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
version = "1.35"
|
||||
features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"net",
|
||||
"io-util",
|
||||
]
|
||||
|
||||
[dev-dependencies.ttf-parser]
|
||||
version = "0.25"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen]
|
||||
version = "0.2"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
|
||||
version = "0.3.72"
|
||||
features = [
|
||||
"HtmlImageElement",
|
||||
"Navigator",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.web-time]
|
||||
version = "1.0"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(target_family = "unix")'.dependencies.gettext-rs]
|
||||
version = "0.7.1"
|
||||
features = ["gettext-system"]
|
||||
optional = true
|
||||
|
||||
[lints.clippy.unnecessary_cast]
|
||||
level = "allow"
|
||||
priority = 0
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "warn"
|
||||
priority = 0
|
||||
check-cfg = [
|
||||
"cfg(slint_debug_property)",
|
||||
"cfg(cbindgen)",
|
||||
"cfg(slint_int_coord)",
|
||||
"cfg(slint_nightly_test)",
|
||||
]
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
[package]
|
||||
name = "i-slint-core"
|
||||
description = "Internal Slint Runtime Library."
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
edition = "2024"
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
categories = ["gui", "development-tools", "no-std"]
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
[features]
|
||||
ffi = ["dep:static_assertions"] # Expose C ABI
|
||||
libm = ["num-traits/libm", "euclid/libm"]
|
||||
# Allow the viewer to query at runtime information about item types
|
||||
rtti = []
|
||||
# Use the standard library
|
||||
std = [
|
||||
"euclid/std",
|
||||
"once_cell/std",
|
||||
"dep:scoped-tls-hkt",
|
||||
"dep:web-time",
|
||||
"dep:lyon_extra",
|
||||
"image-decoders",
|
||||
"svg",
|
||||
"path",
|
||||
"raw-window-handle-06?/std",
|
||||
"chrono/std",
|
||||
"chrono/wasmbind",
|
||||
"chrono/clock",
|
||||
"dep:sys-locale",
|
||||
"shared-fontique",
|
||||
"i-slint-common/color-parsing",
|
||||
"i-slint-common/markdown",
|
||||
"taffy/std",
|
||||
]
|
||||
# Unsafe feature meaning that there is only one core running and all thread_local are static.
|
||||
# You can only enable this feature if you are sure that any API of this crate is only called
|
||||
# from a single core, and not in a interrupt or signal handler.
|
||||
unsafe-single-threaded = []
|
||||
|
||||
unicode = ["unicode-script", "unicode-linebreak"]
|
||||
|
||||
image-decoders = ["dep:image", "dep:clru"]
|
||||
image-default-formats = ["image?/default-formats"]
|
||||
svg = ["dep:resvg"]
|
||||
|
||||
box-shadow-cache = []
|
||||
|
||||
testing = []
|
||||
|
||||
shared-fontique = ["i-slint-common/shared-fontique"]
|
||||
|
||||
raw-window-handle-06 = ["dep:raw-window-handle-06"]
|
||||
|
||||
# The wgpu-{27,28} features enable the internal wgpu module (SurfaceTarget, init helpers)
|
||||
# without exposing public API types (GraphicsAPI::WGPU*, WGPUTexture, WGPUConfiguration).
|
||||
# The unstable-wgpu-* features additionally enable those public API types.
|
||||
wgpu-27 = ["dep:wgpu-27"]
|
||||
|
||||
unstable-wgpu-27 = ["wgpu-27"]
|
||||
wgpu-28 = ["dep:wgpu-28"]
|
||||
|
||||
unstable-wgpu-28 = ["wgpu-28"]
|
||||
|
||||
tr = ["dep:tr"]
|
||||
|
||||
32-bit-color = []
|
||||
default = ["std", "unicode", "shared-parley"]
|
||||
|
||||
shared-parley = ["shared-fontique", "dep:parley", "dep:skrifa", "dep:icu_normalizer"]
|
||||
|
||||
shared-swash = ["dep:swash"]
|
||||
|
||||
path = ["dep:auto_enums", "dep:lyon_path", "dep:lyon_geom", "dep:lyon_algorithms"]
|
||||
|
||||
[dependencies]
|
||||
i-slint-common = { workspace = true, features = ["default"] }
|
||||
i-slint-core-macros = { workspace = true, features = ["default"] }
|
||||
|
||||
const-field-offset = { version = "0.2.0", path = "../../helper_crates/const-field-offset" }
|
||||
vtable = { workspace = true }
|
||||
|
||||
portable-atomic = { version = "1", features = ["critical-section"] }
|
||||
auto_enums = { version = "0.8.0", optional = true }
|
||||
cfg-if = "1"
|
||||
derive_more = { workspace = true, features = ["error"] }
|
||||
euclid = { workspace = true }
|
||||
lyon_algorithms = { version = "1.0", optional = true, default-features = false }
|
||||
lyon_geom = { version = "1.0", optional = true, default-features = false }
|
||||
lyon_path = { workspace = true, optional = true, default-features = false }
|
||||
lyon_extra = { version = "1.0.1", optional = true, default-features = false }
|
||||
num-traits = { version = "0.2", default-features = false }
|
||||
once_cell = { version = "1.5", default-features = false, features = ["critical-section"] }
|
||||
pin-project = "1"
|
||||
pin-weak = { version = "1.1", default-features = false }
|
||||
# Note: the rgb version is extracted in ci.yaml for rustdoc builds
|
||||
rgb = "0.8.27"
|
||||
scoped-tls-hkt = { version = "0.1", optional = true }
|
||||
scopeguard = { version = "1.1.0", default-features = false }
|
||||
slab = { version = "0.4.3", default-features = false }
|
||||
static_assertions = { version = "1.1", optional = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
# Note: We're migrating our unicode handling over to the icu family of crates.
|
||||
# Prefer using icu crates over adding more unicode-* dependencies
|
||||
# parley uses the icu crates internally as well, so we'll share the dependency there.
|
||||
unicode-segmentation = { workspace = true }
|
||||
unicode-linebreak = { version = "0.1.5", optional = true }
|
||||
unicode-script = { version = "0.5.7", optional = true }
|
||||
icu_normalizer = { workspace = true, optional = true }
|
||||
sys-locale = { version = "0.3.2", optional = true, features = ["js"] }
|
||||
parley = { version = "0.8.0", optional = true }
|
||||
|
||||
image = { workspace = true, optional = true, default-features = false }
|
||||
clru = { workspace = true, optional = true }
|
||||
|
||||
resvg = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
raw-window-handle-06 = { workspace = true, optional = true }
|
||||
bitflags = { version = "2.4.2" }
|
||||
|
||||
chrono = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||
skrifa = { workspace = true, optional = true }
|
||||
swash = { workspace = true, optional = true, features = ["scale"] }
|
||||
|
||||
wgpu-27 = { workspace = true, optional = true }
|
||||
wgpu-28 = { workspace = true, optional = true }
|
||||
|
||||
tr = { workspace = true, optional = true }
|
||||
|
||||
taffy = { version = "0.9", default-features = false, features = ["flexbox", "taffy_tree", "alloc"] }
|
||||
|
||||
[target.'cfg(target_family = "unix")'.dependencies]
|
||||
gettext-rs = { version = "0.7.1", optional = true, features = ["gettext-system"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
web-time = { version = "1.0", optional = true }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
web-sys = { workspace = true, features = ["HtmlImageElement", "Navigator"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
slint = { path = "../../api/rs/slint", default-features = false, features = ["std", "compat-1-2", "renderer-software"] }
|
||||
i-slint-backend-testing = { path = "../backends/testing" }
|
||||
rustybuzz = { workspace = true }
|
||||
ttf-parser = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tiny-skia = "0.12.0"
|
||||
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "time", "net", "io-util"] }
|
||||
async-compat = { version = "0.2.4" }
|
||||
tempfile = { version = "3.12.0" }
|
||||
fontique = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(slint_debug_property)", "cfg(cbindgen)", "cfg(slint_int_coord)", "cfg(slint_nightly_test)"] }
|
||||
|
||||
[lints.clippy]
|
||||
# Coord is an alias for f32 in most architectures, but not always, so the cast
|
||||
# to f32 *is* necessary.
|
||||
unnecessary_cast = { level = "allow" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
@@ -0,0 +1,232 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
“This License” refers to version 3 of the GNU General Public License.
|
||||
|
||||
“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
||||
|
||||
“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
|
||||
|
||||
To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
|
||||
|
||||
A “covered work” means either the unmodified Program or a work based on the Program.
|
||||
|
||||
To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
|
||||
|
||||
To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
|
||||
|
||||
A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
||||
|
||||
The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
|
||||
|
||||
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
|
||||
|
||||
A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
|
||||
|
||||
“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
|
||||
|
||||
An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
|
||||
|
||||
A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
|
||||
|
||||
A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Slint Royalty-free Desktop, Mobile, and Web Applications License
|
||||
|
||||
Version 2.0
|
||||
|
||||
## Preamble
|
||||
|
||||
Slint is a toolkit that can be used to build user interfaces for applications. Slint (hereafter referred to as **Software**) is made available under different licenses by SixtyFPS GmbH incorporated at Oranienburger Str. 44, 16540 Hohen Neuendorf, Germany (**SixtyFPS**). The **Slint Royalty-free Desktop, Mobile, and Web Applications License** is suitable for those who develop desktop, mobile, or web applications and do not want to use open source components under copyleft licenses.
|
||||
|
||||
## 1. Grant of Rights
|
||||
|
||||
SixtyFPS hereby grants You a world-wide, royalty-free, non-exclusive license to use, reproduce, make available, modify, display, perform, distribute the Software as part of a Desktop, Mobile, or Web Application.
|
||||
|
||||
A **Desktop Application** is a computer program that is designed to run on a general-purpose computer (PC or notebook), typically installed and executed locally on the computer's operating system.
|
||||
|
||||
A **Mobile Application** is a computer program that is designed to run on a general-purpose mobile computer (mobile phone or tablet), typically installed and executed locally on the computer's operating system.
|
||||
|
||||
A **Web Application** is a computer program that is designed to run in the sandbox environment provided by a web browser.
|
||||
|
||||
Desktop Application, Mobile Application, and Web Application are hereafter referred to as **Application**.
|
||||
|
||||
## 2. License Conditions - Attribution
|
||||
|
||||
You may distribute the Software as part of an Application, modified or unmodified, provided that You do either of the following:
|
||||
|
||||
(a) Display the [`AboutSlint`](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/misc/aboutslint/) widget in an "About" screen or dialog that is accessible from the top level menu of the Application. In the absence of such a screen or dialog, display the widget in the "Splash Screen" of the Application.
|
||||
|
||||
(b) Display the [Slint attribution badge](https://github.com/slint-ui/slint/tree/master/logo/MadeWithSlint-logo-whitebg.png) on a public webpage, preferably where the binaries of your Application can be downloaded from, in such a way that it can be easily found by any visitor to that page.
|
||||
|
||||
## 3. Limitations
|
||||
|
||||
The License does not permit to distribute or make the Software publicly available alone and without integration into an Application. For this purpose you may use the Software under the GNU General Public License, version 3.
|
||||
|
||||
The License does not permit the use of the Software within Embedded Systems. An **Embedded System** is a computer system designed to perform a specific task within a larger mechanical or electrical system.
|
||||
|
||||
The License does not permit the distribution of Application that exposes the APIs, in part or in total, of the Software.
|
||||
|
||||
You may not remove or alter any license notices (including copyright notices, disclaimers of warranty, or limitations of liability) contained within the source code form of the Software.
|
||||
|
||||
## 4. Warranty and Liability
|
||||
|
||||
SixtyFPS is only liable for conflicting rights of third parties if SixtyFPS was aware of these rights without informing you. Unless required by applicable law or agreed to in writing, SixtyFPS provides the Software on an "as is" basis, without warranties or conditions of any kind, either express or implied, including, without limitation, any warranties or conditions of merchantability, or fitness for a particular purpose.
|
||||
|
||||
Unless required by law, SixtyFPS won't be liable for any direct, indirect, incidental, or consequential damages arising in any way out of the use of the Software.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Slint Software License
|
||||
|
||||
Version 3.0.5
|
||||
|
||||
## Preamble
|
||||
|
||||
Slint is a toolkit that can be used to build user interfaces for applications. Slint (hereafter referred to as **Software**) is made available under different licenses by SixtyFPS GmbH incorporated at Oranienburger Str. 44, 16540 Hohen Neuendorf, Germany (**SixtyFPS**). The **Slint Software License** is suitable for those who do not want to use open source components under copyleft licenses.
|
||||
|
||||
## 1. Grant of Rights
|
||||
|
||||
SixtyFPS hereby grants You a world-wide, non-exclusive license to use, reproduce, make available, modify, display, perform, distribute the Software as part of a Desktop, Mobile, or Web Application or as part of an Embedded System (each of which is defined below).
|
||||
|
||||
A **Desktop Application** is a computer program that is designed to run on a general-purpose computer (PC or notebook), typically installed and executed locally on the computer's operating system.
|
||||
|
||||
A **Mobile Application** is a computer program that is designed to run on a general-purpose mobile computer (mobile phone or tablet), typically installed and executed locally on the computer's operating system.
|
||||
|
||||
A **Web Application** is a computer program that is designed to run in the sandbox environment provided by a web browser.
|
||||
|
||||
An **Embedded System** is a computer system designed to perform a specific task within a larger mechanical or electrical system.
|
||||
|
||||
Desktop Application, Mobile Application, and Web Application are hereafter referred to as **Application**.
|
||||
|
||||
## 2. License Conditions
|
||||
|
||||
The grant of rights in section 1 are conditional, provided that You do all of the following:
|
||||
|
||||
(a) You have purchased an appropriate **Paid License Plan** ([see Annex 1](#annex-1-paid-license-plans)) and the required amount of seats to cover all individual users of the Software associated with the designing, developing, or testing your Application or Embedded System. For clarity, each individual user is counted as one seat.
|
||||
|
||||
(b) In the case that You are distributing the Software as part of an Embedded System, You have purchased an appropriate quantity of **Royalties**, one Royalty for each Embedded System. Royalties become due and payable upon manufacture of the Embedded System, regardless of whether such is subsequently sold, shipped, returned, replaced under warranty, or recalled. Payment of royalties is non-refundable under any circumstances. Royalties are not necessary for non-commercial projects, personal projects, and open source projects.
|
||||
|
||||
## 3. Limitations
|
||||
|
||||
The License does not permit to distribute or make the Software publicly available alone and without integration into an Application or into an Embedded System. For this purpose you may use the Software under the GNU General Public License, version 3.
|
||||
|
||||
The License is limited to only the versions of Software that were made available to you under the Paid License Plan. For all other versions, you may use the Software under either the GNU General Public License, version 3 or the Slint Royalty-free Desktop, Mobile, and Web Applications License.
|
||||
|
||||
The License does not permit the distribution of Application that exposes the APIs, in part or in total, of the Software.
|
||||
|
||||
You may not remove or alter any license notices (including copyright notices, disclaimers of warranty, or limitations of liability) contained within the source code form of the Software.
|
||||
|
||||
## 4. Audit Rights
|
||||
|
||||
SixtyFPS or an independent certified auditor on SixtyFPS's behalf, may, upon its reasonable request, with 30 (thirty) days written notice, and at its sole expense, examine your books and records solely with respect to your use of the Software. Any such audit shall be conducted during regular business hours at your facilities and shall not unreasonably interfere with your business activities. The auditor shall not remove, copy, or redistribute any electronic material during an audit. If an audit reveals that you are using the Software in a way that is in material violation of the terms of this License, then you shall pay SixtyFPS reasonable costs of conducting the audit. The auditor shall only be allowed to report violations of the terms of this License, with a copy to you. You shall be provided the right to provide comments to the report before it is finalized.
|
||||
|
||||
## 5. Termination
|
||||
|
||||
(a) SixtyFPS may terminate this License if You materially breach any obligation hereunder, provided You have been provided notice of such breach and an opportunity to cure such breach during a period of not less than sixty (60) days following such notice.
|
||||
|
||||
(b) You may terminate this License with or without cause upon no less than thirty (30) days advance written notice to SixtyFPS.
|
||||
|
||||
(c) Upon termination of this License, You will immediately cease using, reproducing, making available, modifying, displaying, performing, distributing the Software and pay immediately any unpaid Fees and contractual penalties.
|
||||
|
||||
(d) Sections 3 through 8 of this License will survive any termination of the License to the extent necessary to implement their objectives.
|
||||
|
||||
## 6. Assignment
|
||||
|
||||
You may assign this License, in whole or in part (whether by operation of law or otherwise), with prior consent from SixtyFPS, which shall not be unreasonably withheld or delayed. SixtyFPS may assign any of its rights or delegate any of its obligations hereunder with prior notice to You, provided that the successor maintains at least the same level of security, confidentiality, and data protection measures as in place at the time of assignment or delegation. Any attempt to assign this License other than in accordance with this Section 6 shall be null and void.
|
||||
|
||||
## 7. Severability
|
||||
|
||||
In the event that any provision of this License will, for any reason, be determined by any court of competent jurisdiction to be invalid, illegal or unenforceable in any respect, such invalidity, illegality or unenforceability will be interpreted as closely as possible so as not affect any other provision of this License, and such provision will further be modified by said court to permit its enforcement to the maximum extent permitted by law.
|
||||
|
||||
## 8. Governing Law
|
||||
|
||||
This Agreement shall be construed, interpreted, and governed by the laws of the Federal Republic of Germany.
|
||||
|
||||
## Annex 1: Paid License Plans
|
||||
|
||||
### Enterprise Plan
|
||||
|
||||
The following is included as part of the plan
|
||||
|
||||
(a) No restriction on the number of applications that are developed with Slint.
|
||||
|
||||
(b) Live Preview.
|
||||
|
||||
(c) Standard Support that includes addressing technical queries, troubleshooting, and rectifying bugs or errors (faults) present in the latest official stable release.
|
||||
|
||||
(d) Perpetual Fallback License that allows continued use of a specific Slint version, including all bugfix updates (i.e., all Z releases within the X.Y.Z version), without an active subscription. This license applies only to those versions of Slint for which at least 12 consecutive months of subscription have been paid.
|
||||
|
||||
(e) GUI Test Framework.
|
||||
|
||||
### Small Enterprise Plan
|
||||
|
||||
This plan is limited to individual companies with a staff headcount between 10 and 50 and either a turnover or balance sheet total of 10 million EUR or less. If You are a Small Enterprise, You are required to submit the self-assessment report generated from the EU SME Self-Assessment Tool (https://ec.europa.eu/info/funding-tenders/opportunities/portal/sme/public/organisation-name).
|
||||
|
||||
The following is included as part of the plan
|
||||
|
||||
(a) No restriction on the number of applications that are developed with Slint.
|
||||
|
||||
(b) Live Preview.
|
||||
|
||||
(c) Standard Support that includes addressing technical queries, troubleshooting, and rectifying bugs or errors (faults) present in the latest official stable release.
|
||||
|
||||
The following can be purchased as an Add-On
|
||||
|
||||
(a) Perpetual Fallback License that allows continued use of a specific Slint version, including all bugfix updates (i.e., all Z releases within the X.Y.Z version), without an active subscription. This license applies only to those versions of Slint for which at least 12 consecutive months of subscription have been paid.
|
||||
|
||||
(b) GUI Test Framework.
|
||||
|
||||
### Startup & Individual Plan
|
||||
|
||||
This plan is limited to individuals and individual companies with a staff headcount of less than 10 and either a turnover or balance sheet total of 2 million EUR or less. If You are a Startup, you are required to submit the self-assessment report generated from the EU SME Self-Assessment Tool (https://ec.europa.eu/info/funding-tenders/opportunities/portal/sme/public/organisation-name).
|
||||
|
||||
The following is included as part of the plan
|
||||
|
||||
(a) No restriction on the number of applications that are developed with Slint.
|
||||
|
||||
(b) Live Preview.
|
||||
|
||||
The following can be purchased as an Add-On
|
||||
|
||||
(a) Standard Support that includes addressing technical queries, troubleshooting, and rectifying bugs or errors (faults) present in the latest official stable release.
|
||||
|
||||
(b) Perpetual Fallback License that allows continued use of a specific Slint version, including all bugfix updates (i.e., all Z releases within the X.Y.Z version), without an active subscription. This license applies only to those versions of Slint for which at least 12 consecutive months of subscription have been paid.
|
||||
|
||||
(c) GUI Test Framework.
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
|
||||
# Slint Runtime Library
|
||||
|
||||
**NOTE**: This library is an **internal** crate of the [Slint project](https://slint.dev).
|
||||
This crate should **not be used directly** by applications using Slint.
|
||||
You should use the `slint` crate instead.
|
||||
|
||||
**WARNING**: This crate does not follow the semver convention for versioning and can
|
||||
only be used with `version = "=x.y.z"` in Cargo.toml.
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
// cSpell: ignore descendents
|
||||
|
||||
use crate::{
|
||||
SharedString,
|
||||
item_tree::ItemTreeVTable,
|
||||
items::{ItemRc, TextInput},
|
||||
};
|
||||
use alloc::{vec, vec::Vec};
|
||||
use bitflags::bitflags;
|
||||
use vtable::VRcMapped;
|
||||
|
||||
/// The property names of the accessible-properties
|
||||
#[repr(u32)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone, strum::Display)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum AccessibleStringProperty {
|
||||
Checkable,
|
||||
Checked,
|
||||
DelegateFocus,
|
||||
Description,
|
||||
Enabled,
|
||||
Expandable,
|
||||
Expanded,
|
||||
Id,
|
||||
ItemCount,
|
||||
ItemIndex,
|
||||
ItemSelectable,
|
||||
ItemSelected,
|
||||
Label,
|
||||
PlaceholderText,
|
||||
ReadOnly,
|
||||
Value,
|
||||
ValueMaximum,
|
||||
ValueMinimum,
|
||||
ValueStep,
|
||||
}
|
||||
|
||||
/// The argument of an accessible action.
|
||||
#[repr(u32)]
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub enum AccessibilityAction {
|
||||
Default,
|
||||
Decrement,
|
||||
Increment,
|
||||
Expand,
|
||||
/// This is currently unused
|
||||
ReplaceSelectedText(SharedString),
|
||||
SetValue(SharedString),
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Define a accessibility actions that supported by an item.
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub struct SupportedAccessibilityAction: u32 {
|
||||
const Default = 1;
|
||||
const Decrement = 1 << 1;
|
||||
const Increment = 1 << 2;
|
||||
const Expand = 1 << 3;
|
||||
const ReplaceSelectedText = 1 << 4;
|
||||
const SetValue = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
/// Find accessible descendents of `root_item`.
|
||||
///
|
||||
/// This will recurse through all children of `root_item`, but will not recurse
|
||||
/// into nodes that are accessible.
|
||||
pub fn accessible_descendents(root_item: &ItemRc) -> impl Iterator<Item = ItemRc> {
|
||||
fn try_candidate_or_find_next_accessible_descendent(
|
||||
candidate: ItemRc,
|
||||
descendent_candidates: &mut Vec<ItemRc>,
|
||||
) -> Option<ItemRc> {
|
||||
if candidate.is_accessible() {
|
||||
return Some(candidate);
|
||||
}
|
||||
|
||||
candidate.first_child().and_then(|child| {
|
||||
if let Some(next) = child.next_sibling() {
|
||||
descendent_candidates.push(next);
|
||||
}
|
||||
try_candidate_or_find_next_accessible_descendent(child, descendent_candidates)
|
||||
})
|
||||
}
|
||||
|
||||
// Do not look on the root_item: That is either a component root or an
|
||||
// accessible item already handled!
|
||||
let mut descendent_candidates = Vec::new();
|
||||
if let Some(child) = root_item.first_child() {
|
||||
descendent_candidates.push(child);
|
||||
}
|
||||
|
||||
core::iter::from_fn(move || {
|
||||
loop {
|
||||
let candidate = descendent_candidates.pop()?;
|
||||
|
||||
if let Some(next_candidate) = candidate.next_sibling() {
|
||||
descendent_candidates.push(next_candidate);
|
||||
}
|
||||
|
||||
if let Some(descendent) = try_candidate_or_find_next_accessible_descendent(
|
||||
candidate,
|
||||
&mut descendent_candidates,
|
||||
) {
|
||||
return Some(descendent);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Find the first built-in `TextInput` in the descendents of `item`.
|
||||
pub fn find_text_input(item: &ItemRc) -> Option<VRcMapped<ItemTreeVTable, TextInput>> {
|
||||
fn try_candidate_or_find_next_descendent(
|
||||
candidate: ItemRc,
|
||||
descendent_candidates: &mut Vec<ItemRc>,
|
||||
) -> Option<VRcMapped<ItemTreeVTable, TextInput>> {
|
||||
if let Some(input) = candidate.downcast::<TextInput>() {
|
||||
return Some(input);
|
||||
}
|
||||
|
||||
candidate.first_child().and_then(|child| {
|
||||
if let Some(next) = child.next_sibling() {
|
||||
descendent_candidates.push(next);
|
||||
}
|
||||
try_candidate_or_find_next_descendent(child, descendent_candidates)
|
||||
})
|
||||
}
|
||||
|
||||
let mut descendent_candidates = vec![item.clone()];
|
||||
|
||||
loop {
|
||||
let candidate = descendent_candidates.pop()?;
|
||||
|
||||
if let Some(next_candidate) = candidate.next_sibling() {
|
||||
descendent_candidates.push(next_candidate);
|
||||
}
|
||||
|
||||
if let Some(input) =
|
||||
try_candidate_or_find_next_descendent(candidate, &mut descendent_candidates)
|
||||
{
|
||||
return Some(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
#![warn(missing_docs)]
|
||||
//! The animation system
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use core::cell::Cell;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use num_traits::Float;
|
||||
pub(crate) mod physics_simulation;
|
||||
|
||||
mod cubic_bezier {
|
||||
//! This is a copy from lyon_algorithms::geom::cubic_bezier implementation
|
||||
//! (from lyon_algorithms 0.17)
|
||||
type S = f32;
|
||||
use euclid::default::Point2D as Point;
|
||||
#[allow(unused)]
|
||||
use num_traits::Float;
|
||||
trait Scalar {
|
||||
const ONE: f32 = 1.;
|
||||
const THREE: f32 = 3.;
|
||||
const HALF: f32 = 0.5;
|
||||
const SIX: f32 = 6.;
|
||||
const NINE: f32 = 9.;
|
||||
fn value(v: f32) -> f32 {
|
||||
v
|
||||
}
|
||||
}
|
||||
impl Scalar for f32 {}
|
||||
pub struct CubicBezierSegment {
|
||||
pub from: Point<S>,
|
||||
pub ctrl1: Point<S>,
|
||||
pub ctrl2: Point<S>,
|
||||
pub to: Point<S>,
|
||||
}
|
||||
|
||||
impl CubicBezierSegment {
|
||||
/// Sample the x coordinate of the curve at t (expecting t between 0 and 1).
|
||||
pub fn x(&self, t: S) -> S {
|
||||
let t2 = t * t;
|
||||
let t3 = t2 * t;
|
||||
let one_t = S::ONE - t;
|
||||
let one_t2 = one_t * one_t;
|
||||
let one_t3 = one_t2 * one_t;
|
||||
|
||||
self.from.x * one_t3
|
||||
+ self.ctrl1.x * S::THREE * one_t2 * t
|
||||
+ self.ctrl2.x * S::THREE * one_t * t2
|
||||
+ self.to.x * t3
|
||||
}
|
||||
|
||||
/// Sample the y coordinate of the curve at t (expecting t between 0 and 1).
|
||||
pub fn y(&self, t: S) -> S {
|
||||
let t2 = t * t;
|
||||
let t3 = t2 * t;
|
||||
let one_t = S::ONE - t;
|
||||
let one_t2 = one_t * one_t;
|
||||
let one_t3 = one_t2 * one_t;
|
||||
|
||||
self.from.y * one_t3
|
||||
+ self.ctrl1.y * S::THREE * one_t2 * t
|
||||
+ self.ctrl2.y * S::THREE * one_t * t2
|
||||
+ self.to.y * t3
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn derivative_coefficients(&self, t: S) -> (S, S, S, S) {
|
||||
let t2 = t * t;
|
||||
(
|
||||
-S::THREE * t2 + S::SIX * t - S::THREE,
|
||||
S::NINE * t2 - S::value(12.0) * t + S::THREE,
|
||||
-S::NINE * t2 + S::SIX * t,
|
||||
S::THREE * t2,
|
||||
)
|
||||
}
|
||||
|
||||
/// Sample the x coordinate of the curve's derivative at t (expecting t between 0 and 1).
|
||||
pub fn dx(&self, t: S) -> S {
|
||||
let (c0, c1, c2, c3) = self.derivative_coefficients(t);
|
||||
self.from.x * c0 + self.ctrl1.x * c1 + self.ctrl2.x * c2 + self.to.x * c3
|
||||
}
|
||||
}
|
||||
|
||||
impl CubicBezierSegment {
|
||||
// This is actually in the Monotonic<CubicBezierSegment<S>> impl
|
||||
pub fn solve_t_for_x(&self, x: S, t_range: core::ops::Range<S>, tolerance: S) -> S {
|
||||
debug_assert!(t_range.start <= t_range.end);
|
||||
let from = self.x(t_range.start);
|
||||
let to = self.x(t_range.end);
|
||||
if x <= from {
|
||||
return t_range.start;
|
||||
}
|
||||
if x >= to {
|
||||
return t_range.end;
|
||||
}
|
||||
|
||||
// Newton's method.
|
||||
let mut t = x - from / (to - from);
|
||||
for _ in 0..8 {
|
||||
let x2 = self.x(t);
|
||||
|
||||
if S::abs(x2 - x) <= tolerance {
|
||||
return t;
|
||||
}
|
||||
|
||||
let dx = self.dx(t);
|
||||
|
||||
if dx <= S::EPSILON {
|
||||
break;
|
||||
}
|
||||
|
||||
t -= (x2 - x) / dx;
|
||||
}
|
||||
|
||||
// Fall back to binary search.
|
||||
let mut min = t_range.start;
|
||||
let mut max = t_range.end;
|
||||
let mut t = S::HALF;
|
||||
|
||||
while min < max {
|
||||
let x2 = self.x(t);
|
||||
|
||||
if S::abs(x2 - x) < tolerance {
|
||||
return t;
|
||||
}
|
||||
|
||||
if x > x2 {
|
||||
min = t;
|
||||
} else {
|
||||
max = t;
|
||||
}
|
||||
|
||||
t = (max - min) * S::HALF + min;
|
||||
}
|
||||
|
||||
t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The representation of an easing curve, for animations
|
||||
#[repr(C, u32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub enum EasingCurve {
|
||||
/// The linear curve
|
||||
#[default]
|
||||
Linear,
|
||||
/// A Cubic bezier curve, with its 4 parameters
|
||||
CubicBezier([f32; 4]),
|
||||
/// Easing curve as defined at: <https://easings.net/#easeInElastic>
|
||||
EaseInElastic,
|
||||
/// Easing curve as defined at: <https://easings.net/#easeOutElastic>
|
||||
EaseOutElastic,
|
||||
/// Easing curve as defined at: <https://easings.net/#easeInOutElastic>
|
||||
EaseInOutElastic,
|
||||
/// Easing curve as defined at: <https://easings.net/#easeInBounce>
|
||||
EaseInBounce,
|
||||
/// Easing curve as defined at: <https://easings.net/#easeOutBounce>
|
||||
EaseOutBounce,
|
||||
/// Easing curve as defined at: <https://easings.net/#easeInOutBounce>
|
||||
EaseInOutBounce,
|
||||
// Custom(Box<dyn Fn(f32) -> f32>),
|
||||
}
|
||||
|
||||
/// Represent an instant, in milliseconds since the AnimationDriver's initial_instant
|
||||
#[repr(transparent)]
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Ord, PartialOrd, Eq)]
|
||||
pub struct Instant(pub u64);
|
||||
|
||||
impl core::ops::Sub<Instant> for Instant {
|
||||
type Output = core::time::Duration;
|
||||
fn sub(self, other: Self) -> core::time::Duration {
|
||||
core::time::Duration::from_millis(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Sub<core::time::Duration> for Instant {
|
||||
type Output = Instant;
|
||||
fn sub(self, other: core::time::Duration) -> Instant {
|
||||
Self(self.0 - other.as_millis() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Add<core::time::Duration> for Instant {
|
||||
type Output = Instant;
|
||||
fn add(self, other: core::time::Duration) -> Instant {
|
||||
Self(self.0 + other.as_millis() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::AddAssign<core::time::Duration> for Instant {
|
||||
fn add_assign(&mut self, other: core::time::Duration) {
|
||||
self.0 += other.as_millis() as u64;
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::SubAssign<core::time::Duration> for Instant {
|
||||
fn sub_assign(&mut self, other: core::time::Duration) {
|
||||
self.0 -= other.as_millis() as u64;
|
||||
}
|
||||
}
|
||||
|
||||
impl Instant {
|
||||
/// Returns the amount of time elapsed since an other instant.
|
||||
///
|
||||
/// Equivalent to `self - earlier`
|
||||
pub fn duration_since(self, earlier: Instant) -> core::time::Duration {
|
||||
self - earlier
|
||||
}
|
||||
|
||||
/// Wrapper around [`std::time::Instant::now()`] that delegates to the backend
|
||||
/// and allows working in no_std environments.
|
||||
pub fn now() -> Self {
|
||||
Self(Self::duration_since_start().as_millis() as u64)
|
||||
}
|
||||
|
||||
fn duration_since_start() -> core::time::Duration {
|
||||
crate::context::GLOBAL_CONTEXT
|
||||
.with(|p| p.get().map(|p| p.platform().duration_since_start()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Return the number of milliseconds this `Instant` is after the backend has started
|
||||
pub fn as_millis(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The AnimationDriver
|
||||
pub struct AnimationDriver {
|
||||
/// Indicate whether there are any active animations that require a future call to update_animations.
|
||||
active_animations: Cell<bool>,
|
||||
global_instant: core::pin::Pin<Box<crate::Property<Instant>>>,
|
||||
}
|
||||
|
||||
impl Default for AnimationDriver {
|
||||
fn default() -> Self {
|
||||
AnimationDriver {
|
||||
active_animations: Cell::default(),
|
||||
global_instant: Box::pin(crate::Property::new_named(
|
||||
Instant::default(),
|
||||
"i_slint_core::AnimationDriver::global_instant",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationDriver {
|
||||
/// Iterates through all animations based on the new time tick and updates their state. This should be called by
|
||||
/// the windowing system driver for every frame.
|
||||
pub fn update_animations(&self, new_tick: Instant) {
|
||||
let current_tick = self.global_instant.as_ref().get_untracked();
|
||||
assert!(current_tick <= new_tick, "The platform's clock is not monotonic!");
|
||||
if current_tick != new_tick {
|
||||
self.active_animations.set(false);
|
||||
self.global_instant.as_ref().set(new_tick);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if there are any active or ready animations. This is used by the windowing system to determine
|
||||
/// if a new animation frame is required or not. Returns false otherwise.
|
||||
pub fn has_active_animations(&self) -> bool {
|
||||
self.active_animations.get()
|
||||
}
|
||||
|
||||
/// Tell the driver that there are active animations
|
||||
pub fn set_has_active_animations(&self) {
|
||||
self.active_animations.set(true);
|
||||
}
|
||||
/// The current instant that is to be used for animation
|
||||
/// using this function register the current binding as a dependency
|
||||
pub fn current_tick(&self) -> Instant {
|
||||
self.global_instant.as_ref().get()
|
||||
}
|
||||
}
|
||||
|
||||
crate::thread_local!(
|
||||
/// This is the default instance of the animation driver that's used to advance all property animations
|
||||
/// at the same time.
|
||||
pub static CURRENT_ANIMATION_DRIVER : AnimationDriver = AnimationDriver::default()
|
||||
);
|
||||
|
||||
/// The current instant that is to be used for animation
|
||||
/// using this function register the current binding as a dependency
|
||||
pub fn current_tick() -> Instant {
|
||||
CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick())
|
||||
}
|
||||
|
||||
/// Same as [`current_tick`], but also register that one should be running animation
|
||||
/// on next frame
|
||||
pub fn animation_tick() -> u64 {
|
||||
CURRENT_ANIMATION_DRIVER.with(|driver| {
|
||||
driver.set_has_active_animations();
|
||||
driver.current_tick().0
|
||||
})
|
||||
}
|
||||
|
||||
fn ease_out_bounce_curve(value: f32) -> f32 {
|
||||
const N1: f32 = 7.5625;
|
||||
const D1: f32 = 2.75;
|
||||
|
||||
if value < 1.0 / D1 {
|
||||
N1 * value * value
|
||||
} else if value < 2.0 / D1 {
|
||||
let value = value - (1.5 / D1);
|
||||
N1 * value * value + 0.75
|
||||
} else if value < 2.5 / D1 {
|
||||
let value = value - (2.25 / D1);
|
||||
N1 * value * value + 0.9375
|
||||
} else {
|
||||
let value = value - (2.625 / D1);
|
||||
N1 * value * value + 0.984375
|
||||
}
|
||||
}
|
||||
|
||||
/// map a value between 0 and 1 to another value between 0 and 1 according to the curve
|
||||
pub fn easing_curve(curve: &EasingCurve, value: f32) -> f32 {
|
||||
match curve {
|
||||
EasingCurve::Linear => value,
|
||||
EasingCurve::CubicBezier([a, b, c, d]) => {
|
||||
if !(0.0..=1.0).contains(a) && !(0.0..=1.0).contains(c) {
|
||||
return value;
|
||||
};
|
||||
let curve = cubic_bezier::CubicBezierSegment {
|
||||
from: (0., 0.).into(),
|
||||
ctrl1: (*a, *b).into(),
|
||||
ctrl2: (*c, *d).into(),
|
||||
to: (1., 1.).into(),
|
||||
};
|
||||
curve.y(curve.solve_t_for_x(value, 0.0..1.0, 0.01))
|
||||
}
|
||||
EasingCurve::EaseInElastic => {
|
||||
const C4: f32 = 2.0 * core::f32::consts::PI / 3.0;
|
||||
|
||||
if value == 0.0 {
|
||||
0.0
|
||||
} else if value == 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
-f32::powf(2.0, 10.0 * value - 10.0) * f32::sin((value * 10.0 - 10.75) * C4)
|
||||
}
|
||||
}
|
||||
EasingCurve::EaseOutElastic => {
|
||||
let c4 = (2.0 * core::f32::consts::PI) / 3.0;
|
||||
|
||||
if value == 0.0 {
|
||||
0.0
|
||||
} else if value == 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
2.0f32.powf(-10.0 * value) * ((value * 10.0 - 0.75) * c4).sin() + 1.0
|
||||
}
|
||||
}
|
||||
EasingCurve::EaseInOutElastic => {
|
||||
const C5: f32 = 2.0 * core::f32::consts::PI / 4.5;
|
||||
|
||||
if value == 0.0 {
|
||||
0.0
|
||||
} else if value == 1.0 {
|
||||
1.0
|
||||
} else if value < 0.5 {
|
||||
-(f32::powf(2.0, 20.0 * value - 10.0) * f32::sin((20.0 * value - 11.125) * C5))
|
||||
/ 2.0
|
||||
} else {
|
||||
(f32::powf(2.0, -20.0 * value + 10.0) * f32::sin((20.0 * value - 11.125) * C5))
|
||||
/ 2.0
|
||||
+ 1.0
|
||||
}
|
||||
}
|
||||
EasingCurve::EaseInBounce => 1.0 - ease_out_bounce_curve(1.0 - value),
|
||||
EasingCurve::EaseOutBounce => ease_out_bounce_curve(value),
|
||||
EasingCurve::EaseInOutBounce => {
|
||||
if value < 0.5 {
|
||||
(1.0 - ease_out_bounce_curve(1.0 - 2.0 * value)) / 2.0
|
||||
} else {
|
||||
(1.0 + ease_out_bounce_curve(2.0 * value - 1.0)) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn easing_test() {
|
||||
fn test_curve(name: &str, curve: &EasingCurve) {
|
||||
let mut img = image::ImageBuffer::new(500, 500);
|
||||
let white = image::Rgba([255 as u8, 255 as u8, 255 as u8, 255 as u8]);
|
||||
|
||||
for x in 0..img.width() {
|
||||
let t = (x as f32) / (img.width() as f32);
|
||||
let y = easing_curve(curve, t);
|
||||
let y = (y * (img.height() as f32)) as u32;
|
||||
let y = y.min(img.height() - 1);
|
||||
*img.get_pixel_mut(x, img.height() - 1 - y) = white;
|
||||
}
|
||||
|
||||
img.save(
|
||||
std::path::PathBuf::from(std::env::var_os("HOME").unwrap())
|
||||
.join(format!("{}.png", name)),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
test_curve("linear", &EasingCurve::Linear);
|
||||
test_curve("linear2", &EasingCurve::CubicBezier([0.0, 0.0, 1.0, 1.0]));
|
||||
test_curve("ease", &EasingCurve::CubicBezier([0.25, 0.1, 0.25, 1.0]));
|
||||
test_curve("ease_in", &EasingCurve::CubicBezier([0.42, 0.0, 1.0, 1.0]));
|
||||
test_curve("ease_in_out", &EasingCurve::CubicBezier([0.42, 0.0, 0.58, 1.0]));
|
||||
test_curve("ease_out", &EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]));
|
||||
}
|
||||
*/
|
||||
|
||||
/// Update the global animation time to the current time
|
||||
pub fn update_animations() {
|
||||
CURRENT_ANIMATION_DRIVER.with(|driver| {
|
||||
#[allow(unused_mut)]
|
||||
let mut duration = Instant::duration_since_start().as_millis() as u64;
|
||||
#[cfg(feature = "std")]
|
||||
if let Ok(val) = std::env::var("SLINT_SLOW_ANIMATIONS") {
|
||||
let factor = val.parse().unwrap_or(2);
|
||||
duration /= factor;
|
||||
};
|
||||
driver.update_animations(Instant(duration))
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,823 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! This module contains varios physics simulations which can be used as animation (internally only yet).
|
||||
//! Currently it is used in the flickable to animate the viewport position
|
||||
//!
|
||||
//! Currently it contains two simulations:
|
||||
//! - `ConstantDeceleration`
|
||||
//! - `ConstantDecelerationSpringDamper` with spring damper simulation when reaching the limit
|
||||
|
||||
use crate::animations::Instant;
|
||||
#[cfg(all(test, not(feature = "std")))]
|
||||
use num_traits::Float;
|
||||
|
||||
/// The direction the simulation is running
|
||||
#[derive(Debug)]
|
||||
enum Direction {
|
||||
/// The start value is smaller than the limit value
|
||||
Increasing,
|
||||
/// The start value is larger than the limit value
|
||||
Decreasing,
|
||||
}
|
||||
|
||||
/// Common simulation trait
|
||||
/// All simulations must implement this trait
|
||||
pub trait Simulation {
|
||||
fn step(&mut self, new_tick: Instant) -> (f32, bool);
|
||||
fn curr_value(&self) -> f32;
|
||||
}
|
||||
|
||||
/// Trait to convert parameter objects into a simulation
|
||||
/// All parameter objects must implement this trait!
|
||||
pub trait Parameter {
|
||||
type Output;
|
||||
fn simulation(self, start_value: f32, limit_value: f32) -> Self::Output;
|
||||
}
|
||||
|
||||
/// Input parameters for the `ConstantDeceleration` simulation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConstantDecelerationParameters {
|
||||
pub initial_velocity: f32,
|
||||
pub deceleration: f32,
|
||||
}
|
||||
|
||||
impl ConstantDecelerationParameters {
|
||||
pub fn new(initial_velocity: f32, deceleration: f32) -> Self {
|
||||
Self { initial_velocity, deceleration }
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter for ConstantDecelerationParameters {
|
||||
type Output = ConstantDeceleration;
|
||||
fn simulation(self, start_value: f32, limit_value: f32) -> Self::Output {
|
||||
ConstantDeceleration::new(start_value, limit_value, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// This simulation simulates a constant deceleration of a point starting at position `start_value` with
|
||||
/// an initial velocity of `initial_velocity`. When the point reaches the limit value `limit_value` it stops there
|
||||
#[derive(Debug)]
|
||||
pub struct ConstantDeceleration {
|
||||
/// If the limit is not reached, it is also fine. Also exceeding the limit can be ok,
|
||||
/// but at the end of the animation the limit shall not be exceeded
|
||||
limit_value: f32,
|
||||
curr_val: f32,
|
||||
velocity: f32,
|
||||
data: ConstantDecelerationParameters,
|
||||
direction: Direction,
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl ConstantDeceleration {
|
||||
/// Create a new ConstantDeceleration simulation
|
||||
///
|
||||
/// * `start_value` - start position
|
||||
/// * `limit_value` - value at which the simulation ends if the velocity did not get zero before
|
||||
/// * `initial_velocity` - the initial velocity of the point
|
||||
/// * `data` - the properties of this simulation
|
||||
pub fn new(start_value: f32, limit_value: f32, data: ConstantDecelerationParameters) -> Self {
|
||||
Self::new_internal(start_value, limit_value, data, crate::animations::current_tick())
|
||||
}
|
||||
|
||||
fn new_internal(
|
||||
start_value: f32,
|
||||
limit_value: f32,
|
||||
mut data: ConstantDecelerationParameters,
|
||||
start_time: Instant,
|
||||
) -> Self {
|
||||
let mut initial_velocity = data.initial_velocity;
|
||||
let direction = if start_value == limit_value {
|
||||
if initial_velocity >= 0. {
|
||||
data.deceleration = f32::abs(data.deceleration);
|
||||
Direction::Increasing
|
||||
} else {
|
||||
data.deceleration = -f32::abs(data.deceleration);
|
||||
Direction::Decreasing
|
||||
}
|
||||
} else if start_value < limit_value {
|
||||
data.deceleration = f32::abs(data.deceleration);
|
||||
assert!(initial_velocity >= 0.); // Makes no sense yet that the velocity goes into the other direction
|
||||
initial_velocity = f32::abs(initial_velocity);
|
||||
Direction::Increasing
|
||||
} else {
|
||||
data.deceleration = -f32::abs(data.deceleration);
|
||||
initial_velocity = -f32::abs(initial_velocity);
|
||||
assert!(initial_velocity <= 0.);
|
||||
Direction::Decreasing
|
||||
};
|
||||
|
||||
Self {
|
||||
limit_value,
|
||||
curr_val: start_value,
|
||||
velocity: initial_velocity,
|
||||
data,
|
||||
direction,
|
||||
start_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn step_internal(&mut self, new_tick: Instant) -> (f32, bool) {
|
||||
// We have to prevent go go beyond the limit where velocity gets zero
|
||||
let duration = f32::min(
|
||||
new_tick.duration_since(self.start_time).as_secs_f32(),
|
||||
f32::abs(self.velocity / self.data.deceleration),
|
||||
);
|
||||
|
||||
self.start_time = new_tick;
|
||||
|
||||
let new_velocity = self.velocity - duration * self.data.deceleration;
|
||||
|
||||
self.curr_val += duration * (self.velocity + new_velocity) / 2.; // Trapezoidal integration
|
||||
self.velocity = new_velocity;
|
||||
|
||||
match self.direction {
|
||||
Direction::Increasing => {
|
||||
if self.curr_val >= self.limit_value {
|
||||
self.curr_val = self.limit_value;
|
||||
self.velocity = 0.;
|
||||
return (self.curr_val, true);
|
||||
} else if self.velocity <= 0. {
|
||||
return (self.curr_val, true);
|
||||
}
|
||||
}
|
||||
Direction::Decreasing => {
|
||||
if self.curr_val <= self.limit_value {
|
||||
self.curr_val = self.limit_value;
|
||||
self.velocity = 0.;
|
||||
return (self.curr_val, true);
|
||||
} else if self.velocity >= 0. {
|
||||
return (self.curr_val, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
(self.curr_val, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Simulation for ConstantDeceleration {
|
||||
fn curr_value(&self) -> f32 {
|
||||
self.curr_val
|
||||
}
|
||||
|
||||
fn step(&mut self, new_tick: Instant) -> (f32, bool) {
|
||||
self.step_internal(new_tick)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn constant_deceleration_start_eq_limit() {
|
||||
const START_VALUE: f32 = 10.;
|
||||
const LIMIT_VALUE: f32 = 10.;
|
||||
const INITIAL_VELOCITY: f32 = 50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
|
||||
|
||||
let time = Instant::now();
|
||||
let mut simulation =
|
||||
ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone());
|
||||
|
||||
let res = simulation.step(time + Duration::from_hours(10));
|
||||
assert_eq!(res.1, true);
|
||||
assert_eq!(res.0, START_VALUE);
|
||||
}
|
||||
|
||||
/// The velocity becomes zero before we are reaching the limit
|
||||
/// start_value < limit_value
|
||||
#[test]
|
||||
fn constant_deceleration_increasing_limit_not_reached() {
|
||||
const START_VALUE: f32 = 10.;
|
||||
const LIMIT_VALUE: f32 = 2000.;
|
||||
const INITIAL_VELOCITY: f32 = 50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
|
||||
|
||||
let mut time = Instant::now();
|
||||
let mut simulation =
|
||||
ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone());
|
||||
|
||||
// Velocity does not become zero
|
||||
let mut duration = Duration::from_secs(1);
|
||||
assert!(DECELERATION * duration.as_secs_f32() < INITIAL_VELOCITY);
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, false);
|
||||
assert_eq!(
|
||||
res,
|
||||
START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32()
|
||||
- 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
|
||||
);
|
||||
|
||||
// Now the velocity becomes zero and we don't do any further calculations
|
||||
duration = Duration::from_hours(10);
|
||||
assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, true);
|
||||
assert_eq!(
|
||||
res,
|
||||
START_VALUE + INITIAL_VELOCITY * INITIAL_VELOCITY / DECELERATION
|
||||
- 0.5 * DECELERATION * (INITIAL_VELOCITY / DECELERATION).powi(2)
|
||||
);
|
||||
|
||||
assert!(res < LIMIT_VALUE); // We reached velocity zero before we reached the position limit
|
||||
}
|
||||
|
||||
/// We reach the position limit before the velocity got zero
|
||||
#[test]
|
||||
fn constant_deceleration_increasing_limit_reached() {
|
||||
const START_VALUE: f32 = 10.;
|
||||
const LIMIT_VALUE: f32 = 20.;
|
||||
const INITIAL_VELOCITY: f32 = 50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
|
||||
|
||||
let mut time = Instant::now();
|
||||
let mut simulation =
|
||||
ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone());
|
||||
|
||||
let duration = Duration::from_secs(1);
|
||||
assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, true);
|
||||
assert_eq!(res, LIMIT_VALUE); // Limit reached
|
||||
}
|
||||
|
||||
/// We don't reach the position limit. Before the velocity gets zero
|
||||
/// start_value > limit_value
|
||||
#[test]
|
||||
fn constant_deceleration_decreasing_limit_not_reached() {
|
||||
const START_VALUE: f32 = 2000.;
|
||||
const LIMIT_VALUE: f32 = 10.;
|
||||
const INITIAL_VELOCITY: f32 = -50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
|
||||
let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
|
||||
|
||||
let mut time = Instant::now();
|
||||
let mut simulation =
|
||||
ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone());
|
||||
|
||||
let mut duration = Duration::from_secs(1);
|
||||
assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY));
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, false);
|
||||
assert_eq!(
|
||||
res,
|
||||
START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32()
|
||||
- INITIAL_VELOCITY.signum() * 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
|
||||
);
|
||||
|
||||
duration = Duration::from_hours(10);
|
||||
assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, true);
|
||||
assert_eq!(
|
||||
res,
|
||||
START_VALUE + INITIAL_VELOCITY * f32::abs(INITIAL_VELOCITY / DECELERATION)
|
||||
- 0.5
|
||||
* INITIAL_VELOCITY.signum()
|
||||
* DECELERATION
|
||||
* (INITIAL_VELOCITY / DECELERATION).powi(2)
|
||||
);
|
||||
|
||||
assert!(res > LIMIT_VALUE); // We reached velocity zero before we reached the position limit
|
||||
}
|
||||
|
||||
/// We reach the position limit before the velocity got zero
|
||||
/// start_value > limit_value
|
||||
#[test]
|
||||
fn constant_deceleration_decreasing_limit_reached() {
|
||||
const START_VALUE: f32 = 20.;
|
||||
const LIMIT_VALUE: f32 = 10.;
|
||||
const INITIAL_VELOCITY: f32 = -50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
|
||||
|
||||
let mut time = Instant::now();
|
||||
let mut simulation =
|
||||
ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone());
|
||||
|
||||
let duration = Duration::from_secs(3);
|
||||
assert!(f32::abs(DECELERATION * duration.as_secs_f32()) > f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, true);
|
||||
assert_eq!(res, LIMIT_VALUE); // Limit reached
|
||||
}
|
||||
}
|
||||
|
||||
/// Input parameters for the `ConstantDecelerationSpringDamper` simulation
|
||||
/// [1] https://www.maplesoft.com/content/EngineeringFundamentals/6/MapleDocument_32/Free%20Response%20Part%202.pdf
|
||||
#[cfg(test)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConstantDecelerationSpringDamperParameters {
|
||||
pub initial_velocity: f32,
|
||||
pub deceleration: f32,
|
||||
pub mass: f32, // [1] parameter m
|
||||
pub spring_constant: f32, // [1] parameter k
|
||||
pub damping_coefficient: f32, // [1] parameter c
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl ConstantDecelerationSpringDamperParameters {
|
||||
/// Creates a new `ConstantDecelerationSpringDamperParameters` parameter object
|
||||
/// It is more comfortable to use than specifiying the parameters manually because here the parameter calculation
|
||||
/// is done based on the `half_period_time` parameter
|
||||
///
|
||||
/// * `initial_velocity` - the initial velocity of the point
|
||||
/// * `deceleration` - the constant deceleration of the point
|
||||
/// * `half_period_time` - the time of the simulation when the limit value got exceeded to return back to it
|
||||
pub fn new(initial_velocity: f32, deceleration: f32, half_period_time: f32) -> Self {
|
||||
let (mass, spring_constant, damping_coefficient) =
|
||||
Self::calculate_parameters(half_period_time);
|
||||
|
||||
Self { initial_velocity, deceleration, mass, spring_constant, damping_coefficient }
|
||||
}
|
||||
|
||||
fn calculate_parameters(half_period_time: f32) -> (f32, f32, f32) {
|
||||
// [1] eq 13
|
||||
const MASS: f32 = 1.;
|
||||
const DAMPING_COEFFICIENT: f32 = 1.;
|
||||
let w_d = 2. * core::f32::consts::PI * 1. / (2. * half_period_time);
|
||||
let spring_constant = w_d.powi(2) + DAMPING_COEFFICIENT.powi(2) / (4. * MASS.powi(2));
|
||||
|
||||
(MASS, spring_constant, DAMPING_COEFFICIENT)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Parameter for ConstantDecelerationSpringDamperParameters {
|
||||
type Output = ConstantDecelerationSpringDamper;
|
||||
fn simulation(self, start_value: f32, limit_value: f32) -> Self::Output {
|
||||
ConstantDecelerationSpringDamper::new(start_value, limit_value, self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum State {
|
||||
Deceleration,
|
||||
SpringDamper,
|
||||
Done,
|
||||
}
|
||||
|
||||
/// This simulation simulates a constant deceleration of a point starting at position `start_value` with
|
||||
/// an initial velocity of `initial_velocity`. When the point reaches the limit value `limit_value` before
|
||||
/// the velocity reaches zero, the system simulates a spring damper system to go shortly beyond the limit
|
||||
/// value and returning then back
|
||||
#[cfg(test)]
|
||||
#[derive(Debug)]
|
||||
pub struct ConstantDecelerationSpringDamper {
|
||||
/// If the limit is not reached, it is also fine. Also exceeding the limit can be ok,
|
||||
/// but at the end of the animation the limit shall not be exceeded
|
||||
limit_value: f32,
|
||||
curr_val_zeroed: f32,
|
||||
curr_val: f32,
|
||||
velocity: f32,
|
||||
data: ConstantDecelerationSpringDamperParameters,
|
||||
direction: Direction,
|
||||
start_time: Instant,
|
||||
state: State,
|
||||
damping_ratio: f32,
|
||||
/// Undamped natural frequency
|
||||
w_n: f32,
|
||||
/// Damped natural frequency
|
||||
w_d: f32,
|
||||
constant_a: f32,
|
||||
constant_phi: f32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl ConstantDecelerationSpringDamper {
|
||||
pub fn new(
|
||||
start_value: f32,
|
||||
limit_value: f32,
|
||||
data: ConstantDecelerationSpringDamperParameters,
|
||||
) -> Self {
|
||||
Self::new_internal(start_value, limit_value, data, crate::animations::current_tick())
|
||||
}
|
||||
|
||||
fn new_internal(
|
||||
start_value: f32,
|
||||
limit_value: f32,
|
||||
mut data: ConstantDecelerationSpringDamperParameters,
|
||||
start_time: Instant,
|
||||
) -> Self {
|
||||
let mut initial_velocity = data.initial_velocity;
|
||||
let mut state = State::Deceleration;
|
||||
let direction = if start_value == limit_value {
|
||||
state = State::Done;
|
||||
if initial_velocity >= 0. {
|
||||
data.deceleration = f32::abs(data.deceleration);
|
||||
Direction::Increasing
|
||||
} else {
|
||||
data.deceleration = -f32::abs(data.deceleration);
|
||||
Direction::Decreasing
|
||||
}
|
||||
} else if start_value < limit_value {
|
||||
data.deceleration = f32::abs(data.deceleration);
|
||||
assert!(initial_velocity >= 0.); // Makes no sense yet that the velocity goes into the other direction
|
||||
initial_velocity = f32::abs(initial_velocity);
|
||||
Direction::Increasing
|
||||
} else {
|
||||
data.deceleration = -f32::abs(data.deceleration);
|
||||
initial_velocity = -f32::abs(initial_velocity);
|
||||
assert!(initial_velocity <= 0.);
|
||||
Direction::Decreasing
|
||||
};
|
||||
|
||||
assert!(data.mass > 0.);
|
||||
assert!(data.spring_constant >= 0.);
|
||||
|
||||
let c_cr = 2. * f32::sqrt(data.mass * data.spring_constant); // Critical damping coefficient
|
||||
let damping_ratio = data.damping_coefficient / c_cr;
|
||||
assert!(damping_ratio > 0.);
|
||||
assert!(damping_ratio < 1.); // Currently we support only the underdamped motion, because we wanna return to the `limit_value`
|
||||
|
||||
let w_n = c_cr / (2. * data.mass);
|
||||
let w_d = w_n * f32::sqrt(1. - damping_ratio.powi(2));
|
||||
|
||||
Self {
|
||||
limit_value,
|
||||
curr_val: start_value,
|
||||
curr_val_zeroed: 0.,
|
||||
velocity: initial_velocity,
|
||||
data,
|
||||
direction,
|
||||
start_time,
|
||||
state,
|
||||
damping_ratio,
|
||||
w_n,
|
||||
w_d,
|
||||
constant_a: 0., // Calculated when transitioning to the damper spring state
|
||||
constant_phi: 0., // Calculated when transitioning to the damper spring state
|
||||
}
|
||||
}
|
||||
|
||||
fn step_internal(&mut self, new_tick: Instant) -> (f32, bool) {
|
||||
match self.state {
|
||||
State::Deceleration => self.state_deceleration(new_tick),
|
||||
State::SpringDamper => self.state_spring_damper(new_tick),
|
||||
State::Done => (self.curr_value(), true),
|
||||
}
|
||||
}
|
||||
|
||||
fn state_deceleration(&mut self, new_tick: Instant) -> (f32, bool) {
|
||||
let duration_unlimited = new_tick.duration_since(self.start_time);
|
||||
// We have to prevent go go beyond the limit where velocity gets zero
|
||||
let duration = f32::min(
|
||||
duration_unlimited.as_secs_f32(),
|
||||
f32::abs(self.velocity / self.data.deceleration),
|
||||
);
|
||||
|
||||
self.start_time = new_tick;
|
||||
|
||||
let new_velocity = self.velocity - (duration * self.data.deceleration);
|
||||
let new_val = self.curr_val + (duration * (self.velocity + new_velocity) / 2.); // Trapezoidal integration
|
||||
|
||||
enum S {
|
||||
LimitReached,
|
||||
VelocityZero,
|
||||
None,
|
||||
}
|
||||
|
||||
let s = match self.direction {
|
||||
Direction::Increasing if new_val > self.limit_value => S::LimitReached,
|
||||
Direction::Increasing if new_velocity <= 0. => S::VelocityZero,
|
||||
Direction::Decreasing if new_val < self.limit_value => S::LimitReached,
|
||||
Direction::Decreasing if new_velocity >= 0. => S::VelocityZero,
|
||||
_ => S::None,
|
||||
};
|
||||
match s {
|
||||
S::LimitReached => {
|
||||
self.state = State::SpringDamper;
|
||||
|
||||
// time when reaching the limit
|
||||
// solving p_limit = p_old + v_old * dt - 0.5 * a * dt^2
|
||||
let root = f32::sqrt(
|
||||
self.velocity.powi(2)
|
||||
- self.data.deceleration * (self.limit_value - self.curr_val) as f32,
|
||||
);
|
||||
// The smaller is the relevant. The larger is when the initial velocity got zero and due to the constant acceleration we turn
|
||||
let dt = f32::min(
|
||||
(self.velocity - root) / self.data.deceleration,
|
||||
(self.velocity + root) / self.data.deceleration,
|
||||
);
|
||||
|
||||
self.velocity -= dt * self.data.deceleration; // Velocity at limit value point. Solved `new_val` equation for new_velocity
|
||||
self.curr_val_zeroed = 0.;
|
||||
self.curr_val = self.limit_value;
|
||||
|
||||
const X0: f32 = 0.; // Relative point
|
||||
self.constant_a = self.velocity.signum()
|
||||
* f32::sqrt(
|
||||
(self.w_d.powi(2) * X0.powi(2)
|
||||
+ (self.velocity + self.damping_ratio * self.w_n * 0.).powi(2))
|
||||
/ self.w_d.powi(2),
|
||||
);
|
||||
self.constant_phi =
|
||||
f32::atan(self.w_d * X0 / (self.velocity + self.damping_ratio * self.w_n * X0));
|
||||
self.state_spring_damper(
|
||||
new_tick
|
||||
+ (duration_unlimited
|
||||
- core::time::Duration::from_millis((dt * 1000.) as u64)),
|
||||
)
|
||||
}
|
||||
S::VelocityZero => {
|
||||
self.velocity = 0.;
|
||||
self.curr_val = new_val;
|
||||
(self.curr_val, true)
|
||||
}
|
||||
S::None => {
|
||||
self.velocity = new_velocity;
|
||||
self.curr_val = new_val;
|
||||
(self.curr_val, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn state_spring_damper(&mut self, new_tick: Instant) -> (f32, bool) {
|
||||
// Here we use absolute time because it simplifies the equation
|
||||
let t = (new_tick - self.start_time).as_secs_f32();
|
||||
// Underdamped spring damper equation
|
||||
assert!(self.damping_ratio < 1.);
|
||||
let new_val = self.constant_a
|
||||
* f32::exp(-self.damping_ratio * self.w_n * t)
|
||||
* f32::sin(self.w_d * t + self.constant_phi);
|
||||
self.curr_val_zeroed = new_val; // relative value
|
||||
|
||||
let max_time = 2. * core::f32::consts::PI / self.w_d;
|
||||
let current_val = self.curr_value();
|
||||
let finished = match self.direction {
|
||||
Direction::Increasing => {
|
||||
// We are comming back from a value higher than the limit
|
||||
current_val < self.limit_value || t > max_time
|
||||
}
|
||||
Direction::Decreasing => {
|
||||
// We are comming back from a value lower than the limit
|
||||
current_val > self.limit_value || t > max_time
|
||||
}
|
||||
};
|
||||
if finished {
|
||||
self.velocity = 0.;
|
||||
self.curr_val = self.limit_value;
|
||||
self.curr_val_zeroed = 0.;
|
||||
self.state = State::Done;
|
||||
}
|
||||
(current_val, finished)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Simulation for ConstantDecelerationSpringDamper {
|
||||
fn curr_value(&self) -> f32 {
|
||||
self.curr_val + self.curr_val_zeroed
|
||||
}
|
||||
|
||||
fn step(&mut self, new_tick: Instant) -> (f32, bool) {
|
||||
self.step_internal(new_tick)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_spring_damper {
|
||||
use super::*;
|
||||
use core::{f32::consts::PI, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn calculate_parameters() {
|
||||
const INITIAL_VELOCITY: f32 = 50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
const HALF_PERIOD_TIME: f32 = 100e-3;
|
||||
let res = super::ConstantDecelerationSpringDamperParameters::new(
|
||||
INITIAL_VELOCITY,
|
||||
DECELERATION,
|
||||
HALF_PERIOD_TIME,
|
||||
);
|
||||
|
||||
let w_n = f32::sqrt(res.spring_constant * res.mass) / res.mass;
|
||||
let damping_ratio = res.damping_coefficient / (2. * res.mass * w_n);
|
||||
let w_d = w_n * f32::sqrt(1. - damping_ratio.powi(2));
|
||||
assert_eq!(w_d, 2. * PI * 1. / (2. * HALF_PERIOD_TIME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_deceleration_start_eq_limit() {
|
||||
const START_VALUE: f32 = 10.;
|
||||
const LIMIT_VALUE: f32 = 10.;
|
||||
const INITIAL_VELOCITY: f32 = 50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
const HALF_PERIOD_TIME: f32 = 100e-3;
|
||||
let parameters = ConstantDecelerationSpringDamperParameters::new(
|
||||
INITIAL_VELOCITY,
|
||||
DECELERATION,
|
||||
HALF_PERIOD_TIME,
|
||||
);
|
||||
|
||||
assert_eq!(START_VALUE, LIMIT_VALUE);
|
||||
let time = Instant::now();
|
||||
let mut simulation = ConstantDecelerationSpringDamper::new_internal(
|
||||
START_VALUE,
|
||||
LIMIT_VALUE,
|
||||
parameters,
|
||||
time.clone(),
|
||||
);
|
||||
let res = simulation.step(time);
|
||||
assert_eq!(res.0, START_VALUE);
|
||||
assert_eq!(res.1, true);
|
||||
assert_eq!(simulation.state, State::Done);
|
||||
}
|
||||
|
||||
/// The velocity becomes zero before we are reaching the limit
|
||||
/// start_value < limit_value
|
||||
#[test]
|
||||
fn constant_deceleration_increasing_limit_not_reached() {
|
||||
const INITIAL_VELOCITY: f32 = 50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
const HALF_PERIOD_TIME: f32 = 100e-3;
|
||||
let parameters = ConstantDecelerationSpringDamperParameters::new(
|
||||
INITIAL_VELOCITY,
|
||||
DECELERATION,
|
||||
HALF_PERIOD_TIME,
|
||||
);
|
||||
|
||||
let mut time = Instant::now();
|
||||
let mut simulation =
|
||||
ConstantDecelerationSpringDamper::new_internal(10., 2000., parameters, time.clone());
|
||||
|
||||
// Velocity does not become zero
|
||||
let mut duration = Duration::from_secs(1);
|
||||
assert!(DECELERATION * duration.as_secs_f32() < INITIAL_VELOCITY);
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, false);
|
||||
assert_eq!(
|
||||
res,
|
||||
10. + 50. * duration.as_secs_f32()
|
||||
- 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
|
||||
);
|
||||
|
||||
// Now the velocity becomes zero and we don't do any further calculations
|
||||
duration = Duration::from_hours(10);
|
||||
assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, true);
|
||||
assert_eq!(
|
||||
res,
|
||||
10. + 50. * INITIAL_VELOCITY / DECELERATION
|
||||
- 0.5 * DECELERATION * (INITIAL_VELOCITY / DECELERATION).powi(2)
|
||||
);
|
||||
|
||||
assert!(res < 2000.); // We reached velocity zero before we reached the position limit
|
||||
}
|
||||
|
||||
/// We don't reach the position limit. Before the velocity gets zero
|
||||
/// start_value > limit_value
|
||||
#[test]
|
||||
fn constant_deceleration_decreasing_limit_not_reached() {
|
||||
const START_VALUE: f32 = 2000.;
|
||||
const LIMIT_VALUE: f32 = 10.;
|
||||
const INITIAL_VELOCITY: f32 = -50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
const HALF_PERIOD_TIME: f32 = 100e-3;
|
||||
|
||||
let parameters = ConstantDecelerationSpringDamperParameters::new(
|
||||
INITIAL_VELOCITY,
|
||||
DECELERATION,
|
||||
HALF_PERIOD_TIME,
|
||||
);
|
||||
|
||||
let mut time = Instant::now();
|
||||
let mut simulation = ConstantDecelerationSpringDamper::new_internal(
|
||||
START_VALUE,
|
||||
LIMIT_VALUE,
|
||||
parameters,
|
||||
time.clone(),
|
||||
);
|
||||
|
||||
let mut duration = Duration::from_secs(1);
|
||||
assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY));
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, false);
|
||||
assert_eq!(
|
||||
res,
|
||||
START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32()
|
||||
- INITIAL_VELOCITY.signum() * 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
|
||||
);
|
||||
|
||||
duration = Duration::from_hours(10);
|
||||
assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, true);
|
||||
assert_eq!(
|
||||
res,
|
||||
START_VALUE + INITIAL_VELOCITY * f32::abs(INITIAL_VELOCITY / DECELERATION)
|
||||
- 0.5
|
||||
* INITIAL_VELOCITY.signum()
|
||||
* DECELERATION
|
||||
* (INITIAL_VELOCITY / DECELERATION).powi(2)
|
||||
);
|
||||
|
||||
assert!(res > LIMIT_VALUE); // We reached velocity zero before we reached the position limit
|
||||
}
|
||||
|
||||
/// We reach the position limit before the velocity got zero and so we run into the spring damper system
|
||||
/// Increasing case: start_value < limit_value
|
||||
#[test]
|
||||
fn constant_deceleration_spring_damper_increasing_limit_reached() {
|
||||
const INITIAL_VELOCITY: f32 = 50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
const HALF_PERIOD_TIME: f32 = 10.;
|
||||
const START_VALUE: f32 = 10.;
|
||||
const LIMIT_VALUE: f32 = 70.;
|
||||
let parameters = super::ConstantDecelerationSpringDamperParameters::new(
|
||||
INITIAL_VELOCITY,
|
||||
DECELERATION,
|
||||
HALF_PERIOD_TIME,
|
||||
);
|
||||
|
||||
let mut time = Instant::now();
|
||||
let mut simulation = ConstantDecelerationSpringDamper::new_internal(
|
||||
START_VALUE,
|
||||
LIMIT_VALUE,
|
||||
parameters,
|
||||
time.clone(),
|
||||
);
|
||||
|
||||
let duration = Duration::from_secs(1);
|
||||
assert!(f32::abs(DECELERATION) * duration.as_secs_f32() < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, false);
|
||||
assert_eq!(simulation.state, State::Deceleration);
|
||||
assert!(res < LIMIT_VALUE); // We are still in the constant deceleration state
|
||||
|
||||
time += Duration::from_secs((HALF_PERIOD_TIME / 2.) as u64);
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, false);
|
||||
assert_eq!(simulation.state, State::SpringDamper);
|
||||
assert!(res > LIMIT_VALUE);
|
||||
|
||||
time += Duration::from_hours(10);
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, true);
|
||||
assert_eq!(simulation.state, State::Done);
|
||||
assert_eq!(res, LIMIT_VALUE);
|
||||
}
|
||||
|
||||
/// We reach the position limit before the velocity got zero and so we run into the spring damper system
|
||||
/// Decreasing case. limit_value < start_value
|
||||
#[test]
|
||||
fn constant_deceleration_spring_damper_decreasing_limit_reached() {
|
||||
const INITIAL_VELOCITY: f32 = -50.;
|
||||
const DECELERATION: f32 = 20.;
|
||||
const HALF_PERIOD_TIME: f32 = 10.;
|
||||
const START_VALUE: f32 = 70.;
|
||||
const LIMIT_VALUE: f32 = 10.;
|
||||
let parameters = super::ConstantDecelerationSpringDamperParameters::new(
|
||||
INITIAL_VELOCITY,
|
||||
DECELERATION,
|
||||
HALF_PERIOD_TIME,
|
||||
);
|
||||
|
||||
let mut time = Instant::now();
|
||||
let mut simulation = ConstantDecelerationSpringDamper::new_internal(
|
||||
START_VALUE,
|
||||
LIMIT_VALUE,
|
||||
parameters,
|
||||
time.clone(),
|
||||
);
|
||||
|
||||
let duration = Duration::from_secs(1);
|
||||
assert!(f32::abs(DECELERATION) * duration.as_secs_f32() < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
|
||||
time += duration;
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, false);
|
||||
assert_eq!(simulation.state, State::Deceleration);
|
||||
assert!(res > LIMIT_VALUE); // We are still in the constant deceleration state
|
||||
|
||||
time += Duration::from_secs((HALF_PERIOD_TIME / 2.) as u64);
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, false);
|
||||
assert_eq!(simulation.state, State::SpringDamper);
|
||||
assert!(res < LIMIT_VALUE);
|
||||
|
||||
time += Duration::from_hours(10);
|
||||
let (res, finished) = simulation.step(time);
|
||||
assert_eq!(finished, true);
|
||||
assert_eq!(simulation.state, State::Done);
|
||||
assert_eq!(res, LIMIT_VALUE);
|
||||
}
|
||||
}
|
||||
+1379
File diff suppressed because it is too large.
Load diff
+164
@@ -0,0 +1,164 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
Callback that can be connected to one single handler.
|
||||
|
||||
TODO: reconsider if we should rename that to `Event`
|
||||
but then it should also be renamed everywhere, including in the language grammar
|
||||
*/
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use core::cell::Cell;
|
||||
|
||||
/// A Callback that can be connected to a handler.
|
||||
///
|
||||
/// The Arg represents the argument. It should always be a tuple
|
||||
///
|
||||
#[repr(C)]
|
||||
pub struct Callback<Arg: ?Sized, Ret = ()> {
|
||||
/// FIXME: `Box<dyn>` is a fat object and we probably want to put an erased type in there
|
||||
handler: Cell<Option<Box<dyn FnMut(&Arg, &mut Ret)>>>,
|
||||
}
|
||||
|
||||
impl<Arg: ?Sized, Ret> Default for Callback<Arg, Ret> {
|
||||
fn default() -> Self {
|
||||
Self { handler: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Arg: ?Sized, Ret: Default> Callback<Arg, Ret> {
|
||||
/// Call the callback with the given argument.
|
||||
pub fn call(&self, a: &Arg) -> Ret {
|
||||
let mut r = Ret::default();
|
||||
if let Some(mut h) = self.handler.take() {
|
||||
h(a, &mut r);
|
||||
assert!(self.handler.take().is_none(), "Callback Handler set while called");
|
||||
self.handler.set(Some(h));
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// Return whether a callback is registered or not.
|
||||
pub fn has_handler(&self) -> bool {
|
||||
let handler = self.handler.take();
|
||||
let result = handler.is_some();
|
||||
self.handler.set(handler);
|
||||
result
|
||||
}
|
||||
|
||||
/// Set an handler to be called when the callback is called
|
||||
///
|
||||
/// There can only be one single handler per callback.
|
||||
pub fn set_handler(&self, mut f: impl FnMut(&Arg) -> Ret + 'static) {
|
||||
self.handler.set(Some(Box::new(move |a: &Arg, r: &mut Ret| *r = f(a))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_simple_test() {
|
||||
use std::rc::Rc;
|
||||
#[derive(Default)]
|
||||
struct Component {
|
||||
pressed: core::cell::Cell<bool>,
|
||||
clicked: Callback<()>,
|
||||
}
|
||||
let c = Rc::new(Component::default());
|
||||
let weak = Rc::downgrade(&c);
|
||||
c.clicked.set_handler(move |()| weak.upgrade().unwrap().pressed.set(true));
|
||||
c.clicked.call(&());
|
||||
assert!(c.pressed.get());
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
pub(crate) mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
type c_void = ();
|
||||
#[repr(C)]
|
||||
/// Has the same layout as Callback<_>
|
||||
pub struct CallbackOpaque(*const c_void, *const c_void);
|
||||
|
||||
static_assertions::assert_eq_align!(CallbackOpaque, Callback<()>);
|
||||
static_assertions::assert_eq_size!(CallbackOpaque, Callback<()>);
|
||||
static_assertions::assert_eq_align!(CallbackOpaque, Callback<(alloc::string::String,)>);
|
||||
static_assertions::assert_eq_size!(CallbackOpaque, Callback<(alloc::string::String,)>);
|
||||
|
||||
/// Initialize the callback.
|
||||
/// slint_callback_drop must be called.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_callback_init(out: *mut CallbackOpaque) {
|
||||
assert_eq!(core::mem::size_of::<CallbackOpaque>(), core::mem::size_of::<Callback<()>>());
|
||||
unsafe { core::ptr::write(out as *mut Callback<()>, Default::default()) };
|
||||
}
|
||||
|
||||
/// Emit the callback
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_callback_call(
|
||||
sig: *const CallbackOpaque,
|
||||
arg: *const c_void,
|
||||
ret: *mut c_void,
|
||||
) {
|
||||
unsafe {
|
||||
let sig = &*(sig as *const Callback<c_void>);
|
||||
if let Some(mut h) = sig.handler.take() {
|
||||
h(&*arg, &mut *ret);
|
||||
assert!(sig.handler.take().is_none(), "Callback Handler set while called");
|
||||
sig.handler.set(Some(h));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set callback handler.
|
||||
///
|
||||
/// The binding has signature fn(user_data)
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_callback_set_handler(
|
||||
sig: *const CallbackOpaque,
|
||||
binding: extern "C" fn(user_data: *mut c_void, arg: *const c_void, ret: *mut c_void),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
) {
|
||||
unsafe {
|
||||
let sig = &mut *(sig as *mut Callback<c_void>);
|
||||
|
||||
struct UserData {
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
binding:
|
||||
extern "C" fn(user_data: *mut c_void, arg: *const c_void, ret: *mut c_void),
|
||||
}
|
||||
|
||||
impl Drop for UserData {
|
||||
fn drop(&mut self) {
|
||||
if let Some(x) = self.drop_user_data {
|
||||
x(self.user_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserData {
|
||||
/// Safety: the arguments must be valid pointers
|
||||
unsafe fn call(&self, arg: *const c_void, ret: *mut c_void) {
|
||||
(self.binding)(self.user_data, arg, ret)
|
||||
}
|
||||
}
|
||||
|
||||
let ud = UserData { user_data, drop_user_data, binding };
|
||||
sig.handler.set(Some(Box::new(move |a: &(), r: &mut ()| {
|
||||
ud.call(a as *const c_void, r as *mut c_void)
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
/// Destroy callback
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_callback_drop(handle: *mut CallbackOpaque) {
|
||||
unsafe { core::ptr::drop_in_place(handle as *mut Callback<()>) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! This module defines a `ComponentFactory` and related code.
|
||||
use crate::api::ComponentHandle;
|
||||
use crate::item_tree::{ItemTreeRc, ItemTreeVTable, ItemTreeWeak};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use core::fmt::Debug;
|
||||
|
||||
/// The `FactoryContext` provides extra information to the ComponentFactory
|
||||
pub struct FactoryContext {
|
||||
/// The item tree to embed the factory product into
|
||||
pub parent_item_tree: ItemTreeWeak,
|
||||
/// The index in the parent item tree with the dynamic node to connect
|
||||
/// the factories product to.
|
||||
pub parent_item_tree_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ComponentFactoryInner(Rc<dyn Fn(FactoryContext) -> Option<ItemTreeRc> + 'static>);
|
||||
|
||||
impl PartialEq for ComponentFactoryInner {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for ComponentFactoryInner {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_tuple("ComponentFactoryData").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ComponentFactory` can be used to create new Components at runtime,
|
||||
/// taking a factory function returning a [`ComponentHandle`].
|
||||
///
|
||||
/// The `FactoryContext` is passed to that factory function.
|
||||
///
|
||||
/// A `ComponentFactory` implements the `component-factory` type for
|
||||
/// properties in the Slint language.
|
||||
///
|
||||
/// The `component-factory` is used by an `ComponentContainer` element in Slint
|
||||
/// files to embed UI elements based on the produced component within the
|
||||
/// `ComponentContainer` element.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ComponentFactory(Option<ComponentFactoryInner>);
|
||||
|
||||
impl ComponentFactory {
|
||||
/// Create a new `ComponentFactory`
|
||||
pub fn new<
|
||||
X: vtable::HasStaticVTable<ItemTreeVTable> + 'static,
|
||||
T: ComponentHandle<WeakInner = vtable::VWeak<ItemTreeVTable, X>> + 'static,
|
||||
>(
|
||||
factory: impl Fn(FactoryContext) -> Option<T> + 'static,
|
||||
) -> Self {
|
||||
let factory = Box::new(factory) as Box<dyn Fn(FactoryContext) -> Option<T> + 'static>;
|
||||
|
||||
Self(Some(ComponentFactoryInner(Rc::new(move |ctx| -> Option<ItemTreeRc> {
|
||||
let product = (factory)(ctx);
|
||||
product.map(|p| vtable::VRc::into_dyn(p.as_weak().inner().upgrade().unwrap()))
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Build a `Component`
|
||||
pub(crate) fn build(&self, ctx: FactoryContext) -> Option<ItemTreeRc> {
|
||||
self.0.as_ref().and_then(move |b| (b.0)(ctx))
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use crate::Property;
|
||||
use crate::api::PlatformError;
|
||||
use crate::input::InternalKeyboardModifierState;
|
||||
use crate::platform::{EventLoopProxy, Platform};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::Cell;
|
||||
|
||||
crate::thread_local! {
|
||||
pub(crate) static GLOBAL_CONTEXT : once_cell::unsync::OnceCell<SlintContext>
|
||||
= const { once_cell::unsync::OnceCell::new() }
|
||||
}
|
||||
|
||||
pub(crate) struct SlintContextInner {
|
||||
platform: Box<dyn Platform>,
|
||||
pub(crate) window_count: core::cell::RefCell<isize>,
|
||||
/// This property is read by all translations, and marked dirty when the language changes,
|
||||
/// so that every translated string gets re-translated. The property's value is the current selected
|
||||
/// language when bundling translations.
|
||||
pub(crate) translations_dirty: core::pin::Pin<Box<Property<usize>>>,
|
||||
pub(crate) translations_bundle_languages:
|
||||
core::cell::RefCell<Option<alloc::vec::Vec<&'static str>>>,
|
||||
pub(crate) window_shown_hook:
|
||||
core::cell::RefCell<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>>,
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
xdg_app_id: core::cell::RefCell<Option<crate::SharedString>>,
|
||||
#[cfg(feature = "tr")]
|
||||
external_translator: core::cell::RefCell<Option<Box<dyn tr::Translator>>>,
|
||||
#[cfg(feature = "shared-parley")]
|
||||
pub(crate) font_context: core::cell::RefCell<parley::FontContext>,
|
||||
#[cfg(feature = "shared-swash")]
|
||||
pub(crate) swash_scale_context: core::cell::RefCell<swash::scale::ScaleContext>,
|
||||
pub(crate) modifiers: Cell<InternalKeyboardModifierState>,
|
||||
}
|
||||
|
||||
/// This context is meant to hold the state and the backend.
|
||||
/// Currently it is not possible to have several platform at the same time in one process, but in the future it might be.
|
||||
/// See issue #4294
|
||||
#[derive(Clone)]
|
||||
pub struct SlintContext(pub(crate) Rc<SlintContextInner>);
|
||||
|
||||
impl SlintContext {
|
||||
/// Create a new context with a given platform
|
||||
pub fn new(platform: Box<dyn Platform + 'static>) -> Self {
|
||||
#[cfg(feature = "shared-parley")]
|
||||
let collection = i_slint_common::sharedfontique::create_collection(true);
|
||||
|
||||
Self(Rc::new(SlintContextInner {
|
||||
platform,
|
||||
window_count: 0.into(),
|
||||
translations_dirty: Box::pin(Property::new_named(0, "SlintContext::translations")),
|
||||
translations_bundle_languages: Default::default(),
|
||||
window_shown_hook: Default::default(),
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
xdg_app_id: Default::default(),
|
||||
#[cfg(feature = "tr")]
|
||||
external_translator: Default::default(),
|
||||
#[cfg(feature = "shared-parley")]
|
||||
font_context: {
|
||||
let font_context = parley::FontContext {
|
||||
collection: collection.inner,
|
||||
source_cache: collection.source_cache,
|
||||
};
|
||||
core::cell::RefCell::new(font_context)
|
||||
},
|
||||
#[cfg(feature = "shared-swash")]
|
||||
swash_scale_context: core::cell::RefCell::new(swash::scale::ScaleContext::new()),
|
||||
modifiers: Cell::new(Default::default()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return a reference to the platform abstraction
|
||||
pub fn platform(&self) -> &dyn Platform {
|
||||
&*self.0.platform
|
||||
}
|
||||
|
||||
/// Return a reference to the font context
|
||||
#[cfg(feature = "shared-parley")]
|
||||
pub fn font_context(&self) -> &core::cell::RefCell<parley::FontContext> {
|
||||
&self.0.font_context
|
||||
}
|
||||
|
||||
/// Return a reference to the swash scale context
|
||||
#[cfg(feature = "shared-swash")]
|
||||
pub fn swash_scale_context(&self) -> &core::cell::RefCell<swash::scale::ScaleContext> {
|
||||
&self.0.swash_scale_context
|
||||
}
|
||||
|
||||
/// Return an event proxy
|
||||
// FIXME: Make EvenLoopProxy cloneable, and maybe wrap in a struct
|
||||
pub fn event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
|
||||
self.0.platform.new_event_loop_proxy()
|
||||
}
|
||||
|
||||
#[cfg(target_has_atomic = "ptr")]
|
||||
/// Context specific version of `slint::spawn_local`
|
||||
pub fn spawn_local<F: core::future::Future + 'static>(
|
||||
&self,
|
||||
fut: F,
|
||||
) -> Result<crate::future::JoinHandle<F::Output>, crate::api::EventLoopError> {
|
||||
crate::future::spawn_local_with_ctx(self, fut)
|
||||
}
|
||||
|
||||
pub fn run_event_loop(&self) -> Result<(), PlatformError> {
|
||||
self.0.platform.run_event_loop()
|
||||
}
|
||||
|
||||
pub fn set_xdg_app_id(&self, _app_id: crate::SharedString) {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
{
|
||||
self.0.xdg_app_id.replace(Some(_app_id));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
pub fn xdg_app_id(&self) -> Option<crate::SharedString> {
|
||||
self.0.xdg_app_id.borrow().clone()
|
||||
}
|
||||
|
||||
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||
pub fn xdg_app_id(&self) -> Option<crate::SharedString> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "tr")]
|
||||
pub fn set_external_translator(&self, translator: Option<Box<dyn tr::Translator>>) {
|
||||
*self.0.external_translator.borrow_mut() = translator;
|
||||
self.0.translations_dirty.mark_dirty();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tr")]
|
||||
pub fn external_translator(&self) -> Option<core::cell::Ref<'_, Box<dyn tr::Translator>>> {
|
||||
core::cell::Ref::filter_map(self.0.external_translator.borrow(), |maybe_translator| {
|
||||
maybe_translator.as_ref()
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal function to access the context.
|
||||
/// The factory function is called if the platform abstraction is not yet
|
||||
/// initialized, and should be given by the platform_selector
|
||||
pub fn with_global_context<R>(
|
||||
factory: impl FnOnce() -> Result<Box<dyn Platform + 'static>, PlatformError>,
|
||||
f: impl FnOnce(&SlintContext) -> R,
|
||||
) -> Result<R, PlatformError> {
|
||||
GLOBAL_CONTEXT.with(|p| match p.get() {
|
||||
Some(ctx) => Ok(f(ctx)),
|
||||
None => {
|
||||
if crate::platform::with_event_loop_proxy(|proxy| proxy.is_some()) {
|
||||
return Err(PlatformError::SetPlatformError(
|
||||
crate::platform::SetPlatformError::AlreadySet,
|
||||
));
|
||||
}
|
||||
crate::platform::set_platform(factory()?).map_err(PlatformError::SetPlatformError)?;
|
||||
Ok(f(p.get().unwrap()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Internal function to set a hook that's invoked whenever a slint::Window is shown. This
|
||||
/// is used by the system testing module. Returns a previously set hook, if any.
|
||||
pub fn set_window_shown_hook(
|
||||
hook: Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>,
|
||||
) -> Result<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>, PlatformError> {
|
||||
GLOBAL_CONTEXT.with(|p| match p.get() {
|
||||
Some(ctx) => Ok(ctx.0.window_shown_hook.replace(hook)),
|
||||
None => Err(PlatformError::NoPlatform),
|
||||
})
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use crate::SharedString;
|
||||
#[cfg(feature = "std")]
|
||||
use chrono::Local;
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
|
||||
pub fn use_24_hour_format() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns the number of days in a given month
|
||||
pub fn month_day_count(month: u32, year: i32) -> Option<i32> {
|
||||
Some(
|
||||
NaiveDate::from_ymd_opt(
|
||||
match month {
|
||||
12 => year + 1,
|
||||
_ => year,
|
||||
},
|
||||
match month {
|
||||
12 => 1,
|
||||
_ => month + 1,
|
||||
},
|
||||
1,
|
||||
)?
|
||||
.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1)?)
|
||||
.num_days() as i32,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn month_offset(month: u32, year: i32) -> i32 {
|
||||
if let Some(date) = NaiveDate::from_ymd_opt(year, month, 1) {
|
||||
let offset = date.weekday().number_from_monday() as i32;
|
||||
|
||||
// sunday
|
||||
if offset >= 7 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
// The result is only None if month == 0, it should not happen because the function is only
|
||||
// used internal and not directly by the user. So it is ok to return 0 on a None result
|
||||
0
|
||||
}
|
||||
|
||||
pub fn format_date(format: &str, day: u32, month: u32, year: i32) -> SharedString {
|
||||
if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
|
||||
return crate::format!("{}", date.format(format));
|
||||
}
|
||||
|
||||
// Don't panic, this function is used only internal
|
||||
SharedString::default()
|
||||
}
|
||||
|
||||
pub fn parse_date(date: &str, format: &str) -> Option<[i32; 3]> {
|
||||
NaiveDate::parse_from_str(date, format)
|
||||
.ok()
|
||||
.map(|date| [date.day() as i32, date.month() as i32, date.year()])
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn date_now() -> [i32; 3] {
|
||||
let now = Local::now().date_naive();
|
||||
[now.day() as i32, now.month() as i32, now.year()]
|
||||
}
|
||||
|
||||
// display the today date is currently not implemented for no_std
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub fn date_now() -> [i32; 3] {
|
||||
[-1, -1, -1]
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_date_time_use_24_hour_format() -> bool {
|
||||
use_24_hour_format()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_date_time_month_day_count(month: u32, year: i32) -> i32 {
|
||||
month_day_count(month, year).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_date_time_month_offset(month: u32, year: i32) -> i32 {
|
||||
month_offset(month, year)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_date_time_format_date(
|
||||
format: &SharedString,
|
||||
day: u32,
|
||||
month: u32,
|
||||
year: i32,
|
||||
out: &mut SharedString,
|
||||
) {
|
||||
*out = format_date(format, day, month, year)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_date_time_date_now(d: &mut i32, m: &mut i32, y: &mut i32) {
|
||||
[*d, *m, *y] = date_now();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_date_time_parse_date(
|
||||
date: &SharedString,
|
||||
format: &SharedString,
|
||||
d: &mut i32,
|
||||
m: &mut i32,
|
||||
y: &mut i32,
|
||||
) -> bool {
|
||||
if let Some(x) = parse_date(date, format) {
|
||||
[*d, *m, *y] = x;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
#![cfg(target_has_atomic = "ptr")] // Arc is not available. TODO: implement using RawWarker
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! This module contains the code that runs futures
|
||||
|
||||
use crate::SlintContext;
|
||||
use crate::api::EventLoopError;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::task::Wake;
|
||||
use alloc::vec::Vec;
|
||||
use core::future::Future;
|
||||
use core::ops::DerefMut;
|
||||
use core::pin::Pin;
|
||||
use core::task::Poll;
|
||||
use portable_atomic as atomic;
|
||||
|
||||
enum FutureState<T> {
|
||||
Running(Pin<Box<dyn Future<Output = T>>>),
|
||||
Finished(Option<T>),
|
||||
}
|
||||
|
||||
struct FutureRunnerInner<T> {
|
||||
fut: FutureState<T>,
|
||||
wakers: Vec<core::task::Waker>,
|
||||
}
|
||||
|
||||
struct FutureRunner<T> {
|
||||
#[cfg(not(feature = "std"))]
|
||||
inner: core::cell::RefCell<FutureRunnerInner<T>>,
|
||||
#[cfg(feature = "std")]
|
||||
inner: std::sync::Mutex<FutureRunnerInner<T>>,
|
||||
aborted: atomic::AtomicBool,
|
||||
proxy: Box<dyn crate::platform::EventLoopProxy>,
|
||||
#[cfg(feature = "std")]
|
||||
thread: std::thread::ThreadId,
|
||||
}
|
||||
|
||||
impl<T> FutureRunner<T> {
|
||||
fn inner(&self) -> impl DerefMut<Target = FutureRunnerInner<T>> + '_ {
|
||||
#[cfg(feature = "std")]
|
||||
return self.inner.lock().unwrap();
|
||||
#[cfg(not(feature = "std"))]
|
||||
return self.inner.borrow_mut();
|
||||
}
|
||||
}
|
||||
|
||||
// # Safety:
|
||||
// The Future might not be Send, but we only poll the future from the main thread.
|
||||
// (We even assert that)
|
||||
// We may access the finished value from another thread only if T is Send
|
||||
// (because JoinHandle only implement Send if T:Send)
|
||||
#[allow(unsafe_code)]
|
||||
unsafe impl<T> Send for FutureRunner<T> {}
|
||||
#[allow(unsafe_code)]
|
||||
unsafe impl<T> Sync for FutureRunner<T> {}
|
||||
|
||||
impl<T: 'static> Wake for FutureRunner<T> {
|
||||
fn wake(self: alloc::sync::Arc<Self>) {
|
||||
self.clone().proxy.invoke_from_event_loop(Box::new(move || {
|
||||
#[cfg(feature = "std")]
|
||||
assert_eq!(self.thread, std::thread::current().id(), "the future was moved to a thread despite we checked it was created in the event loop thread");
|
||||
let waker = self.clone().into();
|
||||
let mut inner = self.inner();
|
||||
let mut cx = core::task::Context::from_waker(&waker);
|
||||
if let FutureState::Running(fut) = &mut inner.fut {
|
||||
if self.aborted.load(atomic::Ordering::Relaxed) {
|
||||
inner.fut = FutureState::Finished(None);
|
||||
} else {
|
||||
match fut.as_mut().poll(&mut cx) {
|
||||
Poll::Ready(val) => {
|
||||
inner.fut = FutureState::Finished(Some(val));
|
||||
for w in core::mem::take(&mut inner.wakers) {
|
||||
w.wake();
|
||||
}
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.expect("No event loop despite we checked");
|
||||
}
|
||||
}
|
||||
|
||||
/// The return value of the `spawn_local()` function
|
||||
///
|
||||
/// Can be used to abort the future, or to get the value from a different thread with `.await`
|
||||
///
|
||||
/// This trait implements future. Polling it after it finished or aborted may result in a panic.
|
||||
pub struct JoinHandle<T>(alloc::sync::Arc<FutureRunner<T>>);
|
||||
|
||||
impl<T> Future for JoinHandle<T> {
|
||||
type Output = T;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut core::task::Context<'_>) -> Poll<Self::Output> {
|
||||
let mut inner = self.0.inner();
|
||||
match &mut inner.fut {
|
||||
FutureState::Running(_) => {
|
||||
let waker = cx.waker();
|
||||
if !inner.wakers.iter().any(|w| w.will_wake(waker)) {
|
||||
inner.wakers.push(waker.clone());
|
||||
}
|
||||
Poll::Pending
|
||||
}
|
||||
FutureState::Finished(x) => {
|
||||
Poll::Ready(x.take().expect("Polling completed or aborted JoinHandle"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> JoinHandle<T> {
|
||||
/// If the future hasn't completed yet, this will make the event loop stop polling the corresponding future and it will be dropped
|
||||
///
|
||||
/// Once this handle has been aborted, it can no longer be polled
|
||||
pub fn abort(self) {
|
||||
self.0.aborted.store(true, atomic::Ordering::Relaxed);
|
||||
}
|
||||
/// Checks if the task associated with this `JoinHandle` has finished.
|
||||
pub fn is_finished(&self) -> bool {
|
||||
matches!(self.0.inner().fut, FutureState::Finished(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[allow(unsafe_code)]
|
||||
// Safety: JoinHandle doesn't access the future, only the
|
||||
unsafe impl<T: Send> Send for JoinHandle<T> {}
|
||||
|
||||
/// Implementation for [`SlintContext::spawn_local`]
|
||||
pub(crate) fn spawn_local_with_ctx<F: Future + 'static>(
|
||||
ctx: &SlintContext,
|
||||
fut: F,
|
||||
) -> Result<JoinHandle<F::Output>, EventLoopError> {
|
||||
let arc = alloc::sync::Arc::new(FutureRunner {
|
||||
#[cfg(feature = "std")]
|
||||
thread: std::thread::current().id(),
|
||||
inner: FutureRunnerInner { fut: FutureState::Running(Box::pin(fut)), wakers: Vec::new() }
|
||||
.into(),
|
||||
aborted: Default::default(),
|
||||
proxy: ctx.event_loop_proxy().ok_or(EventLoopError::NoEventLoopProvider)?,
|
||||
});
|
||||
arc.wake_by_ref();
|
||||
Ok(JoinHandle(arc))
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright © Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
|
||||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
# GDB pretty printers for types defined in i_slint_core
|
||||
import gdb
|
||||
import gdb.printing
|
||||
|
||||
|
||||
class SharedVectorProvider:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
def to_string(self):
|
||||
try:
|
||||
size = int(self.val["inner"]["pointer"]["header"]["size"])
|
||||
return f"<SharedVector, len={size}>"
|
||||
except Exception as e:
|
||||
return f"<SharedVector: error reading size: {e}>"
|
||||
|
||||
def children(self):
|
||||
try:
|
||||
size = int(self.val["inner"]["pointer"]["header"]["size"])
|
||||
if size == 0:
|
||||
return
|
||||
|
||||
inner_struct = self.val["inner"]["pointer"].dereference()
|
||||
maybe_uninit = inner_struct["data"]
|
||||
elem_type = self._element_type(maybe_uninit)
|
||||
data_ptr = maybe_uninit.address.cast(elem_type.pointer())
|
||||
for i in range(size):
|
||||
yield f"[{i}]", (data_ptr + i).dereference()
|
||||
except Exception as e:
|
||||
yield "<error>", f"error reading elements: {e}"
|
||||
|
||||
def display_hint(self):
|
||||
return "array"
|
||||
|
||||
@staticmethod
|
||||
def _element_type(maybe_uninit_val):
|
||||
ty = maybe_uninit_val.type
|
||||
for field in ty.fields():
|
||||
if field.name == "value":
|
||||
val_type = field.type
|
||||
if val_type.code == gdb.TYPE_CODE_STRUCT:
|
||||
for sub in val_type.fields():
|
||||
if sub.name in ("value", "__0", "0"):
|
||||
return sub.type
|
||||
return val_type
|
||||
raise RuntimeError(
|
||||
"unsupported MaybeUninit layout for SharedVector element type"
|
||||
)
|
||||
|
||||
|
||||
class SharedVectorSubPrinter(gdb.printing.SubPrettyPrinter):
|
||||
def __init__(self):
|
||||
super().__init__("SharedVector")
|
||||
|
||||
def __call__(self, val):
|
||||
if not self.enabled:
|
||||
return None
|
||||
t = val.type.strip_typedefs()
|
||||
if t.code == gdb.TYPE_CODE_PTR: # also support reference to SharedVector
|
||||
try:
|
||||
val = val.dereference()
|
||||
t = val.type.strip_typedefs()
|
||||
except gdb.error:
|
||||
return None
|
||||
if (
|
||||
t.code == gdb.TYPE_CODE_STRUCT
|
||||
and t.tag
|
||||
and t.tag.startswith("i_slint_core::sharedvector::SharedVector<")
|
||||
):
|
||||
return SharedVectorProvider(val)
|
||||
return None
|
||||
|
||||
|
||||
class SliceProvider:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
def to_string(self):
|
||||
try:
|
||||
return f"<Slice, len={int(self.val['len'])}>"
|
||||
except Exception as e:
|
||||
return f"<Slice: error reading len: {e}>"
|
||||
|
||||
def children(self):
|
||||
try:
|
||||
length = int(self.val["len"])
|
||||
if length == 0:
|
||||
return
|
||||
|
||||
data_ptr = self._data_pointer()
|
||||
elem_type = data_ptr.type.target()
|
||||
for i in range(length):
|
||||
yield f"[{i}]", (data_ptr + i).dereference()
|
||||
except Exception as e:
|
||||
yield "<error>", f"error reading elements: {e}"
|
||||
|
||||
def display_hint(self):
|
||||
return "array"
|
||||
|
||||
def _data_pointer(self):
|
||||
nn = self.val["ptr"]
|
||||
if nn.type.code == gdb.TYPE_CODE_PTR:
|
||||
return nn
|
||||
for field in nn.type.fields():
|
||||
if field.name in ("pointer", "__0", "0"):
|
||||
candidate = nn[field]
|
||||
if candidate.type.code == gdb.TYPE_CODE_PTR:
|
||||
return candidate
|
||||
raise RuntimeError("unsupported NonNull layout in Slice")
|
||||
|
||||
|
||||
class SliceSubPrinter(gdb.printing.SubPrettyPrinter):
|
||||
def __init__(self):
|
||||
super().__init__("Slice")
|
||||
|
||||
def __call__(self, val):
|
||||
if not self.enabled:
|
||||
return None
|
||||
t = val.type.strip_typedefs()
|
||||
if t.code == gdb.TYPE_CODE_PTR: # also support reference to Slice
|
||||
try:
|
||||
val = val.dereference()
|
||||
t = val.type.strip_typedefs()
|
||||
except gdb.error:
|
||||
return None
|
||||
if (
|
||||
t.code == gdb.TYPE_CODE_STRUCT
|
||||
and t.tag
|
||||
and t.tag.startswith("i_slint_core::slice::Slice<")
|
||||
):
|
||||
return SliceProvider(val)
|
||||
return None
|
||||
|
||||
|
||||
class SlintCorePrettyPrinter(gdb.printing.PrettyPrinter):
|
||||
def __init__(self):
|
||||
super().__init__("i_slint_core", [])
|
||||
self.subprinters = [SharedVectorSubPrinter(), SliceSubPrinter()]
|
||||
|
||||
def __call__(self, val):
|
||||
for sp in self.subprinters:
|
||||
pp = sp(val)
|
||||
if pp is not None:
|
||||
return pp
|
||||
return None
|
||||
|
||||
|
||||
printer = SlintCorePrettyPrinter()
|
||||
|
||||
|
||||
def register_printers(objfile=None):
|
||||
gdb.printing.register_pretty_printer(objfile, printer, replace=True)
|
||||
|
||||
|
||||
register_printers()
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
/*!
|
||||
Graphics Abstractions.
|
||||
|
||||
This module contains the abstractions and convenience types used for rendering.
|
||||
*/
|
||||
extern crate alloc;
|
||||
use crate::Coord;
|
||||
use crate::SharedString;
|
||||
use crate::api::PlatformError;
|
||||
use crate::lengths::LogicalLength;
|
||||
use alloc::boxed::Box;
|
||||
|
||||
pub use euclid;
|
||||
/// 2D Rectangle
|
||||
pub type Rect = euclid::default::Rect<Coord>;
|
||||
/// 2D Rectangle with integer coordinates
|
||||
pub type IntRect = euclid::default::Rect<i32>;
|
||||
/// 2D Point
|
||||
pub type Point = euclid::default::Point2D<Coord>;
|
||||
/// 2D Size
|
||||
pub type Size = euclid::default::Size2D<Coord>;
|
||||
/// 2D Size in integer coordinates
|
||||
pub type IntSize = euclid::default::Size2D<u32>;
|
||||
/// 2D Transform
|
||||
pub type Transform = euclid::default::Transform2D<Coord>;
|
||||
|
||||
pub(crate) mod color;
|
||||
pub use color::*;
|
||||
|
||||
#[cfg(feature = "shared-fontique")]
|
||||
use i_slint_common::sharedfontique::{self, fontique};
|
||||
#[cfg(feature = "path")]
|
||||
mod path;
|
||||
#[cfg(feature = "path")]
|
||||
pub use path::*;
|
||||
|
||||
mod brush;
|
||||
pub use brush::*;
|
||||
|
||||
pub(crate) mod image;
|
||||
pub use self::image::*;
|
||||
|
||||
pub(crate) mod bitmapfont;
|
||||
pub use self::bitmapfont::*;
|
||||
|
||||
pub mod rendering_metrics_collector;
|
||||
|
||||
#[cfg(feature = "box-shadow-cache")]
|
||||
pub mod boxshadowcache;
|
||||
|
||||
pub mod border_radius;
|
||||
pub use border_radius::*;
|
||||
|
||||
#[cfg(feature = "wgpu-27")]
|
||||
pub mod wgpu_27;
|
||||
#[cfg(feature = "wgpu-28")]
|
||||
pub mod wgpu_28;
|
||||
|
||||
/// CachedGraphicsData allows the graphics backend to store an arbitrary piece of data associated with
|
||||
/// an item, which is typically computed by accessing properties. The dependency_tracker is used to allow
|
||||
/// for a lazy computation. Typically, back ends store either compute intensive data or handles that refer to
|
||||
/// data that's stored in GPU memory.
|
||||
pub struct CachedGraphicsData<T> {
|
||||
/// The backend specific data.
|
||||
pub data: T,
|
||||
/// The property tracker that should be used to evaluate whether the primitive needs to be re-created
|
||||
/// or not.
|
||||
pub dependency_tracker: Option<core::pin::Pin<Box<crate::properties::PropertyTracker>>>,
|
||||
}
|
||||
|
||||
impl<T> CachedGraphicsData<T> {
|
||||
/// Creates a new TrackingRenderingPrimitive by evaluating the provided update_fn once, storing the returned
|
||||
/// rendering primitive and initializing the dependency tracker.
|
||||
pub fn new(update_fn: impl FnOnce() -> T) -> Self {
|
||||
let dependency_tracker = Box::pin(crate::properties::PropertyTracker::default());
|
||||
let data = dependency_tracker.as_ref().evaluate(update_fn);
|
||||
Self { data, dependency_tracker: Some(dependency_tracker) }
|
||||
}
|
||||
}
|
||||
|
||||
/// FontRequest collects all the developer-configurable properties for fonts, such as family, weight, etc.
|
||||
/// It is submitted as a request to the platform font system (i.e. CoreText on macOS) and in exchange the
|
||||
/// backend returns a `Box<dyn Font>`.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct FontRequest {
|
||||
/// The name of the font family to be used, such as "Helvetica". An empty family name means the system
|
||||
/// default font family should be used.
|
||||
pub family: Option<SharedString>,
|
||||
/// If the weight is None, the system default font weight should be used.
|
||||
pub weight: Option<i32>,
|
||||
/// If the pixel size is None, the system default font size should be used.
|
||||
pub pixel_size: Option<LogicalLength>,
|
||||
/// The additional spacing (or shrinking if negative) between glyphs. This is usually not submitted to
|
||||
/// the font-subsystem but collected here for API convenience
|
||||
pub letter_spacing: Option<LogicalLength>,
|
||||
/// Whether to select an italic face of the font family.
|
||||
pub italic: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "shared-fontique")]
|
||||
impl FontRequest {
|
||||
/// Attempts to query the fontique font collection for a matching font.
|
||||
pub fn query_fontique(
|
||||
&self,
|
||||
collection: &mut fontique::Collection,
|
||||
source_cache: &mut fontique::SourceCache,
|
||||
) -> Option<fontique::QueryFont> {
|
||||
let mut query = collection.query(source_cache);
|
||||
query.set_families(
|
||||
self.family
|
||||
.as_ref()
|
||||
.map(|family| fontique::QueryFamily::from(family.as_str()))
|
||||
.into_iter()
|
||||
.chain(
|
||||
sharedfontique::FALLBACK_FAMILIES
|
||||
.into_iter()
|
||||
.map(fontique::QueryFamily::Generic),
|
||||
),
|
||||
);
|
||||
|
||||
query.set_attributes(fontique::Attributes {
|
||||
weight: self
|
||||
.weight
|
||||
.as_ref()
|
||||
.map(|&weight| fontique::FontWeight::new(weight as f32))
|
||||
.unwrap_or_default(),
|
||||
style: if self.italic {
|
||||
fontique::FontStyle::Italic
|
||||
} else {
|
||||
fontique::FontStyle::Normal
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut font = None;
|
||||
|
||||
query.matches_with(|queried_font| {
|
||||
font = Some(queried_font.clone());
|
||||
fontique::QueryStatus::Stop
|
||||
});
|
||||
|
||||
font
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal enum to specify which version of OpenGL to request
|
||||
/// from the windowing system.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RequestedOpenGLVersion {
|
||||
/// OpenGL
|
||||
OpenGL(Option<(u8, u8)>),
|
||||
/// OpenGL ES
|
||||
OpenGLES(Option<(u8, u8)>),
|
||||
}
|
||||
|
||||
/// Internal enum specify which graphics API should be used, when
|
||||
/// the backend selector requests that from a built-in backend.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum RequestedGraphicsAPI {
|
||||
/// OpenGL (ES)
|
||||
OpenGL(RequestedOpenGLVersion),
|
||||
/// Metal
|
||||
Metal,
|
||||
/// Vulkan
|
||||
Vulkan,
|
||||
/// Direct 3D
|
||||
Direct3D,
|
||||
#[cfg(feature = "unstable-wgpu-27")]
|
||||
/// WGPU 27.x
|
||||
WGPU27(wgpu_27::api::WGPUConfiguration),
|
||||
#[cfg(feature = "unstable-wgpu-28")]
|
||||
/// WGPU 28.x
|
||||
WGPU28(wgpu_28::api::WGPUConfiguration),
|
||||
}
|
||||
|
||||
impl TryFrom<&RequestedGraphicsAPI> for RequestedOpenGLVersion {
|
||||
type Error = PlatformError;
|
||||
|
||||
fn try_from(requested_graphics_api: &RequestedGraphicsAPI) -> Result<Self, Self::Error> {
|
||||
match requested_graphics_api {
|
||||
RequestedGraphicsAPI::OpenGL(requested_open_glversion) => {
|
||||
Ok(requested_open_glversion.clone())
|
||||
}
|
||||
RequestedGraphicsAPI::Metal => {
|
||||
Err("Metal rendering is not supported with an OpenGL renderer".into())
|
||||
}
|
||||
RequestedGraphicsAPI::Vulkan => {
|
||||
Err("Vulkan rendering is not supported with an OpenGL renderer".into())
|
||||
}
|
||||
RequestedGraphicsAPI::Direct3D => {
|
||||
Err("Direct3D rendering is not supported with an OpenGL renderer".into())
|
||||
}
|
||||
#[cfg(feature = "unstable-wgpu-27")]
|
||||
RequestedGraphicsAPI::WGPU27(..) => {
|
||||
Err("WGPU 27.x rendering is not supported with an OpenGL renderer".into())
|
||||
}
|
||||
#[cfg(feature = "unstable-wgpu-28")]
|
||||
RequestedGraphicsAPI::WGPU28(..) => {
|
||||
Err("WGPU 28.x rendering is not supported with an OpenGL renderer".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestedOpenGLVersion> for RequestedGraphicsAPI {
|
||||
fn from(version: RequestedOpenGLVersion) -> Self {
|
||||
Self::OpenGL(version)
|
||||
}
|
||||
}
|
||||
|
||||
/// Private API exposed to just the renderers to create GraphicsAPI instance with
|
||||
/// non-exhaustive enum variant.
|
||||
#[cfg(feature = "unstable-wgpu-27")]
|
||||
pub fn create_graphics_api_wgpu_27(
|
||||
instance: wgpu_27::wgpu::Instance,
|
||||
device: wgpu_27::wgpu::Device,
|
||||
queue: wgpu_27::wgpu::Queue,
|
||||
) -> crate::api::GraphicsAPI<'static> {
|
||||
crate::api::GraphicsAPI::WGPU27 { instance, device, queue }
|
||||
}
|
||||
|
||||
/// Private API exposed to just the renderers to create GraphicsAPI instance with
|
||||
/// non-exhaustive enum variant.
|
||||
#[cfg(feature = "unstable-wgpu-28")]
|
||||
pub fn create_graphics_api_wgpu_28(
|
||||
instance: wgpu_28::wgpu::Instance,
|
||||
device: wgpu_28::wgpu::Device,
|
||||
queue: wgpu_28::wgpu::Queue,
|
||||
) -> crate::api::GraphicsAPI<'static> {
|
||||
crate::api::GraphicsAPI::WGPU28 { instance, device, queue }
|
||||
}
|
||||
|
||||
/// Internal module for use by cbindgen and the C++ platform API layer.
|
||||
#[cfg(feature = "ffi")]
|
||||
pub mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
/// Expand Rect so that cbindgen can see it. ( is in fact euclid::default::Rect<f32>)
|
||||
#[cfg(cbindgen)]
|
||||
#[repr(C)]
|
||||
struct Rect {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
/// Expand IntRect so that cbindgen can see it. ( is in fact euclid::default::Rect<i32>)
|
||||
#[cfg(cbindgen)]
|
||||
#[repr(C)]
|
||||
struct IntRect {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
/// Expand Point so that cbindgen can see it. ( is in fact euclid::default::Point2D<f32>)
|
||||
#[cfg(cbindgen)]
|
||||
#[repr(C)]
|
||||
struct Point {
|
||||
x: f32,
|
||||
y: f32,
|
||||
}
|
||||
|
||||
/// Expand Box2D so that cbindgen can see it.
|
||||
#[cfg(cbindgen)]
|
||||
#[repr(C)]
|
||||
struct Box2D<T, U> {
|
||||
min: euclid::Point2D<T>,
|
||||
max: euclid::Point2D<T>,
|
||||
_unit: std::marker::PhantomData<U>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub use super::path::ffi::*;
|
||||
|
||||
/// Conversion function used by C++ platform API layer to
|
||||
/// convert the PhysicalSize used in the Rust WindowAdapter API
|
||||
/// to the ffi.
|
||||
pub fn physical_size_from_api(
|
||||
size: crate::api::PhysicalSize,
|
||||
) -> crate::graphics::euclid::default::Size2D<u32> {
|
||||
size.to_euclid()
|
||||
}
|
||||
|
||||
/// Conversion function used by C++ platform API layer to
|
||||
/// convert the PhysicalPosition used in the Rust WindowAdapter API
|
||||
/// to the ffi.
|
||||
pub fn physical_position_from_api(
|
||||
position: crate::api::PhysicalPosition,
|
||||
) -> crate::graphics::euclid::default::Point2D<i32> {
|
||||
position.to_euclid()
|
||||
}
|
||||
|
||||
/// Conversion function used by C++ platform API layer to
|
||||
/// convert from the ffi to PhysicalPosition.
|
||||
pub fn physical_position_to_api(
|
||||
position: crate::graphics::euclid::default::Point2D<i32>,
|
||||
) -> crate::api::PhysicalPosition {
|
||||
crate::api::PhysicalPosition::from_euclid(position)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use crate::slice::Slice;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
/// A pre-rendered glyph with the alpha map and associated metrics
|
||||
pub struct BitmapGlyph {
|
||||
/// The starting x-coordinate for the glyph, relative to the base line
|
||||
/// This is a fixed point number that is shifted by 6 bits
|
||||
pub x: i16,
|
||||
/// The starting y-coordinate for the glyph, relative to the base line
|
||||
/// This is a fixed point number that is shifted by 6 bits
|
||||
pub y: i16,
|
||||
/// The width of the glyph in pixels
|
||||
pub width: i16,
|
||||
/// The height of the glyph in pixels
|
||||
pub height: i16,
|
||||
/// The horizontal distance to the next glyph
|
||||
/// This is a fixed point number that is shifted by 6 bits
|
||||
pub x_advance: i16,
|
||||
/// The 8-bit alpha map that's to be blended with the current text color
|
||||
/// or 8-bit signed distance field depending on `BitmapFont::sdf`
|
||||
pub data: Slice<'static, u8>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
/// A set of pre-rendered bitmap glyphs at a fixed pixel size
|
||||
pub struct BitmapGlyphs {
|
||||
/// The font size in pixels at which the glyphs were pre-rendered. The boundaries of glyphs may exceed this
|
||||
/// size, if the font designer has chosen so. This is only used for matching.
|
||||
pub pixel_size: i16,
|
||||
/// The data of the pre-rendered glyphs
|
||||
pub glyph_data: Slice<'static, BitmapGlyph>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
/// An entry in the character map of a [`BitmapFont`].
|
||||
pub struct CharacterMapEntry {
|
||||
/// The unicode code point for a given glyph
|
||||
pub code_point: char,
|
||||
/// The corresponding index in the `glyph_data` of [`BitmapGlyphs`]
|
||||
pub glyph_index: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
/// A subset of an originally scalable font that's rendered ahead of time.
|
||||
pub struct BitmapFont {
|
||||
/// The family name of the font
|
||||
pub family_name: Slice<'static, u8>,
|
||||
/// A vector of code points and their corresponding glyph index, sorted by code point.
|
||||
pub character_map: Slice<'static, CharacterMapEntry>,
|
||||
/// The font supplied size of the em square.
|
||||
pub units_per_em: f32,
|
||||
/// The font ascent in design metrics (typically positive)
|
||||
pub ascent: f32,
|
||||
/// The font descent in design metrics (typically negative)
|
||||
pub descent: f32,
|
||||
/// The font's x-height.
|
||||
pub x_height: f32,
|
||||
/// The font's cap-height.
|
||||
pub cap_height: f32,
|
||||
/// A vector of pre-rendered glyph sets. Each glyph set must have the same number of glyphs,
|
||||
/// which must be at least as big as the largest glyph index in the character map.
|
||||
pub glyphs: Slice<'static, BitmapGlyphs>,
|
||||
/// The weight of the font in CSS units (400 is normal).
|
||||
pub weight: u16,
|
||||
/// Whether the type-face is rendered italic.
|
||||
pub italic: bool,
|
||||
/// Whether the format of the font is a signed distance field
|
||||
pub sdf: bool,
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains border radius related types for the run-time library.
|
||||
*/
|
||||
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
|
||||
use euclid::approxord::{max, min};
|
||||
use euclid::num::Zero;
|
||||
use euclid::{Length, Scale};
|
||||
use num_traits::NumCast;
|
||||
|
||||
/// Top-left, top-right, bottom-right, and bottom-left border radius, optionally
|
||||
/// tagged with a unit.
|
||||
#[repr(C)]
|
||||
pub struct BorderRadius<T, U> {
|
||||
/// The top-left radius.
|
||||
pub top_left: T,
|
||||
/// The top-right radius.
|
||||
pub top_right: T,
|
||||
/// The bottom-right radius.
|
||||
pub bottom_right: T,
|
||||
/// The bottom-left radius.
|
||||
pub bottom_left: T,
|
||||
#[doc(hidden)]
|
||||
pub _unit: PhantomData<U>,
|
||||
}
|
||||
|
||||
impl<T, U> Copy for BorderRadius<T, U> where T: Copy {}
|
||||
|
||||
impl<T, U> Clone for BorderRadius<T, U>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
BorderRadius {
|
||||
top_left: self.top_left.clone(),
|
||||
top_right: self.top_right.clone(),
|
||||
bottom_right: self.bottom_right.clone(),
|
||||
bottom_left: self.bottom_left.clone(),
|
||||
_unit: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Eq for BorderRadius<T, U> where T: Eq {}
|
||||
|
||||
impl<T, U> PartialEq for BorderRadius<T, U>
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.top_left == other.top_left
|
||||
&& self.top_right == other.top_right
|
||||
&& self.bottom_right == other.bottom_right
|
||||
&& self.bottom_left == other.bottom_left
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> fmt::Debug for BorderRadius<T, U>
|
||||
where
|
||||
T: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"BorderRadius({:?}, {:?}, {:?}, {:?})",
|
||||
self.top_left, self.top_right, self.bottom_right, self.bottom_left
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Default for BorderRadius<T, U>
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
BorderRadius::new(T::default(), T::default(), T::default(), T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Zero for BorderRadius<T, U>
|
||||
where
|
||||
T: Zero,
|
||||
{
|
||||
fn zero() -> Self {
|
||||
BorderRadius::new(T::zero(), T::zero(), T::zero(), T::zero())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> BorderRadius<T, U> {
|
||||
/// Constructor taking a scalar for each radius.
|
||||
///
|
||||
/// Radii are specified in top-left, top-right, bottom-right, bottom-left
|
||||
/// order following CSS's convention.
|
||||
pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self {
|
||||
BorderRadius { top_left, top_right, bottom_right, bottom_left, _unit: PhantomData }
|
||||
}
|
||||
|
||||
/// Constructor taking a typed Length for each radius.
|
||||
///
|
||||
/// Radii are specified in top-left, top-right, bottom-right, bottom-left
|
||||
/// order following CSS's convention.
|
||||
pub fn from_lengths(
|
||||
top_left: Length<T, U>,
|
||||
top_right: Length<T, U>,
|
||||
bottom_right: Length<T, U>,
|
||||
bottom_left: Length<T, U>,
|
||||
) -> Self {
|
||||
BorderRadius::new(top_left.0, top_right.0, bottom_right.0, bottom_left.0)
|
||||
}
|
||||
|
||||
/// Constructor taking the same scalar value for all radii.
|
||||
pub fn new_uniform(all: T) -> Self
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
BorderRadius::new(all, all, all, all)
|
||||
}
|
||||
|
||||
/// Constructor taking the same typed Length for all radii.
|
||||
pub fn from_length(all: Length<T, U>) -> Self
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
BorderRadius::new_uniform(all.0)
|
||||
}
|
||||
|
||||
/// Returns `true` if all radii are equal.
|
||||
pub fn is_uniform(&self) -> bool
|
||||
where
|
||||
T: ApproxEq<T>,
|
||||
{
|
||||
self.top_left.approx_eq(&self.top_right)
|
||||
&& self.top_left.approx_eq(&self.bottom_right)
|
||||
&& self.top_left.approx_eq(&self.bottom_left)
|
||||
}
|
||||
|
||||
/// Returns the uniform radius if all are equal, or `None` otherwise.
|
||||
pub fn as_uniform(&self) -> Option<T>
|
||||
where
|
||||
T: Copy + ApproxEq<T>,
|
||||
{
|
||||
if self.is_uniform() { Some(self.top_left) } else { None }
|
||||
}
|
||||
|
||||
/// Returns `true` if all radii are zero.
|
||||
pub fn is_zero(&self) -> bool
|
||||
where
|
||||
T: ApproxEq<T> + Zero,
|
||||
{
|
||||
let zero = T::zero();
|
||||
self.top_left.approx_eq(&zero)
|
||||
&& self.top_right.approx_eq(&zero)
|
||||
&& self.bottom_right.approx_eq(&zero)
|
||||
&& self.bottom_left.approx_eq(&zero)
|
||||
}
|
||||
|
||||
/// Returns the outer radius.
|
||||
///
|
||||
/// For any corner with a positive radius, the radius is ensured to be at
|
||||
/// least `half_border_width`.
|
||||
pub fn outer(&self, half_border_width: Length<T, U>) -> Self
|
||||
where
|
||||
T: Copy + PartialOrd + Zero,
|
||||
{
|
||||
let zero = T::zero();
|
||||
BorderRadius::new(
|
||||
if self.top_left > zero {
|
||||
max(self.top_left, half_border_width.0)
|
||||
} else {
|
||||
self.top_left
|
||||
},
|
||||
if self.top_right > zero {
|
||||
max(self.top_right, half_border_width.0)
|
||||
} else {
|
||||
self.top_right
|
||||
},
|
||||
if self.bottom_right > zero {
|
||||
max(self.bottom_right, half_border_width.0)
|
||||
} else {
|
||||
self.bottom_right
|
||||
},
|
||||
if self.bottom_left > zero {
|
||||
max(self.bottom_left, half_border_width.0)
|
||||
} else {
|
||||
self.bottom_left
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the inner radius.
|
||||
///
|
||||
/// A positive radius of each corner is subtracted by `half_border_width`
|
||||
/// and min-clamped to zero.
|
||||
pub fn inner(&self, half_border_width: Length<T, U>) -> Self
|
||||
where
|
||||
T: Copy + PartialOrd + Sub<T, Output = T> + Zero,
|
||||
{
|
||||
BorderRadius::new(
|
||||
self.top_left - half_border_width.0,
|
||||
self.top_right - half_border_width.0,
|
||||
self.bottom_right - half_border_width.0,
|
||||
self.bottom_left - half_border_width.0,
|
||||
)
|
||||
.max(Self::zero())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for testing approximate equality
|
||||
pub trait ApproxEq<Eps> {
|
||||
/// Returns `true` is this object is approximately equal to the other one.
|
||||
fn approx_eq(&self, other: &Self) -> bool;
|
||||
}
|
||||
|
||||
macro_rules! approx_eq {
|
||||
($ty:ty, $eps:expr) => {
|
||||
impl ApproxEq<$ty> for $ty {
|
||||
#[inline]
|
||||
fn approx_eq(&self, other: &$ty) -> bool {
|
||||
num_traits::sign::abs(*self - *other) <= $eps
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
approx_eq!(i16, 0);
|
||||
approx_eq!(i32, 0);
|
||||
approx_eq!(f32, f32::EPSILON);
|
||||
|
||||
impl<T, U> Add for BorderRadius<T, U>
|
||||
where
|
||||
T: Add<T, Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn add(self, other: Self) -> Self {
|
||||
BorderRadius::new(
|
||||
self.top_left + other.top_left,
|
||||
self.top_right + other.top_right,
|
||||
self.bottom_right + other.bottom_right,
|
||||
self.bottom_left + other.bottom_left,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> AddAssign<Self> for BorderRadius<T, U>
|
||||
where
|
||||
T: AddAssign<T>,
|
||||
{
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
self.top_left += other.top_left;
|
||||
self.top_right += other.top_right;
|
||||
self.bottom_right += other.bottom_right;
|
||||
self.bottom_left += other.bottom_left;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Sub for BorderRadius<T, U>
|
||||
where
|
||||
T: Sub<T, Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn sub(self, other: Self) -> Self {
|
||||
BorderRadius::new(
|
||||
self.top_left - other.top_left,
|
||||
self.top_right - other.top_right,
|
||||
self.bottom_right - other.bottom_right,
|
||||
self.bottom_left - other.bottom_left,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> SubAssign<Self> for BorderRadius<T, U>
|
||||
where
|
||||
T: SubAssign<T>,
|
||||
{
|
||||
fn sub_assign(&mut self, other: Self) {
|
||||
self.top_left -= other.top_left;
|
||||
self.top_right -= other.top_right;
|
||||
self.bottom_right -= other.bottom_right;
|
||||
self.bottom_left -= other.bottom_left;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Neg for BorderRadius<T, U>
|
||||
where
|
||||
T: Neg<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self {
|
||||
BorderRadius {
|
||||
top_left: -self.top_left,
|
||||
top_right: -self.top_right,
|
||||
bottom_right: -self.bottom_right,
|
||||
bottom_left: -self.bottom_left,
|
||||
_unit: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Mul<T> for BorderRadius<T, U>
|
||||
where
|
||||
T: Copy + Mul,
|
||||
{
|
||||
type Output = BorderRadius<T::Output, U>;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, scale: T) -> Self::Output {
|
||||
BorderRadius::new(
|
||||
self.top_left * scale,
|
||||
self.top_right * scale,
|
||||
self.bottom_right * scale,
|
||||
self.bottom_left * scale,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> MulAssign<T> for BorderRadius<T, U>
|
||||
where
|
||||
T: Copy + MulAssign,
|
||||
{
|
||||
#[inline]
|
||||
fn mul_assign(&mut self, other: T) {
|
||||
self.top_left *= other;
|
||||
self.top_right *= other;
|
||||
self.bottom_right *= other;
|
||||
self.bottom_left *= other;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U1, U2> Mul<Scale<T, U1, U2>> for BorderRadius<T, U1>
|
||||
where
|
||||
T: Copy + Mul,
|
||||
{
|
||||
type Output = BorderRadius<T::Output, U2>;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, scale: Scale<T, U1, U2>) -> Self::Output {
|
||||
BorderRadius::new(
|
||||
self.top_left * scale.0,
|
||||
self.top_right * scale.0,
|
||||
self.bottom_right * scale.0,
|
||||
self.bottom_left * scale.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> MulAssign<Scale<T, U, U>> for BorderRadius<T, U>
|
||||
where
|
||||
T: Copy + MulAssign,
|
||||
{
|
||||
#[inline]
|
||||
fn mul_assign(&mut self, other: Scale<T, U, U>) {
|
||||
*self *= other.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Div<T> for BorderRadius<T, U>
|
||||
where
|
||||
T: Copy + Div,
|
||||
{
|
||||
type Output = BorderRadius<T::Output, U>;
|
||||
|
||||
#[inline]
|
||||
fn div(self, scale: T) -> Self::Output {
|
||||
BorderRadius::new(
|
||||
self.top_left / scale,
|
||||
self.top_right / scale,
|
||||
self.bottom_right / scale,
|
||||
self.bottom_left / scale,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> DivAssign<T> for BorderRadius<T, U>
|
||||
where
|
||||
T: Copy + DivAssign,
|
||||
{
|
||||
#[inline]
|
||||
fn div_assign(&mut self, other: T) {
|
||||
self.top_left /= other;
|
||||
self.top_right /= other;
|
||||
self.bottom_right /= other;
|
||||
self.bottom_left /= other;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U1, U2> Div<Scale<T, U1, U2>> for BorderRadius<T, U2>
|
||||
where
|
||||
T: Copy + Div,
|
||||
{
|
||||
type Output = BorderRadius<T::Output, U1>;
|
||||
|
||||
#[inline]
|
||||
fn div(self, scale: Scale<T, U1, U2>) -> Self::Output {
|
||||
BorderRadius::new(
|
||||
self.top_left / scale.0,
|
||||
self.top_right / scale.0,
|
||||
self.bottom_right / scale.0,
|
||||
self.bottom_left / scale.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> DivAssign<Scale<T, U, U>> for BorderRadius<T, U>
|
||||
where
|
||||
T: Copy + DivAssign,
|
||||
{
|
||||
fn div_assign(&mut self, other: Scale<T, U, U>) {
|
||||
*self /= other.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> BorderRadius<T, U>
|
||||
where
|
||||
T: PartialOrd,
|
||||
{
|
||||
/// Returns the minimum of the two radii.
|
||||
#[inline]
|
||||
pub fn min(self, other: Self) -> Self {
|
||||
BorderRadius::new(
|
||||
min(self.top_left, other.top_left),
|
||||
min(self.top_right, other.top_right),
|
||||
min(self.bottom_right, other.bottom_right),
|
||||
min(self.bottom_left, other.bottom_left),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the maximum of the two radii.
|
||||
#[inline]
|
||||
pub fn max(self, other: Self) -> Self {
|
||||
BorderRadius::new(
|
||||
max(self.top_left, other.top_left),
|
||||
max(self.top_right, other.top_right),
|
||||
max(self.bottom_right, other.bottom_right),
|
||||
max(self.bottom_left, other.bottom_left),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> BorderRadius<T, U>
|
||||
where
|
||||
T: NumCast + Copy,
|
||||
{
|
||||
/// Cast from one numeric representation to another, preserving the units.
|
||||
#[inline]
|
||||
pub fn cast<NewT: NumCast>(self) -> BorderRadius<NewT, U> {
|
||||
self.try_cast().unwrap()
|
||||
}
|
||||
|
||||
/// Fallible cast from one numeric representation to another, preserving the units.
|
||||
pub fn try_cast<NewT: NumCast>(self) -> Option<BorderRadius<NewT, U>> {
|
||||
match (
|
||||
NumCast::from(self.top_left),
|
||||
NumCast::from(self.top_right),
|
||||
NumCast::from(self.bottom_right),
|
||||
NumCast::from(self.bottom_left),
|
||||
) {
|
||||
(Some(top_left), Some(top_right), Some(bottom_right), Some(bottom_left)) => {
|
||||
Some(BorderRadius::new(top_left, top_right, bottom_right, bottom_left))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::lengths::{LogicalBorderRadius, LogicalLength, PhysicalPx, ScaleFactor};
|
||||
use euclid::UnknownUnit;
|
||||
|
||||
type BorderRadius = super::BorderRadius<f32, UnknownUnit>;
|
||||
type IntBorderRadius = super::BorderRadius<i16, UnknownUnit>;
|
||||
type PhysicalBorderRadius = super::BorderRadius<f32, PhysicalPx>;
|
||||
|
||||
#[test]
|
||||
fn test_eq() {
|
||||
let a = BorderRadius::new(1., 2., 3., 4.);
|
||||
let b = BorderRadius::new(1., 2., 3., 4.);
|
||||
let c = BorderRadius::new(4., 3., 2., 1.);
|
||||
let d = BorderRadius::new(
|
||||
c.top_left + f32::EPSILON / 2.,
|
||||
c.top_right - f32::EPSILON / 2.,
|
||||
c.bottom_right - f32::EPSILON / 2.,
|
||||
c.bottom_left + f32::EPSILON / 2.,
|
||||
);
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, c);
|
||||
assert_eq!(c, d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_max() {
|
||||
let a = BorderRadius::new(1., 2., 3., 4.);
|
||||
let b = BorderRadius::new(4., 3., 2., 1.);
|
||||
assert_eq!(a.min(b), BorderRadius::new(1., 2., 2., 1.));
|
||||
assert_eq!(a.max(b), BorderRadius::new(4., 3., 3., 4.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scale() {
|
||||
let scale = ScaleFactor::new(2.);
|
||||
let logical_radius = LogicalBorderRadius::new(1., 2., 3., 4.);
|
||||
let physical_radius = PhysicalBorderRadius::new(2., 4., 6., 8.);
|
||||
assert_eq!(logical_radius * scale, physical_radius);
|
||||
assert_eq!(physical_radius / scale, logical_radius);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero() {
|
||||
assert!(BorderRadius::new_uniform(0.).is_zero());
|
||||
assert!(BorderRadius::new_uniform(1.0e-9).is_zero());
|
||||
assert!(!BorderRadius::new_uniform(1.0e-3).is_zero());
|
||||
assert!(IntBorderRadius::new_uniform(0).is_zero());
|
||||
assert!(!IntBorderRadius::new_uniform(1).is_zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_outer() {
|
||||
let radius = LogicalBorderRadius::new(0., 2.5, 5., 10.);
|
||||
let half_border_width = LogicalLength::new(5.);
|
||||
assert_eq!(radius.inner(half_border_width), LogicalBorderRadius::new(0., 0., 0., 5.));
|
||||
assert_eq!(radius.outer(half_border_width), LogicalBorderRadius::new(0., 5., 5., 10.));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains a cache helper for caching box shadow textures.
|
||||
*/
|
||||
|
||||
use std::{cell::RefCell, collections::BTreeMap};
|
||||
|
||||
use crate::items::ItemRc;
|
||||
use crate::lengths::RectLengths;
|
||||
use crate::{
|
||||
Color,
|
||||
lengths::{PhysicalPx, ScaleFactor},
|
||||
};
|
||||
|
||||
/// Struct to store options affecting the rendering of a box shadow
|
||||
#[derive(Clone, PartialEq, Debug, Default)]
|
||||
pub struct BoxShadowOptions {
|
||||
/// The width of the box shadow texture in physical pixels.
|
||||
pub width: euclid::Length<f32, PhysicalPx>,
|
||||
/// The height of the box shadow texture in physical pixels.
|
||||
pub height: euclid::Length<f32, PhysicalPx>,
|
||||
/// The color for the box shadow.
|
||||
pub color: Color,
|
||||
/// The blur to apply to the box shadow in pixels.
|
||||
pub blur: euclid::Length<f32, PhysicalPx>,
|
||||
/// The radius of the box shadow.
|
||||
pub radius: euclid::Length<f32, PhysicalPx>,
|
||||
}
|
||||
|
||||
impl Eq for BoxShadowOptions {}
|
||||
impl Ord for BoxShadowOptions {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
if (other.width, other.height, other.color, other.blur, other.radius)
|
||||
< (self.width, self.height, self.color, self.blur, self.radius)
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else if (self.width, self.height, self.color, self.blur, self.radius)
|
||||
< (other.width, other.height, other.color, other.blur, other.radius)
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for BoxShadowOptions {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxShadowOptions {
|
||||
/// Extracts the rendering specific properties from the BoxShadow item and scales the logical
|
||||
/// coordinates to physical pixels used in the BoxShadowOptions. Returns None if for example the
|
||||
/// alpha on the box shadow would imply that no shadow is to be rendered.
|
||||
pub fn new(
|
||||
item_rc: &ItemRc,
|
||||
box_shadow: std::pin::Pin<&crate::items::BoxShadow>,
|
||||
scale_factor: ScaleFactor,
|
||||
) -> Option<Self> {
|
||||
let color = box_shadow.color();
|
||||
if color.alpha() == 0 {
|
||||
return None;
|
||||
}
|
||||
let geometry = item_rc.geometry();
|
||||
let width = geometry.width_length() * scale_factor;
|
||||
let height = geometry.height_length() * scale_factor;
|
||||
if width.get() < 1. || height.get() < 1. {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
width,
|
||||
height,
|
||||
color,
|
||||
blur: box_shadow.blur() * scale_factor, // This effectively becomes the blur radius, so scale to physical pixels
|
||||
radius: box_shadow.border_radius() * scale_factor,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache to hold box textures for given box shadow options.
|
||||
pub struct BoxShadowCache<ImageType>(RefCell<BTreeMap<BoxShadowOptions, Option<ImageType>>>);
|
||||
|
||||
impl<ImageType> Default for BoxShadowCache<ImageType> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<ImageType: Clone> BoxShadowCache<ImageType> {
|
||||
/// Look up a box shadow texture for a given box shadow item, or create a new one if needed.
|
||||
pub fn get_box_shadow(
|
||||
&self,
|
||||
item_rc: &ItemRc,
|
||||
item_cache: &crate::item_rendering::ItemCache<Option<ImageType>>,
|
||||
box_shadow: std::pin::Pin<&crate::items::BoxShadow>,
|
||||
scale_factor: ScaleFactor,
|
||||
shadow_render_fn: impl FnOnce(&BoxShadowOptions) -> Option<ImageType>,
|
||||
) -> Option<ImageType> {
|
||||
item_cache.get_or_update_cache_entry(item_rc, || {
|
||||
let shadow_options = BoxShadowOptions::new(item_rc, box_shadow, scale_factor)?;
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.entry(shadow_options.clone())
|
||||
.or_insert_with(|| shadow_render_fn(&shadow_options))
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
+778
@@ -0,0 +1,778 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains brush related types for the run-time library.
|
||||
*/
|
||||
|
||||
use super::Color;
|
||||
use crate::SharedVector;
|
||||
use crate::properties::InterpolatedPropertyValue;
|
||||
use euclid::default::{Point2D, Size2D};
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use num_traits::float::Float;
|
||||
|
||||
/// A brush is a data structure that is used to describe how
|
||||
/// a shape, such as a rectangle, path or even text, shall be filled.
|
||||
/// A brush can also be applied to the outline of a shape, that means
|
||||
/// the fill of the outline itself.
|
||||
#[derive(Clone, PartialEq, Debug, derive_more::From)]
|
||||
#[repr(C)]
|
||||
#[non_exhaustive]
|
||||
pub enum Brush {
|
||||
/// The color variant of brush is a plain color that is to be used for the fill.
|
||||
SolidColor(Color),
|
||||
/// The linear gradient variant of a brush describes the gradient stops for a fill
|
||||
/// where all color stops are along a line that's rotated by the specified angle.
|
||||
LinearGradient(LinearGradientBrush),
|
||||
/// The radial gradient variant of a brush describes a circle variant centered
|
||||
/// in the middle
|
||||
RadialGradient(RadialGradientBrush),
|
||||
/// The conical gradient variant of a brush describes a gradient that rotates around
|
||||
/// a center point, like the hands of a clock
|
||||
ConicGradient(ConicGradientBrush),
|
||||
}
|
||||
|
||||
/// Construct a brush with transparent color
|
||||
impl Default for Brush {
|
||||
fn default() -> Self {
|
||||
Self::SolidColor(Color::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Brush {
|
||||
/// If the brush is SolidColor, the contained color is returned.
|
||||
/// If the brush is a LinearGradient, the color of the first stop is returned.
|
||||
pub fn color(&self) -> Color {
|
||||
match self {
|
||||
Brush::SolidColor(col) => *col,
|
||||
Brush::LinearGradient(gradient) => {
|
||||
gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
|
||||
}
|
||||
Brush::RadialGradient(gradient) => {
|
||||
gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
|
||||
}
|
||||
Brush::ConicGradient(gradient) => {
|
||||
gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this brush contains a fully transparent color (alpha value is zero)
|
||||
///
|
||||
/// ```
|
||||
/// # use i_slint_core::graphics::*;
|
||||
/// assert!(Brush::default().is_transparent());
|
||||
/// assert!(Brush::SolidColor(Color::from_argb_u8(0, 255, 128, 140)).is_transparent());
|
||||
/// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 128, 140, 210)).is_transparent());
|
||||
/// ```
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
match self {
|
||||
Brush::SolidColor(c) => c.alpha() == 0,
|
||||
Brush::LinearGradient(_) => false,
|
||||
Brush::RadialGradient(_) => false,
|
||||
Brush::ConicGradient(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this brush is fully opaque
|
||||
///
|
||||
/// ```
|
||||
/// # use i_slint_core::graphics::*;
|
||||
/// assert!(!Brush::default().is_opaque());
|
||||
/// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 255, 128, 140)).is_opaque());
|
||||
/// assert!(Brush::SolidColor(Color::from_rgb_u8(128, 140, 210)).is_opaque());
|
||||
/// ```
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Brush::SolidColor(c) => c.alpha() == 255,
|
||||
Brush::LinearGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
|
||||
Brush::RadialGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
|
||||
Brush::ConicGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new version of this brush that has the brightness increased
|
||||
/// by the specified factor. This is done by calling [`Color::brighter`] on
|
||||
/// all the colors of this brush.
|
||||
#[must_use]
|
||||
pub fn brighter(&self, factor: f32) -> Self {
|
||||
match self {
|
||||
Brush::SolidColor(c) => Brush::SolidColor(c.brighter(factor)),
|
||||
Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
|
||||
g.angle(),
|
||||
g.stops().map(|s| GradientStop {
|
||||
color: s.color.brighter(factor),
|
||||
position: s.position,
|
||||
}),
|
||||
)),
|
||||
Brush::RadialGradient(g) => {
|
||||
Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
|
||||
GradientStop { color: s.color.brighter(factor), position: s.position }
|
||||
})))
|
||||
}
|
||||
Brush::ConicGradient(g) => {
|
||||
let mut new_grad = g.clone();
|
||||
// Skip the first stop (which contains the angle), modify only color stops
|
||||
for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
|
||||
x.color = x.color.brighter(factor);
|
||||
}
|
||||
Brush::ConicGradient(new_grad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new version of this brush that has the brightness decreased
|
||||
/// by the specified factor. This is done by calling [`Color::darker`] on
|
||||
/// all the color of this brush.
|
||||
#[must_use]
|
||||
pub fn darker(&self, factor: f32) -> Self {
|
||||
match self {
|
||||
Brush::SolidColor(c) => Brush::SolidColor(c.darker(factor)),
|
||||
Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
|
||||
g.angle(),
|
||||
g.stops()
|
||||
.map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
|
||||
)),
|
||||
Brush::RadialGradient(g) => Brush::RadialGradient(RadialGradientBrush::new_circle(
|
||||
g.stops()
|
||||
.map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
|
||||
)),
|
||||
Brush::ConicGradient(g) => {
|
||||
let mut new_grad = g.clone();
|
||||
// Skip the first stop (which contains the angle), modify only color stops
|
||||
for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
|
||||
x.color = x.color.darker(factor);
|
||||
}
|
||||
Brush::ConicGradient(new_grad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new version of this brush with the opacity decreased by `factor`.
|
||||
///
|
||||
/// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
|
||||
///
|
||||
/// See also [`Color::transparentize`]
|
||||
#[must_use]
|
||||
pub fn transparentize(&self, amount: f32) -> Self {
|
||||
match self {
|
||||
Brush::SolidColor(c) => Brush::SolidColor(c.transparentize(amount)),
|
||||
Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
|
||||
g.angle(),
|
||||
g.stops().map(|s| GradientStop {
|
||||
color: s.color.transparentize(amount),
|
||||
position: s.position,
|
||||
}),
|
||||
)),
|
||||
Brush::RadialGradient(g) => {
|
||||
Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
|
||||
GradientStop { color: s.color.transparentize(amount), position: s.position }
|
||||
})))
|
||||
}
|
||||
Brush::ConicGradient(g) => {
|
||||
let mut new_grad = g.clone();
|
||||
// Skip the first stop (which contains the angle), modify only color stops
|
||||
for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
|
||||
x.color = x.color.transparentize(amount);
|
||||
}
|
||||
Brush::ConicGradient(new_grad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new version of this brush with the related color's opacities
|
||||
/// set to `alpha`.
|
||||
#[must_use]
|
||||
pub fn with_alpha(&self, alpha: f32) -> Self {
|
||||
match self {
|
||||
Brush::SolidColor(c) => Brush::SolidColor(c.with_alpha(alpha)),
|
||||
Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
|
||||
g.angle(),
|
||||
g.stops().map(|s| GradientStop {
|
||||
color: s.color.with_alpha(alpha),
|
||||
position: s.position,
|
||||
}),
|
||||
)),
|
||||
Brush::RadialGradient(g) => {
|
||||
Brush::RadialGradient(RadialGradientBrush::new_circle(g.stops().map(|s| {
|
||||
GradientStop { color: s.color.with_alpha(alpha), position: s.position }
|
||||
})))
|
||||
}
|
||||
Brush::ConicGradient(g) => {
|
||||
let mut new_grad = g.clone();
|
||||
// Skip the first stop (which contains the angle), modify only color stops
|
||||
for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
|
||||
x.color = x.color.with_alpha(alpha);
|
||||
}
|
||||
Brush::ConicGradient(new_grad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The LinearGradientBrush describes a way of filling a shape with different colors, which
|
||||
/// are interpolated between different stops. The colors are aligned with a line that's rotated
|
||||
/// by the LinearGradient's angle.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct LinearGradientBrush(SharedVector<GradientStop>);
|
||||
|
||||
impl LinearGradientBrush {
|
||||
/// Creates a new linear gradient, described by the specified angle and the provided color stops.
|
||||
///
|
||||
/// The angle need to be specified in degrees.
|
||||
/// The stops don't need to be sorted as this function will sort them.
|
||||
pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||||
let stop_iter = stops.into_iter();
|
||||
let mut encoded_angle_and_stops = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
|
||||
// The gradient's first stop is a fake stop to store the angle
|
||||
encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
|
||||
encoded_angle_and_stops.extend(stop_iter);
|
||||
Self(encoded_angle_and_stops)
|
||||
}
|
||||
/// Returns the angle of the linear gradient in degrees.
|
||||
pub fn angle(&self) -> f32 {
|
||||
self.0[0].position
|
||||
}
|
||||
/// Returns the color stops of the linear gradient.
|
||||
/// The stops are sorted by positions.
|
||||
pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
|
||||
// skip the first fake stop that just contains the angle
|
||||
self.0.iter().skip(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// The RadialGradientBrush describes a way of filling a shape with a circular gradient
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct RadialGradientBrush(SharedVector<GradientStop>);
|
||||
|
||||
impl RadialGradientBrush {
|
||||
/// Creates a new circle radial gradient, centered in the middle and described
|
||||
/// by the provided color stops.
|
||||
pub fn new_circle(stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||||
Self(stops.into_iter().collect())
|
||||
}
|
||||
/// Returns the color stops of the linear gradient.
|
||||
pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// The ConicGradientBrush describes a way of filling a shape with a gradient
|
||||
/// that rotates around a center point
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct ConicGradientBrush(SharedVector<GradientStop>);
|
||||
|
||||
impl ConicGradientBrush {
|
||||
/// Creates a new conic gradient, described by the specified angle and the provided color stops.
|
||||
///
|
||||
/// The angle need to be specified in degrees (CSS `from <angle>` syntax).
|
||||
/// The stops don't need to be sorted as this function will normalize and process them.
|
||||
pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||||
let stop_iter = stops.into_iter();
|
||||
let mut encoded_angle_and_stops = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
|
||||
// The gradient's first stop is a fake stop to store the angle
|
||||
encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
|
||||
encoded_angle_and_stops.extend(stop_iter);
|
||||
let mut result = Self(encoded_angle_and_stops);
|
||||
result.normalize_stops();
|
||||
if angle.abs() > f32::EPSILON {
|
||||
result.apply_rotation(angle);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Normalizes the gradient stops to be within [0, 1] range with proper boundary stops.
|
||||
fn normalize_stops(&mut self) {
|
||||
// Check if we need to make any changes
|
||||
let stops_slice = &self.0[1..];
|
||||
let has_stop_at_0 = stops_slice.iter().any(|s| s.position.abs() < f32::EPSILON);
|
||||
let has_stop_at_1 = stops_slice.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
|
||||
let has_stops_outside = stops_slice.iter().any(|s| s.position < 0.0 || s.position > 1.0);
|
||||
let is_empty = stops_slice.is_empty();
|
||||
|
||||
// If no changes needed, return early
|
||||
if has_stop_at_0 && has_stop_at_1 && !has_stops_outside && !is_empty {
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to make changes, so copy
|
||||
let mut stops: alloc::vec::Vec<_> = stops_slice.to_vec();
|
||||
|
||||
// Add interpolated boundary stop at 0.0 if needed
|
||||
if !has_stop_at_0 {
|
||||
let stop_below_0 = stops.iter().filter(|s| s.position < 0.0).max_by(|a, b| {
|
||||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
let stop_above_0 = stops.iter().filter(|s| s.position > 0.0).min_by(|a, b| {
|
||||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
if let (Some(below), Some(above)) = (stop_below_0, stop_above_0) {
|
||||
let t = (0.0 - below.position) / (above.position - below.position);
|
||||
let color_at_0 = Self::interpolate_color(&below.color, &above.color, t);
|
||||
stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
|
||||
} else if let Some(above) = stop_above_0 {
|
||||
stops.insert(0, GradientStop { position: 0.0, color: above.color });
|
||||
} else if let Some(below) = stop_below_0 {
|
||||
stops.insert(0, GradientStop { position: 0.0, color: below.color });
|
||||
}
|
||||
}
|
||||
|
||||
// Add interpolated boundary stop at 1.0 if needed
|
||||
if !has_stop_at_1 {
|
||||
let stop_below_1 = stops.iter().filter(|s| s.position < 1.0).max_by(|a, b| {
|
||||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
let stop_above_1 = stops.iter().filter(|s| s.position > 1.0).min_by(|a, b| {
|
||||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
if let (Some(below), Some(above)) = (stop_below_1, stop_above_1) {
|
||||
let t = (1.0 - below.position) / (above.position - below.position);
|
||||
let color_at_1 = Self::interpolate_color(&below.color, &above.color, t);
|
||||
stops.push(GradientStop { position: 1.0, color: color_at_1 });
|
||||
} else if let Some(below) = stop_below_1 {
|
||||
stops.push(GradientStop { position: 1.0, color: below.color });
|
||||
} else if let Some(above) = stop_above_1 {
|
||||
stops.push(GradientStop { position: 1.0, color: above.color });
|
||||
}
|
||||
}
|
||||
|
||||
// Drop stops outside [0, 1] range
|
||||
if has_stops_outside {
|
||||
stops.retain(|s| 0.0 <= s.position && s.position <= 1.0);
|
||||
}
|
||||
|
||||
// Handle empty gradients
|
||||
if stops.is_empty() {
|
||||
stops.push(GradientStop { position: 0.0, color: Color::default() });
|
||||
stops.push(GradientStop { position: 1.0, color: Color::default() });
|
||||
}
|
||||
|
||||
// Update the internal storage
|
||||
let angle = self.angle();
|
||||
self.0 = SharedVector::with_capacity(stops.len() + 1);
|
||||
self.0.push(GradientStop { color: Default::default(), position: angle });
|
||||
self.0.extend(stops);
|
||||
}
|
||||
|
||||
/// Apply rotation to the gradient (CSS `from <angle>` syntax).
|
||||
///
|
||||
/// The `from_angle` parameter is specified in degrees and rotates the entire gradient clockwise.
|
||||
fn apply_rotation(&mut self, from_angle: f32) {
|
||||
// Convert degrees to normalized 0-1 range
|
||||
let normalized_from_angle = (from_angle / 360.0) - (from_angle / 360.0).floor();
|
||||
|
||||
// If no rotation needed, just update the stored angle
|
||||
if normalized_from_angle.abs() < f32::EPSILON {
|
||||
self.0.make_mut_slice()[0].position = from_angle;
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the stored angle
|
||||
self.0.make_mut_slice()[0].position = from_angle;
|
||||
|
||||
// Need to rotate, so copy
|
||||
let mut stops: alloc::vec::Vec<_> = self.0.iter().skip(1).copied().collect();
|
||||
|
||||
// Adjust first stop (at 0.0) to avoid duplicate with stop at 1.0
|
||||
if let Some(first) = stops.first_mut()
|
||||
&& first.position.abs() < f32::EPSILON
|
||||
{
|
||||
first.position = f32::EPSILON;
|
||||
}
|
||||
|
||||
// Step 1: Apply rotation by adding from_angle and wrapping to [0, 1) range
|
||||
stops = stops
|
||||
.iter()
|
||||
.map(|stop| {
|
||||
// f32::rem_euclid is not always available, and when it is it has a
|
||||
// different signature than num_traits::Euclid::rem_euclid (issue #11333).
|
||||
let rotated_position =
|
||||
num_traits::Euclid::rem_euclid(&(stop.position + normalized_from_angle), &1.0);
|
||||
GradientStop { position: rotated_position, color: stop.color }
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Step 2: Separate duplicate positions with different colors to avoid flickering
|
||||
for i in 0..stops.len() {
|
||||
let j = (i + 1) % stops.len();
|
||||
if (stops[i].position - stops[j].position).abs() < f32::EPSILON
|
||||
&& stops[i].color != stops[j].color
|
||||
{
|
||||
stops[i].position = (stops[i].position - f32::EPSILON).max(0.0);
|
||||
stops[j].position = (stops[j].position + f32::EPSILON).min(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Sort by rotated position
|
||||
stops.sort_by(|a, b| {
|
||||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
// Step 4: Add boundary stops at 0.0 and 1.0 if missing
|
||||
let has_stop_at_0 = stops.iter().any(|s| s.position.abs() < f32::EPSILON);
|
||||
if !has_stop_at_0 && let (Some(last), Some(first)) = (stops.last(), stops.first()) {
|
||||
let gap = 1.0 - last.position + first.position;
|
||||
let color_at_0 = if gap > f32::EPSILON {
|
||||
let t = (1.0 - last.position) / gap;
|
||||
Self::interpolate_color(&last.color, &first.color, t)
|
||||
} else {
|
||||
last.color
|
||||
};
|
||||
stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
|
||||
}
|
||||
|
||||
let has_stop_at_1 = stops.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
|
||||
if !has_stop_at_1 && let Some(first) = stops.first() {
|
||||
stops.push(GradientStop { position: 1.0, color: first.color });
|
||||
}
|
||||
|
||||
// Update the internal storage
|
||||
self.0 = SharedVector::with_capacity(stops.len() + 1);
|
||||
self.0.push(GradientStop { color: Default::default(), position: from_angle });
|
||||
self.0.extend(stops);
|
||||
}
|
||||
|
||||
/// Returns the starting angle (rotation) of the conic gradient in degrees.
|
||||
fn angle(&self) -> f32 {
|
||||
self.0[0].position
|
||||
}
|
||||
|
||||
/// Returns the color stops of the conic gradient.
|
||||
/// The stops are already rotated according to the `from_angle` specified in `new()`.
|
||||
pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
|
||||
// skip the first fake stop that just contains the angle
|
||||
self.0.iter().skip(1)
|
||||
}
|
||||
|
||||
/// Helper: Linearly interpolate between two colors using premultiplied alpha.
|
||||
///
|
||||
/// This is used for interpolating gradient boundary colors in CSS-style gradients.
|
||||
/// We cannot use Color::mix() here because it implements Sass color mixing algorithm,
|
||||
/// which is different from CSS gradient color interpolation.
|
||||
///
|
||||
/// CSS gradients interpolate in premultiplied RGBA space:
|
||||
/// https://www.w3.org/TR/css-color-4/#interpolation-alpha
|
||||
fn interpolate_color(c1: &Color, c2: &Color, factor: f32) -> Color {
|
||||
let argb1 = c1.to_argb_u8();
|
||||
let argb2 = c2.to_argb_u8();
|
||||
|
||||
// Convert to premultiplied alpha
|
||||
let a1 = argb1.alpha as f32 / 255.0;
|
||||
let a2 = argb2.alpha as f32 / 255.0;
|
||||
let r1 = argb1.red as f32 * a1;
|
||||
let g1 = argb1.green as f32 * a1;
|
||||
let b1 = argb1.blue as f32 * a1;
|
||||
let r2 = argb2.red as f32 * a2;
|
||||
let g2 = argb2.green as f32 * a2;
|
||||
let b2 = argb2.blue as f32 * a2;
|
||||
|
||||
// Interpolate in premultiplied space
|
||||
let alpha = (1.0 - factor) * a1 + factor * a2;
|
||||
let red = (1.0 - factor) * r1 + factor * r2;
|
||||
let green = (1.0 - factor) * g1 + factor * g2;
|
||||
let blue = (1.0 - factor) * b1 + factor * b2;
|
||||
|
||||
// Convert back from premultiplied alpha
|
||||
if alpha > 0.0 {
|
||||
Color::from_argb_u8(
|
||||
(alpha * 255.0) as u8,
|
||||
(red / alpha).min(255.0) as u8,
|
||||
(green / alpha).min(255.0) as u8,
|
||||
(blue / alpha).min(255.0) as u8,
|
||||
)
|
||||
} else {
|
||||
Color::from_argb_u8(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// C FFI function to normalize the gradient stops to be within [0, 1] range
|
||||
#[cfg(feature = "ffi")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_conic_gradient_normalize_stops(gradient: &mut ConicGradientBrush) {
|
||||
gradient.normalize_stops();
|
||||
}
|
||||
|
||||
/// C FFI function to apply rotation to a ConicGradientBrush
|
||||
#[cfg(feature = "ffi")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_conic_gradient_apply_rotation(
|
||||
gradient: &mut ConicGradientBrush,
|
||||
angle_degrees: f32,
|
||||
) {
|
||||
gradient.apply_rotation(angle_degrees);
|
||||
}
|
||||
|
||||
/// GradientStop describes a single color stop in a gradient. The colors between multiple
|
||||
/// stops are interpolated.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct GradientStop {
|
||||
/// The color to draw at this stop.
|
||||
pub color: Color,
|
||||
/// The position of this stop on the entire shape, as a normalized value between 0 and 1.
|
||||
pub position: f32,
|
||||
}
|
||||
|
||||
/// Returns the start / end points of a gradient within a rectangle of the given size, based on the angle (in degree).
|
||||
pub fn line_for_angle(angle: f32, size: Size2D<f32>) -> (Point2D<f32>, Point2D<f32>) {
|
||||
let angle = (angle + 90.).to_radians();
|
||||
let (s, c) = angle.sin_cos();
|
||||
|
||||
let (a, b) = if s.abs() < f32::EPSILON {
|
||||
let y = size.height / 2.;
|
||||
return if c < 0. {
|
||||
(Point2D::new(0., y), Point2D::new(size.width, y))
|
||||
} else {
|
||||
(Point2D::new(size.width, y), Point2D::new(0., y))
|
||||
};
|
||||
} else if c * s < 0. {
|
||||
// Intersection between the gradient line, and an orthogonal line that goes through (height, 0)
|
||||
let x = (s * size.width + c * size.height) * s / 2.;
|
||||
let y = -c * x / s + size.height;
|
||||
(Point2D::new(x, y), Point2D::new(size.width - x, size.height - y))
|
||||
} else {
|
||||
// Intersection between the gradient line, and an orthogonal line that goes through (0, 0)
|
||||
let x = (s * size.width - c * size.height) * s / 2.;
|
||||
let y = -c * x / s;
|
||||
(Point2D::new(size.width - x, size.height - y), Point2D::new(x, y))
|
||||
};
|
||||
|
||||
if s > 0. { (a, b) } else { (b, a) }
|
||||
}
|
||||
|
||||
impl InterpolatedPropertyValue for Brush {
|
||||
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
|
||||
match (self, target_value) {
|
||||
(Brush::SolidColor(source_col), Brush::SolidColor(target_col)) => {
|
||||
Brush::SolidColor(source_col.interpolate(target_col, t))
|
||||
}
|
||||
(Brush::SolidColor(col), Brush::LinearGradient(grad)) => {
|
||||
let mut new_grad = grad.clone();
|
||||
for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
|
||||
x.color = col.interpolate(&x.color, t);
|
||||
}
|
||||
Brush::LinearGradient(new_grad)
|
||||
}
|
||||
(a @ Brush::LinearGradient(_), b @ Brush::SolidColor(_)) => {
|
||||
Self::interpolate(b, a, 1. - t)
|
||||
}
|
||||
(Brush::LinearGradient(lhs), Brush::LinearGradient(rhs)) => {
|
||||
if lhs.0.len() < rhs.0.len() {
|
||||
Self::interpolate(target_value, self, 1. - t)
|
||||
} else {
|
||||
let mut new_grad = lhs.clone();
|
||||
let mut iter = new_grad.0.make_mut_slice().iter_mut();
|
||||
{
|
||||
let angle = &mut iter.next().unwrap().position;
|
||||
*angle = angle.interpolate(&rhs.angle(), t);
|
||||
}
|
||||
for s2 in rhs.stops() {
|
||||
let s1 = iter.next().unwrap();
|
||||
s1.color = s1.color.interpolate(&s2.color, t);
|
||||
s1.position = s1.position.interpolate(&s2.position, t);
|
||||
}
|
||||
for x in iter {
|
||||
x.position = x.position.interpolate(&1.0, t);
|
||||
}
|
||||
Brush::LinearGradient(new_grad)
|
||||
}
|
||||
}
|
||||
(Brush::SolidColor(col), Brush::RadialGradient(grad)) => {
|
||||
let mut new_grad = grad.clone();
|
||||
for x in new_grad.0.make_mut_slice().iter_mut() {
|
||||
x.color = col.interpolate(&x.color, t);
|
||||
}
|
||||
Brush::RadialGradient(new_grad)
|
||||
}
|
||||
(a @ Brush::RadialGradient(_), b @ Brush::SolidColor(_)) => {
|
||||
Self::interpolate(b, a, 1. - t)
|
||||
}
|
||||
(Brush::RadialGradient(lhs), Brush::RadialGradient(rhs)) => {
|
||||
if lhs.0.len() < rhs.0.len() {
|
||||
Self::interpolate(target_value, self, 1. - t)
|
||||
} else {
|
||||
let mut new_grad = lhs.clone();
|
||||
let mut iter = new_grad.0.make_mut_slice().iter_mut();
|
||||
let mut last_color = Color::default();
|
||||
for s2 in rhs.stops() {
|
||||
let s1 = iter.next().unwrap();
|
||||
last_color = s2.color;
|
||||
s1.color = s1.color.interpolate(&s2.color, t);
|
||||
s1.position = s1.position.interpolate(&s2.position, t);
|
||||
}
|
||||
for x in iter {
|
||||
x.position = x.position.interpolate(&1.0, t);
|
||||
x.color = x.color.interpolate(&last_color, t);
|
||||
}
|
||||
Brush::RadialGradient(new_grad)
|
||||
}
|
||||
}
|
||||
(Brush::SolidColor(col), Brush::ConicGradient(grad)) => {
|
||||
let mut new_grad = grad.clone();
|
||||
for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
|
||||
x.color = col.interpolate(&x.color, t);
|
||||
}
|
||||
Brush::ConicGradient(new_grad)
|
||||
}
|
||||
(a @ Brush::ConicGradient(_), b @ Brush::SolidColor(_)) => {
|
||||
Self::interpolate(b, a, 1. - t)
|
||||
}
|
||||
(Brush::ConicGradient(lhs), Brush::ConicGradient(rhs)) => {
|
||||
if lhs.0.len() < rhs.0.len() {
|
||||
Self::interpolate(target_value, self, 1. - t)
|
||||
} else {
|
||||
let mut new_grad = lhs.clone();
|
||||
let mut iter = new_grad.0.make_mut_slice().iter_mut();
|
||||
{
|
||||
let angle = &mut iter.next().unwrap().position;
|
||||
*angle = angle.interpolate(&rhs.angle(), t);
|
||||
}
|
||||
for s2 in rhs.stops() {
|
||||
let s1 = iter.next().unwrap();
|
||||
s1.color = s1.color.interpolate(&s2.color, t);
|
||||
s1.position = s1.position.interpolate(&s2.position, t);
|
||||
}
|
||||
for x in iter {
|
||||
x.position = x.position.interpolate(&1.0, t);
|
||||
}
|
||||
Brush::ConicGradient(new_grad)
|
||||
}
|
||||
}
|
||||
(a @ Brush::LinearGradient(_), b @ Brush::RadialGradient(_))
|
||||
| (a @ Brush::RadialGradient(_), b @ Brush::LinearGradient(_))
|
||||
| (a @ Brush::LinearGradient(_), b @ Brush::ConicGradient(_))
|
||||
| (a @ Brush::ConicGradient(_), b @ Brush::LinearGradient(_))
|
||||
| (a @ Brush::RadialGradient(_), b @ Brush::ConicGradient(_))
|
||||
| (a @ Brush::ConicGradient(_), b @ Brush::RadialGradient(_)) => {
|
||||
// Just go to an intermediate color.
|
||||
let color = Color::interpolate(&b.color(), &a.color(), t);
|
||||
if t < 0.5 {
|
||||
Self::interpolate(a, &Brush::SolidColor(color), t * 2.)
|
||||
} else {
|
||||
Self::interpolate(&Brush::SolidColor(color), b, (t - 0.5) * 2.)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::float_cmp)] // We want bit-wise equality here
|
||||
fn test_linear_gradient_encoding() {
|
||||
let stops: SharedVector<GradientStop> = [
|
||||
GradientStop { position: 0.0, color: Color::from_argb_u8(255, 255, 0, 0) },
|
||||
GradientStop { position: 0.5, color: Color::from_argb_u8(255, 0, 255, 0) },
|
||||
GradientStop { position: 1.0, color: Color::from_argb_u8(255, 0, 0, 255) },
|
||||
]
|
||||
.into();
|
||||
let grad = LinearGradientBrush::new(256., stops.clone());
|
||||
assert_eq!(grad.angle(), 256.);
|
||||
assert!(grad.stops().eq(stops.iter()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conic_gradient_basic() {
|
||||
// Test basic conic gradient with no rotation
|
||||
let grad = ConicGradientBrush::new(
|
||||
0.0,
|
||||
[
|
||||
GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||||
GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
|
||||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||||
],
|
||||
);
|
||||
assert_eq!(grad.angle(), 0.0);
|
||||
assert_eq!(grad.stops().count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conic_gradient_with_rotation() {
|
||||
// Test conic gradient with 90 degree rotation
|
||||
let grad = ConicGradientBrush::new(
|
||||
90.0,
|
||||
[
|
||||
GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||||
],
|
||||
);
|
||||
assert_eq!(grad.angle(), 90.0);
|
||||
// After rotation, stops should still be present and sorted
|
||||
assert!(grad.stops().count() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conic_gradient_negative_angle() {
|
||||
// Test with negative angle - should be normalized
|
||||
let grad = ConicGradientBrush::new(
|
||||
-90.0,
|
||||
[GradientStop { position: 0.5, color: Color::from_rgb_u8(255, 0, 0) }],
|
||||
);
|
||||
assert_eq!(grad.angle(), -90.0); // Angle is stored as-is
|
||||
assert!(grad.stops().count() >= 2); // Should have boundary stops added
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conic_gradient_stops_outside_range() {
|
||||
// Test with stops outside [0, 1] range
|
||||
let grad = ConicGradientBrush::new(
|
||||
0.0,
|
||||
[
|
||||
GradientStop { position: -0.2, color: Color::from_rgb_u8(255, 0, 0) },
|
||||
GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
|
||||
GradientStop { position: 1.2, color: Color::from_rgb_u8(0, 0, 255) },
|
||||
],
|
||||
);
|
||||
// All stops should be within [0, 1] after processing
|
||||
for stop in grad.stops() {
|
||||
assert!(stop.position >= 0.0 && stop.position <= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conic_gradient_all_stops_below_zero() {
|
||||
// Test edge case: all stops are below 0
|
||||
let grad = ConicGradientBrush::new(
|
||||
0.0,
|
||||
[
|
||||
GradientStop { position: -0.5, color: Color::from_rgb_u8(255, 0, 0) },
|
||||
GradientStop { position: -0.3, color: Color::from_rgb_u8(0, 255, 0) },
|
||||
],
|
||||
);
|
||||
// Should create valid boundary stops
|
||||
assert!(grad.stops().count() >= 2);
|
||||
// First stop should be at or near 0.0
|
||||
let first = grad.stops().next().unwrap();
|
||||
assert!(first.position >= 0.0 && first.position < 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conic_gradient_all_stops_above_one() {
|
||||
// Test edge case: all stops are above 1
|
||||
let grad = ConicGradientBrush::new(
|
||||
0.0,
|
||||
[
|
||||
GradientStop { position: 1.2, color: Color::from_rgb_u8(255, 0, 0) },
|
||||
GradientStop { position: 1.5, color: Color::from_rgb_u8(0, 255, 0) },
|
||||
],
|
||||
);
|
||||
// Should create valid boundary stops
|
||||
assert!(grad.stops().count() >= 2);
|
||||
// Last stop should be at or near 1.0
|
||||
let last = grad.stops().last().unwrap();
|
||||
assert!(last.position > 0.9 && last.position <= 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conic_gradient_empty() {
|
||||
// Test edge case: no stops provided
|
||||
let grad = ConicGradientBrush::new(0.0, []);
|
||||
// Should create default transparent stops
|
||||
assert_eq!(grad.stops().count(), 2);
|
||||
}
|
||||
+905
@@ -0,0 +1,905 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains color related types for the run-time library.
|
||||
*/
|
||||
|
||||
use crate::properties::InterpolatedPropertyValue;
|
||||
#[cfg(not(feature = "std"))]
|
||||
#[allow(unused_imports)]
|
||||
use num_traits::Float;
|
||||
|
||||
/// RgbaColor stores the red, green, blue and alpha components of a color
|
||||
/// with the precision of the generic parameter T. For example if T is f32,
|
||||
/// the values are normalized between 0 and 1. If T is u8, they values range
|
||||
/// is 0 to 255.
|
||||
/// This is merely a helper class for use with [`Color`].
|
||||
#[derive(Copy, Clone, PartialEq, Debug, Default)]
|
||||
pub struct RgbaColor<T> {
|
||||
/// The alpha component.
|
||||
pub alpha: T,
|
||||
/// The red channel.
|
||||
pub red: T,
|
||||
/// The green channel.
|
||||
pub green: T,
|
||||
/// The blue channel.
|
||||
pub blue: T,
|
||||
}
|
||||
|
||||
#[cfg(feature = "32-bit-color")]
|
||||
type Channel = f32;
|
||||
#[cfg(not(feature = "32-bit-color"))]
|
||||
type Channel = u8;
|
||||
|
||||
/// Color represents a color in the Slint run-time, represented using 8-bit channels for
|
||||
/// red, green, blue and the alpha (opacity).
|
||||
/// It can be conveniently converted using the `to_` and `from_` (a)rgb helper functions:
|
||||
/// ```
|
||||
/// # fn do_something_with_red_and_green(_:f32, _:f32) {}
|
||||
/// # fn do_something_with_red(_:u8) {}
|
||||
/// # use i_slint_core::graphics::{Color, RgbaColor};
|
||||
/// # let some_color = Color::from_rgb_u8(0, 0, 0);
|
||||
/// let col = some_color.to_argb_f32();
|
||||
/// do_something_with_red_and_green(col.red, col.green);
|
||||
///
|
||||
/// let RgbaColor { red, blue, green, .. } = some_color.to_argb_u8();
|
||||
/// do_something_with_red(red);
|
||||
///
|
||||
/// let new_col = Color::from(RgbaColor{ red: 0.5, green: 0.65, blue: 0.32, alpha: 1.});
|
||||
/// ```
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct Color {
|
||||
red: Channel,
|
||||
green: Channel,
|
||||
blue: Channel,
|
||||
alpha: Channel,
|
||||
}
|
||||
|
||||
// until slint uses rust 1.90 as MSRV.
|
||||
const fn round(mut value: f32) -> u8 {
|
||||
if value % 1.0 > 0.5 {
|
||||
value += 0.5;
|
||||
}
|
||||
|
||||
value as _
|
||||
}
|
||||
|
||||
const fn quantize(value: f32) -> u8 {
|
||||
round(value * 255.0)
|
||||
}
|
||||
|
||||
const fn unquantize(value: u8) -> f32 {
|
||||
(value as f32) / 255.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unquantize_roundtrip() {
|
||||
for v in 0..=255 {
|
||||
assert_eq!(v, quantize(unquantize(v)));
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbaColor<u8>> for RgbaColor<f32> {
|
||||
#[inline]
|
||||
fn from(col: RgbaColor<u8>) -> Self {
|
||||
Self {
|
||||
red: unquantize(col.red),
|
||||
green: unquantize(col.green),
|
||||
blue: unquantize(col.blue),
|
||||
alpha: unquantize(col.alpha),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbaColor<f32>> for RgbaColor<u8> {
|
||||
#[inline]
|
||||
fn from(col: RgbaColor<f32>) -> Self {
|
||||
Self {
|
||||
red: quantize(col.red),
|
||||
green: quantize(col.green),
|
||||
blue: quantize(col.blue),
|
||||
alpha: quantize(col.alpha),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for RgbaColor<f32> {
|
||||
#[inline]
|
||||
fn from(col: Color) -> Self {
|
||||
#[cfg(feature = "32-bit-color")]
|
||||
{
|
||||
Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
|
||||
}
|
||||
#[cfg(not(feature = "32-bit-color"))]
|
||||
{
|
||||
let col: RgbaColor<u8> = col.into();
|
||||
col.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbaColor<f32>> for Color {
|
||||
#[inline]
|
||||
fn from(col: RgbaColor<f32>) -> Self {
|
||||
#[cfg(feature = "32-bit-color")]
|
||||
{
|
||||
Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
|
||||
}
|
||||
#[cfg(not(feature = "32-bit-color"))]
|
||||
{
|
||||
let col: RgbaColor<u8> = col.into();
|
||||
col.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbaColor<u8>> for Color {
|
||||
#[inline]
|
||||
fn from(col: RgbaColor<u8>) -> Self {
|
||||
#[cfg(feature = "32-bit-color")]
|
||||
{
|
||||
let col: RgbaColor<f32> = col.into();
|
||||
col.into()
|
||||
}
|
||||
#[cfg(not(feature = "32-bit-color"))]
|
||||
{
|
||||
Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for RgbaColor<u8> {
|
||||
#[inline]
|
||||
fn from(col: Color) -> Self {
|
||||
#[cfg(feature = "32-bit-color")]
|
||||
{
|
||||
let col: RgbaColor<f32> = col.into();
|
||||
col.into()
|
||||
}
|
||||
#[cfg(not(feature = "32-bit-color"))]
|
||||
{
|
||||
Self { red: col.red, green: col.green, blue: col.blue, alpha: col.alpha }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Construct a color from an integer encoded as `0xAARRGGBB`
|
||||
pub const fn from_argb_encoded(encoded: u32) -> Color {
|
||||
Self::from_argb_u8(
|
||||
(encoded >> 24) as u8,
|
||||
(encoded >> 16) as u8,
|
||||
(encoded >> 8) as u8,
|
||||
encoded as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `(alpha, red, green, blue)` encoded as u32
|
||||
pub fn as_argb_encoded(&self) -> u32 {
|
||||
let col: RgbaColor<u8> = (*self).into();
|
||||
((col.red as u32) << 16)
|
||||
| ((col.green as u32) << 8)
|
||||
| (col.blue as u32)
|
||||
| ((col.alpha as u32) << 24)
|
||||
}
|
||||
|
||||
/// Construct a color from the alpha, red, green and blue color channel parameters.
|
||||
pub const fn from_argb_u8(alpha: u8, red: u8, green: u8, blue: u8) -> Self {
|
||||
#[cfg(feature = "32-bit-color")]
|
||||
{
|
||||
Self {
|
||||
red: unquantize(red),
|
||||
green: unquantize(green),
|
||||
blue: unquantize(blue),
|
||||
alpha: unquantize(alpha),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "32-bit-color"))]
|
||||
{
|
||||
Self { red, green, blue, alpha }
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a color from the red, green and blue color channel parameters. The alpha
|
||||
/// channel will have the value 255.
|
||||
pub const fn from_rgb_u8(red: u8, green: u8, blue: u8) -> Self {
|
||||
Self::from_argb_u8(255, red, green, blue)
|
||||
}
|
||||
|
||||
/// Construct a color from the alpha, red, green and blue color channel parameters.
|
||||
pub fn from_argb_f32(alpha: f32, red: f32, green: f32, blue: f32) -> Self {
|
||||
RgbaColor { alpha, red, green, blue }.into()
|
||||
}
|
||||
|
||||
/// Construct a color from the red, green and blue color channel parameters. The alpha
|
||||
/// channel will have the value 255.
|
||||
pub fn from_rgb_f32(red: f32, green: f32, blue: f32) -> Self {
|
||||
Self::from_argb_f32(1.0, red, green, blue)
|
||||
}
|
||||
|
||||
/// Converts this color to an RgbaColor struct for easy destructuring.
|
||||
pub fn to_argb_u8(&self) -> RgbaColor<u8> {
|
||||
RgbaColor::from(*self)
|
||||
}
|
||||
|
||||
/// Converts this color to an RgbaColor struct for easy destructuring.
|
||||
pub fn to_argb_f32(&self) -> RgbaColor<f32> {
|
||||
RgbaColor::from(*self)
|
||||
}
|
||||
|
||||
/// Converts this color to the HSV color space.
|
||||
pub fn to_hsva(&self) -> HsvaColor {
|
||||
let rgba: RgbaColor<f32> = (*self).into();
|
||||
rgba.into()
|
||||
}
|
||||
|
||||
/// Construct a color from the hue, saturation, and value HSV color space parameters.
|
||||
///
|
||||
/// Hue is between 0 and 360, the others parameters between 0 and 1.
|
||||
pub fn from_hsva(hue: f32, saturation: f32, value: f32, alpha: f32) -> Self {
|
||||
let hsva = HsvaColor { hue, saturation, value, alpha };
|
||||
<RgbaColor<f32>>::from(hsva).into()
|
||||
}
|
||||
|
||||
/// Converts this color to the Oklch color space.
|
||||
///
|
||||
/// Oklch is a perceptually uniform color space with:
|
||||
/// - Lightness (L): 0 to 1
|
||||
/// - Chroma (C): typically 0 to ~0.4
|
||||
/// - Hue (h): 0 to 360 degrees
|
||||
pub fn to_oklch(&self) -> OklchColor {
|
||||
let rgba: RgbaColor<f32> = (*self).into();
|
||||
rgba.into()
|
||||
}
|
||||
|
||||
/// Construct a color from the Oklch color space parameters.
|
||||
///
|
||||
/// - `lightness`: 0 to 1 (black to white)
|
||||
/// - `chroma`: typically 0 to ~0.4 (grayscale to vivid)
|
||||
/// - `hue`: 0 to 360 degrees
|
||||
/// - `alpha`: 0 to 1
|
||||
pub fn from_oklch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self {
|
||||
let oklch = OklchColor { lightness, chroma, hue, alpha };
|
||||
<RgbaColor<f32>>::from(oklch).into()
|
||||
}
|
||||
|
||||
/// Returns the red channel of the color as u8 in the range 0..255.
|
||||
#[inline(always)]
|
||||
pub fn red(self) -> u8 {
|
||||
RgbaColor::<u8>::from(self).red
|
||||
}
|
||||
|
||||
/// Returns the green channel of the color as u8 in the range 0..255.
|
||||
#[inline(always)]
|
||||
pub fn green(self) -> u8 {
|
||||
RgbaColor::<u8>::from(self).green
|
||||
}
|
||||
|
||||
/// Returns the blue channel of the color as u8 in the range 0..255.
|
||||
#[inline(always)]
|
||||
pub fn blue(self) -> u8 {
|
||||
RgbaColor::<u8>::from(self).blue
|
||||
}
|
||||
|
||||
/// Returns the alpha channel of the color as u8 in the range 0..255.
|
||||
#[inline(always)]
|
||||
pub fn alpha(self) -> u8 {
|
||||
RgbaColor::<u8>::from(self).alpha
|
||||
}
|
||||
|
||||
/// Returns a new version of this color that has the brightness increased
|
||||
/// by the specified factor. This is done by converting the color to the HSV
|
||||
/// color space and multiplying the brightness (value) with (1 + factor).
|
||||
/// The result is converted back to RGB and the alpha channel is unchanged.
|
||||
/// So for example `brighter(0.2)` will increase the brightness by 20%, and
|
||||
/// calling `brighter(-0.5)` will return a color that's 50% darker.
|
||||
#[must_use]
|
||||
pub fn brighter(&self, factor: f32) -> Self {
|
||||
let rgba: RgbaColor<f32> = (*self).into();
|
||||
let mut hsva: HsvaColor = rgba.into();
|
||||
hsva.value *= 1. + factor;
|
||||
let rgba: RgbaColor<f32> = hsva.into();
|
||||
rgba.into()
|
||||
}
|
||||
|
||||
/// Returns a new version of this color that has the brightness decreased
|
||||
/// by the specified factor. This is done by converting the color to the HSV
|
||||
/// color space and dividing the brightness (value) by (1 + factor). The
|
||||
/// result is converted back to RGB and the alpha channel is unchanged.
|
||||
/// So for example `darker(0.3)` will decrease the brightness by 30%.
|
||||
#[must_use]
|
||||
pub fn darker(&self, factor: f32) -> Self {
|
||||
let rgba: RgbaColor<f32> = (*self).into();
|
||||
let mut hsva: HsvaColor = rgba.into();
|
||||
hsva.value /= 1. + factor;
|
||||
let rgba: RgbaColor<f32> = hsva.into();
|
||||
rgba.into()
|
||||
}
|
||||
|
||||
/// Returns a new version of this color with the opacity decreased by `factor`.
|
||||
///
|
||||
/// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
|
||||
///
|
||||
/// # Examples
|
||||
/// Decreasing the opacity of a red color by half:
|
||||
/// ```
|
||||
/// # use i_slint_core::graphics::Color;
|
||||
/// let red = Color::from_argb_f32(1.0, 1.0, 0.0, 0.0);
|
||||
/// assert_eq!(red.transparentize(0.5), Color::from_argb_f32(0.5, 1.0, 0.0, 0.0));
|
||||
/// ```
|
||||
///
|
||||
/// Decreasing the opacity of a blue color by 20%:
|
||||
/// ```
|
||||
/// # use i_slint_core::graphics::Color;
|
||||
/// let blue = Color::from_argb_f32(1.0, 0.0, 0.0, 1.0);
|
||||
/// assert_eq!(blue.transparentize(0.2), Color::from_argb_f32(0.8, 0.0, 0.0, 1.0));
|
||||
/// ```
|
||||
///
|
||||
/// Negative values increase the opacity
|
||||
///
|
||||
/// ```
|
||||
/// # use i_slint_core::graphics::Color;
|
||||
/// let blue = Color::from_argb_f32(0.5, 0.0, 0.0, 1.0);
|
||||
/// assert_eq!(blue.transparentize(-0.1), Color::from_argb_f32(0.55, 0.0, 0.0, 1.0));
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn transparentize(&self, factor: f32) -> Self {
|
||||
let mut col: RgbaColor<f32> = (*self).into();
|
||||
col.alpha = (col.alpha * (1.0 - factor)).clamp(0.0, 1.0);
|
||||
col.into()
|
||||
}
|
||||
|
||||
/// Returns a new color that is a mix of this color and `other`. The specified factor is
|
||||
/// clamped to be between `0.0` and `1.0` and then applied to this color, while `1.0 - factor`
|
||||
/// is applied to `other`.
|
||||
///
|
||||
/// # Examples
|
||||
/// Mix red with black half-and-half:
|
||||
/// ```
|
||||
/// # use i_slint_core::graphics::Color;
|
||||
/// let red = Color::from_rgb_f32(1.0, 0.0, 0.0);
|
||||
/// let black = Color::from_rgb_f32(0.0, 0.0, 0.0);
|
||||
/// assert_eq!(red.mix(&black, 0.5), Color::from_rgb_f32(0.5, 0.0, 0.0));
|
||||
/// ```
|
||||
///
|
||||
/// Mix Purple with OrangeRed, with `75%` purpe and `25%` orange red ratio:
|
||||
/// ```
|
||||
/// # use i_slint_core::graphics::{Color, RgbaColor};
|
||||
/// let purple = Color::from_rgb_u8(128, 0, 128);
|
||||
/// let orange_red = Color::from_rgb_u8(255, 69, 0);
|
||||
/// assert_eq!(purple.mix(&orange_red, 0.75), Color::from_rgb_f32(0.6264706, 0.06764706, 0.37647063));
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn mix(&self, other: &Self, factor: f32) -> Self {
|
||||
// * NOTE: The opacity (`alpha` as a "percentage") of each color involved
|
||||
// * must be taken into account when mixing them. Because of this,
|
||||
// * we cannot just interpolate between them.
|
||||
// * NOTE: Considering the spec (textual):
|
||||
// * <https://github.com/sass/sass/blob/47d30713765b975c86fa32ec359ed16e83ad1ecc/spec/built-in-modules/color.md#mix>
|
||||
|
||||
fn lerp(v1: f32, v2: f32, f: f32) -> f32 {
|
||||
(v1 * f + v2 * (1.0 - f)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
let original_factor = factor.clamp(0.0, 1.0);
|
||||
|
||||
let col = RgbaColor::<f32>::from(*self);
|
||||
let other = RgbaColor::<f32>::from(*other);
|
||||
|
||||
let normal_weight = 2.0 * original_factor - 1.0;
|
||||
let alpha_distance = col.alpha - other.alpha;
|
||||
let weight_by_distance = normal_weight * alpha_distance;
|
||||
|
||||
// As to not divide by 0.0
|
||||
let combined_weight = if weight_by_distance == -1.0 {
|
||||
normal_weight
|
||||
} else {
|
||||
(normal_weight + alpha_distance) / (1.0 + weight_by_distance)
|
||||
};
|
||||
|
||||
let channels_factor = (combined_weight + 1.0) / 2.0;
|
||||
|
||||
let red = lerp(col.red, other.red, channels_factor);
|
||||
let green = lerp(col.green, other.green, channels_factor);
|
||||
let blue = lerp(col.blue, other.blue, channels_factor);
|
||||
|
||||
let alpha = lerp(col.alpha, other.alpha, original_factor);
|
||||
|
||||
RgbaColor { red, green, blue, alpha }.into()
|
||||
}
|
||||
|
||||
/// Returns a new version of this color with the opacity set to `alpha`.
|
||||
#[must_use]
|
||||
pub fn with_alpha(&self, alpha: f32) -> Self {
|
||||
let mut rgba: RgbaColor<f32> = (*self).into();
|
||||
rgba.alpha = alpha.clamp(0.0, 1.0);
|
||||
rgba.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl InterpolatedPropertyValue for Color {
|
||||
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
|
||||
target_value.mix(self, t)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Color {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "argb({}, {}, {}, {})", self.alpha(), self.red(), self.green(), self.blue())
|
||||
}
|
||||
}
|
||||
|
||||
/// HsvaColor stores the hue, saturation, value and alpha components of a color
|
||||
/// in the HSV color space as `f32 ` fields.
|
||||
/// This is merely a helper struct for use with [`Color`].
|
||||
#[derive(Copy, Clone, PartialOrd, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct HsvaColor {
|
||||
/// The hue component in degrees between 0 and 360.
|
||||
pub hue: f32,
|
||||
/// The saturation component, between 0 and 1.
|
||||
pub saturation: f32,
|
||||
/// The value component, between 0 and 1.
|
||||
pub value: f32,
|
||||
/// The alpha component, between 0 and 1.
|
||||
pub alpha: f32,
|
||||
}
|
||||
|
||||
impl PartialEq for HsvaColor {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(self.hue - other.hue).abs() < 0.00001
|
||||
&& (self.saturation - other.saturation).abs() < 0.00001
|
||||
&& (self.value - other.value).abs() < 0.00001
|
||||
&& (self.alpha - other.alpha).abs() < 0.00001
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbaColor<f32>> for HsvaColor {
|
||||
fn from(col: RgbaColor<f32>) -> Self {
|
||||
// RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
|
||||
|
||||
let red = col.red;
|
||||
let green = col.green;
|
||||
let blue = col.blue;
|
||||
|
||||
let min = red.min(green).min(blue);
|
||||
let max = red.max(green).max(blue);
|
||||
let chroma = max - min;
|
||||
|
||||
#[allow(clippy::float_cmp)] // `max` is either `red`, `green` or `blue`
|
||||
let hue = num_traits::Euclid::rem_euclid(
|
||||
&(60.
|
||||
* if chroma == 0.0 {
|
||||
0.0
|
||||
} else if max == red {
|
||||
((green - blue) / chroma) % 6.0
|
||||
} else if max == green {
|
||||
2. + (blue - red) / chroma
|
||||
} else {
|
||||
4. + (red - green) / chroma
|
||||
}),
|
||||
&360.0,
|
||||
);
|
||||
let saturation = if max == 0. { 0. } else { chroma / max };
|
||||
|
||||
Self { hue, saturation, value: max, alpha: col.alpha }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HsvaColor> for RgbaColor<f32> {
|
||||
fn from(col: HsvaColor) -> Self {
|
||||
// RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
|
||||
|
||||
let chroma = col.saturation * col.value;
|
||||
|
||||
let hue = num_traits::Euclid::rem_euclid(&col.hue, &360.0);
|
||||
|
||||
let x = chroma * (1. - ((hue / 60.) % 2. - 1.).abs());
|
||||
|
||||
let (red, green, blue) = match (hue / 60.0) as usize {
|
||||
0 => (chroma, x, 0.),
|
||||
1 => (x, chroma, 0.),
|
||||
2 => (0., chroma, x),
|
||||
3 => (0., x, chroma),
|
||||
4 => (x, 0., chroma),
|
||||
5 => (chroma, 0., x),
|
||||
_ => (0., 0., 0.),
|
||||
};
|
||||
|
||||
let m = col.value - chroma;
|
||||
|
||||
Self { red: red + m, green: green + m, blue: blue + m, alpha: col.alpha }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HsvaColor> for Color {
|
||||
fn from(value: HsvaColor) -> Self {
|
||||
RgbaColor::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for HsvaColor {
|
||||
fn from(value: Color) -> Self {
|
||||
value.to_hsva()
|
||||
}
|
||||
}
|
||||
|
||||
/// OklchColor stores the lightness, chroma, hue and alpha components of a color
|
||||
/// in the Oklch color space as `f32` fields.
|
||||
/// Oklch is a perceptually uniform color space, useful for color manipulation.
|
||||
/// This is merely a helper struct for use with [`Color`].
|
||||
///
|
||||
/// Reference: <https://bottosson.github.io/posts/oklab/>
|
||||
#[derive(Copy, Clone, PartialOrd, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct OklchColor {
|
||||
/// The lightness component, between 0 (black) and 1 (white).
|
||||
pub lightness: f32,
|
||||
/// The chroma component (color intensity), typically between 0 and about 0.4.
|
||||
pub chroma: f32,
|
||||
/// The hue component in degrees between 0 and 360.
|
||||
pub hue: f32,
|
||||
/// The alpha component, between 0 and 1.
|
||||
pub alpha: f32,
|
||||
}
|
||||
|
||||
impl PartialEq for OklchColor {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(self.lightness - other.lightness).abs() < 0.00001
|
||||
&& (self.chroma - other.chroma).abs() < 0.00001
|
||||
&& (self.hue - other.hue).abs() < 0.00001
|
||||
&& (self.alpha - other.alpha).abs() < 0.00001
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper struct for Oklab color space (intermediate representation).
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct OklabColor {
|
||||
l: f32,
|
||||
a: f32,
|
||||
b: f32,
|
||||
alpha: f32,
|
||||
}
|
||||
|
||||
impl From<OklchColor> for OklabColor {
|
||||
fn from(oklch: OklchColor) -> Self {
|
||||
let hue_rad = oklch.hue * core::f32::consts::PI / 180.0;
|
||||
Self {
|
||||
l: oklch.lightness,
|
||||
a: oklch.chroma * hue_rad.cos(),
|
||||
b: oklch.chroma * hue_rad.sin(),
|
||||
alpha: oklch.alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OklabColor> for OklchColor {
|
||||
fn from(oklab: OklabColor) -> Self {
|
||||
let chroma = (oklab.a * oklab.a + oklab.b * oklab.b).sqrt();
|
||||
let hue = if chroma < 0.00001 {
|
||||
0.0
|
||||
} else {
|
||||
let hue_rad = oklab.b.atan2(oklab.a);
|
||||
num_traits::Euclid::rem_euclid(&(hue_rad * 180.0 / core::f32::consts::PI), &360.0)
|
||||
};
|
||||
Self { lightness: oklab.l, chroma, hue, alpha: oklab.alpha }
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert sRGB component to linear RGB.
|
||||
fn srgb_to_linear(c: f32) -> f32 {
|
||||
if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
|
||||
}
|
||||
|
||||
/// Convert linear RGB component to sRGB.
|
||||
fn linear_to_srgb(c: f32) -> f32 {
|
||||
if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
|
||||
}
|
||||
|
||||
impl From<RgbaColor<f32>> for OklabColor {
|
||||
fn from(col: RgbaColor<f32>) -> Self {
|
||||
// Convert sRGB to linear RGB
|
||||
let r = srgb_to_linear(col.red);
|
||||
let g = srgb_to_linear(col.green);
|
||||
let b = srgb_to_linear(col.blue);
|
||||
|
||||
// Linear RGB to LMS (using Oklab M1 matrix)
|
||||
let l = 0.41222146 * r + 0.53633255 * g + 0.051445995 * b;
|
||||
let m = 0.2119035 * r + 0.6806995 * g + 0.10739696 * b;
|
||||
let s = 0.08830246 * r + 0.28171885 * g + 0.6299787 * b;
|
||||
|
||||
// Cube root
|
||||
let l_ = l.cbrt();
|
||||
let m_ = m.cbrt();
|
||||
let s_ = s.cbrt();
|
||||
|
||||
// LMS' to Oklab (using M2 matrix)
|
||||
Self {
|
||||
l: 0.21045426 * l_ + 0.7936178 * m_ - 0.004072047 * s_,
|
||||
a: 1.9779985 * l_ - 2.4285922 * m_ + 0.4505937 * s_,
|
||||
b: 0.025904037 * l_ + 0.78277177 * m_ - 0.80867577 * s_,
|
||||
alpha: col.alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OklabColor> for RgbaColor<f32> {
|
||||
fn from(oklab: OklabColor) -> Self {
|
||||
// Oklab to LMS' (inverse of M2 matrix)
|
||||
let l_ = oklab.l + 0.39633778 * oklab.a + 0.21580376 * oklab.b;
|
||||
let m_ = oklab.l - 0.105561346 * oklab.a - 0.06385417 * oklab.b;
|
||||
let s_ = oklab.l - 0.08948418 * oklab.a - 1.2914855 * oklab.b;
|
||||
|
||||
// Cube to get LMS
|
||||
let l = l_ * l_ * l_;
|
||||
let m = m_ * m_ * m_;
|
||||
let s = s_ * s_ * s_;
|
||||
|
||||
// LMS to linear RGB (inverse of M1 matrix)
|
||||
let r = 4.0767417 * l - 3.3077116 * m + 0.23096994 * s;
|
||||
let g = -1.268438 * l + 2.6097574 * m - 0.34131938 * s;
|
||||
let b = -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s;
|
||||
|
||||
// Convert linear RGB to sRGB and clamp
|
||||
Self {
|
||||
red: linear_to_srgb(r).clamp(0.0, 1.0),
|
||||
green: linear_to_srgb(g).clamp(0.0, 1.0),
|
||||
blue: linear_to_srgb(b).clamp(0.0, 1.0),
|
||||
alpha: oklab.alpha,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OklchColor> for RgbaColor<f32> {
|
||||
fn from(oklch: OklchColor) -> Self {
|
||||
let oklab = OklabColor::from(oklch);
|
||||
RgbaColor::from(oklab)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbaColor<f32>> for OklchColor {
|
||||
fn from(col: RgbaColor<f32>) -> Self {
|
||||
let oklab = OklabColor::from(col);
|
||||
OklchColor::from(oklab)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OklchColor> for Color {
|
||||
fn from(value: OklchColor) -> Self {
|
||||
RgbaColor::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for OklchColor {
|
||||
fn from(value: Color) -> Self {
|
||||
value.to_oklch()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_hsv() {
|
||||
// White
|
||||
assert_eq!(
|
||||
HsvaColor::from(RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.5 }),
|
||||
HsvaColor { hue: 0., saturation: 0., value: 1., alpha: 0.5 }
|
||||
);
|
||||
assert_eq!(
|
||||
RgbaColor::<f32>::from(HsvaColor { hue: 0., saturation: 0., value: 1., alpha: 0.3 }),
|
||||
RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.3 }
|
||||
);
|
||||
|
||||
// #8a0c77ff ensure the hue ends up positive
|
||||
assert_eq!(
|
||||
HsvaColor::from(Color::from_argb_u8(0xff, 0x8a, 0xc, 0x77,).to_argb_f32()),
|
||||
HsvaColor { hue: 309.0476, saturation: 0.9130435, value: 0.5411765, alpha: 1.0 }
|
||||
);
|
||||
|
||||
let received = RgbaColor::<f32>::from(HsvaColor {
|
||||
hue: 309.0476,
|
||||
saturation: 0.9130435,
|
||||
value: 0.5411765,
|
||||
alpha: 1.0,
|
||||
});
|
||||
let expected = Color::from_argb_u8(0xff, 0x8a, 0xc, 0x77).to_argb_f32();
|
||||
|
||||
assert!(
|
||||
(received.alpha - expected.alpha).abs() < 0.00001
|
||||
&& (received.red - expected.red).abs() < 0.00001
|
||||
&& (received.green - expected.green).abs() < 0.00001
|
||||
&& (received.blue - expected.blue).abs() < 0.00001
|
||||
);
|
||||
|
||||
// Bright greenish, verified via colorizer.org
|
||||
assert_eq!(
|
||||
HsvaColor::from(RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }),
|
||||
HsvaColor { hue: 120., saturation: 1., value: 0.9, alpha: 1.0 }
|
||||
);
|
||||
assert_eq!(
|
||||
RgbaColor::<f32>::from(HsvaColor { hue: 120., saturation: 1., value: 0.9, alpha: 1.0 }),
|
||||
RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }
|
||||
);
|
||||
|
||||
// Hue should wrap around 360deg i.e. 480 == 120 && -240 == 240
|
||||
assert_eq!(
|
||||
RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 },
|
||||
RgbaColor::<f32>::from(HsvaColor { hue: 480., saturation: 1., value: 0.9, alpha: 1.0 }),
|
||||
);
|
||||
assert_eq!(
|
||||
RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 },
|
||||
RgbaColor::<f32>::from(HsvaColor { hue: -240., saturation: 1., value: 0.9, alpha: 1.0 }),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_brighter_darker() {
|
||||
let blue = Color::from_rgb_u8(0, 0, 128);
|
||||
assert_eq!(blue.brighter(0.5), Color::from_rgb_f32(0.0, 0.0, 0.75294125));
|
||||
assert_eq!(blue.darker(0.5), Color::from_rgb_f32(0.0, 0.0, 0.33464053));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparent_transition() {
|
||||
let color = Color::from_argb_f32(0.0, 0.0, 0.0, 0.0);
|
||||
let interpolated = color.interpolate(&Color::from_rgb_f32(0.8, 0.8, 0.8), 0.25);
|
||||
assert_eq!(interpolated, Color::from_argb_f32(0.25, 0.8, 0.8, 0.8));
|
||||
let interpolated = color.interpolate(&Color::from_rgb_f32(0.8, 0.8, 0.8), 0.5);
|
||||
assert_eq!(interpolated, Color::from_argb_f32(0.5, 0.8, 0.8, 0.8));
|
||||
let interpolated = color.interpolate(&Color::from_rgb_f32(0.8, 0.8, 0.8), 0.75);
|
||||
assert_eq!(interpolated, Color::from_argb_f32(0.75, 0.8, 0.8, 0.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oklch_roundtrip() {
|
||||
// Test that Oklch roundtrips correctly through RGB
|
||||
// Use colors with low chroma that are definitely within sRGB gamut
|
||||
let test_colors = [
|
||||
OklchColor { lightness: 0.5, chroma: 0.08, hue: 30.0, alpha: 1.0 },
|
||||
OklchColor { lightness: 0.6, chroma: 0.1, hue: 120.0, alpha: 0.8 },
|
||||
OklchColor { lightness: 0.4, chroma: 0.08, hue: 240.0, alpha: 1.0 },
|
||||
OklchColor { lightness: 0.8, chroma: 0.05, hue: 0.0, alpha: 1.0 },
|
||||
// Grayscale (chroma = 0)
|
||||
OklchColor { lightness: 0.5, chroma: 0.0, hue: 0.0, alpha: 1.0 },
|
||||
];
|
||||
|
||||
for oklch in test_colors {
|
||||
let rgba = RgbaColor::<f32>::from(oklch);
|
||||
let roundtrip = OklchColor::from(rgba);
|
||||
// Allow some tolerance due to floating point operations and gamut mapping
|
||||
assert!(
|
||||
(oklch.lightness - roundtrip.lightness).abs() < 0.01,
|
||||
"Lightness mismatch: {:?} vs {:?}",
|
||||
oklch,
|
||||
roundtrip
|
||||
);
|
||||
// Skip chroma/hue comparison for grayscale since hue is undefined
|
||||
if oklch.chroma > 0.001 {
|
||||
assert!(
|
||||
(oklch.chroma - roundtrip.chroma).abs() < 0.02,
|
||||
"Chroma mismatch: {:?} vs {:?}",
|
||||
oklch,
|
||||
roundtrip
|
||||
);
|
||||
// Hue can wrap around, so we need to handle that
|
||||
let hue_diff = (oklch.hue - roundtrip.hue).abs();
|
||||
let hue_diff = hue_diff.min(360.0 - hue_diff);
|
||||
assert!(hue_diff < 2.0, "Hue mismatch: {:?} vs {:?}", oklch, roundtrip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oklch_known_values() {
|
||||
// Test conversion of a known Oklch value to RGB
|
||||
// These values are approximate and verified against online converters
|
||||
let red_oklch = OklchColor { lightness: 0.63, chroma: 0.26, hue: 29.0, alpha: 1.0 };
|
||||
let red_rgba = RgbaColor::<f32>::from(red_oklch);
|
||||
// Red should have high red component and low green/blue
|
||||
assert!(red_rgba.red > 0.8, "Red component should be high: {}", red_rgba.red);
|
||||
assert!(red_rgba.green < 0.3, "Green component should be low: {}", red_rgba.green);
|
||||
assert!(red_rgba.blue < 0.3, "Blue component should be low: {}", red_rgba.blue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_oklch() {
|
||||
// White: should have high lightness, zero chroma
|
||||
let white = OklchColor::from(RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.5 });
|
||||
assert!((white.lightness - 1.0).abs() < 0.01, "White lightness should be ~1.0");
|
||||
assert!(white.chroma < 0.001, "White chroma should be ~0");
|
||||
assert!((white.alpha - 0.5).abs() < 0.001, "Alpha should be preserved");
|
||||
|
||||
// Black: should have zero lightness, zero chroma
|
||||
let black = OklchColor::from(RgbaColor::<f32> { red: 0., green: 0., blue: 0., alpha: 1.0 });
|
||||
assert!(black.lightness < 0.01, "Black lightness should be ~0");
|
||||
assert!(black.chroma < 0.001, "Black chroma should be ~0");
|
||||
|
||||
// Pure red: should have hue around 29 degrees (red in Oklch)
|
||||
let red = OklchColor::from(RgbaColor::<f32> { red: 1., green: 0., blue: 0., alpha: 1.0 });
|
||||
assert!(red.lightness > 0.5 && red.lightness < 0.7, "Red lightness should be ~0.63");
|
||||
assert!(red.chroma > 0.2, "Red should have significant chroma");
|
||||
assert!(red.hue > 20.0 && red.hue < 40.0, "Red hue should be around 29 degrees");
|
||||
|
||||
// Pure blue: should have hue around 264 degrees
|
||||
let blue = OklchColor::from(RgbaColor::<f32> { red: 0., green: 0., blue: 1., alpha: 1.0 });
|
||||
assert!(blue.lightness > 0.4 && blue.lightness < 0.5, "Blue lightness should be ~0.45");
|
||||
assert!(blue.chroma > 0.2, "Blue should have significant chroma");
|
||||
assert!(blue.hue > 250.0 && blue.hue < 280.0, "Blue hue should be around 264 degrees");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
pub(crate) mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
use super::*;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_color_brighter(col: &Color, factor: f32, out: *mut Color) {
|
||||
unsafe { core::ptr::write(out, col.brighter(factor)) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_color_darker(col: &Color, factor: f32, out: *mut Color) {
|
||||
unsafe { core::ptr::write(out, col.darker(factor)) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_color_transparentize(col: &Color, factor: f32, out: *mut Color) {
|
||||
unsafe { core::ptr::write(out, col.transparentize(factor)) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_color_mix(
|
||||
col1: &Color,
|
||||
col2: &Color,
|
||||
factor: f32,
|
||||
out: *mut Color,
|
||||
) {
|
||||
unsafe { core::ptr::write(out, col1.mix(col2, factor)) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_color_with_alpha(col: &Color, alpha: f32, out: *mut Color) {
|
||||
unsafe { core::ptr::write(out, col.with_alpha(alpha)) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_color_to_hsva(
|
||||
col: &Color,
|
||||
h: &mut f32,
|
||||
s: &mut f32,
|
||||
v: &mut f32,
|
||||
a: &mut f32,
|
||||
) {
|
||||
let hsv = col.to_hsva();
|
||||
*h = hsv.hue;
|
||||
*s = hsv.saturation;
|
||||
*v = hsv.value;
|
||||
*a = hsv.alpha;
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_color_from_hsva(h: f32, s: f32, v: f32, a: f32) -> Color {
|
||||
Color::from_hsva(h, s, v, a)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_color_from_oklch(l: f32, c: f32, h: f32, a: f32) -> Color {
|
||||
Color::from_oklch(l, c, h, a)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_color_to_oklch(
|
||||
col: &Color,
|
||||
l: &mut f32,
|
||||
c: &mut f32,
|
||||
h: &mut f32,
|
||||
a: &mut f32,
|
||||
) {
|
||||
let oklch = col.to_oklch();
|
||||
*l = oklch.lightness;
|
||||
*c = oklch.chroma;
|
||||
*h = oklch.hue;
|
||||
*a = oklch.alpha;
|
||||
}
|
||||
}
|
||||
+1519
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,241 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains image and caching related types for the run-time library.
|
||||
*/
|
||||
|
||||
use super::{CachedPath, Image, ImageCacheKey, ImageInner, SharedImageBuffer, SharedPixelBuffer};
|
||||
use crate::{SharedString, slice::Slice};
|
||||
|
||||
struct ImageWeightInBytes;
|
||||
|
||||
impl clru::WeightScale<ImageCacheKey, ImageInner> for ImageWeightInBytes {
|
||||
fn weight(&self, _key: &ImageCacheKey, value: &ImageInner) -> usize {
|
||||
match value {
|
||||
ImageInner::None => 0,
|
||||
ImageInner::EmbeddedImage { buffer, .. } => match buffer {
|
||||
SharedImageBuffer::RGB8(pixels) => pixels.as_bytes().len(),
|
||||
SharedImageBuffer::RGBA8(pixels) => pixels.as_bytes().len(),
|
||||
SharedImageBuffer::RGBA8Premultiplied(pixels) => pixels.as_bytes().len(),
|
||||
},
|
||||
#[cfg(feature = "svg")]
|
||||
ImageInner::Svg(_) => 512, // Don't know how to measure the size of the parsed SVG tree...
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
ImageInner::HTMLImage(_) => 512, // Something... the web browser maintainers its own cache. The purpose of this cache is to reduce the amount of DOM elements.
|
||||
ImageInner::StaticTextures(_) => 0,
|
||||
ImageInner::BackendStorage(x) => vtable::VRc::borrow(x).size().area() as usize,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ImageInner::BorrowedOpenGLTexture(..) => 0, // Assume storage in GPU memory
|
||||
ImageInner::NineSlice(nine) => self.weight(_key, &nine.0),
|
||||
#[cfg(any(feature = "unstable-wgpu-27", feature = "unstable-wgpu-28"))]
|
||||
ImageInner::WGPUTexture(..) => 0, // The texture is imported from the application and will never reside in our cache.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache used to avoid repeatedly decoding images from disk.
|
||||
pub(crate) struct ImageCache(
|
||||
clru::CLruCache<
|
||||
ImageCacheKey,
|
||||
ImageInner,
|
||||
std::collections::hash_map::RandomState,
|
||||
ImageWeightInBytes,
|
||||
>,
|
||||
);
|
||||
|
||||
crate::thread_local!(pub(crate) static IMAGE_CACHE: core::cell::RefCell<ImageCache> =
|
||||
core::cell::RefCell::new(
|
||||
ImageCache(
|
||||
clru::CLruCache::with_config(
|
||||
clru::CLruCacheConfig::new(core::num::NonZeroUsize::new(5 * 1024 * 1024).unwrap())
|
||||
.with_scale(ImageWeightInBytes)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
impl ImageCache {
|
||||
// Look up the given image cache key in the image cache and upgrade the weak reference to a strong one if found,
|
||||
// otherwise a new image is created/loaded from the given callback.
|
||||
fn lookup_image_in_cache_or_create(
|
||||
&mut self,
|
||||
cache_key: ImageCacheKey,
|
||||
image_create_fn: impl Fn(ImageCacheKey) -> Option<ImageInner>,
|
||||
) -> Option<Image> {
|
||||
Some(Image(if let Some(entry) = self.0.get(&cache_key) {
|
||||
entry.clone()
|
||||
} else {
|
||||
let new_image = image_create_fn(cache_key.clone())?;
|
||||
self.0.put_with_weight(cache_key, new_image.clone()).ok();
|
||||
new_image
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn load_image_from_path(&mut self, path: &SharedString) -> Option<Image> {
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let cache_key = ImageCacheKey::Path(CachedPath::new(path.as_str()));
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return self.lookup_image_in_cache_or_create(cache_key, |_| {
|
||||
return Some(ImageInner::HTMLImage(vtable::VRc::new(
|
||||
super::htmlimage::HTMLImage::new(&path),
|
||||
)));
|
||||
});
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
return self.lookup_image_in_cache_or_create(cache_key, |cache_key| {
|
||||
if cfg!(feature = "svg") && (path.ends_with(".svg") || path.ends_with(".svgz")) {
|
||||
return Some(ImageInner::Svg(vtable::VRc::new(
|
||||
super::svg::load_from_path(path, cache_key).map_or_else(
|
||||
|err| {
|
||||
crate::debug_log!("Error loading SVG from {}: {}", &path, err);
|
||||
None
|
||||
},
|
||||
Some,
|
||||
)?,
|
||||
)));
|
||||
}
|
||||
|
||||
image::open(std::path::Path::new(&path.as_str())).map_or_else(
|
||||
|decode_err| {
|
||||
crate::debug_log!("Error loading image from {}: {}", &path, decode_err);
|
||||
None
|
||||
},
|
||||
|image| {
|
||||
Some(ImageInner::EmbeddedImage {
|
||||
cache_key,
|
||||
buffer: dynamic_image_to_shared_image_buffer(image),
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn load_image_from_embedded_data(
|
||||
&mut self,
|
||||
data: Slice<'static, u8>,
|
||||
format: Slice<'_, u8>,
|
||||
) -> Option<Image> {
|
||||
let cache_key = ImageCacheKey::from_embedded_image_data(data.as_slice());
|
||||
self.lookup_image_in_cache_or_create(cache_key, |cache_key| {
|
||||
#[cfg(feature = "svg")]
|
||||
if format.as_slice() == b"svg" || format.as_slice() == b"svgz" {
|
||||
return Some(ImageInner::Svg(vtable::VRc::new(
|
||||
super::svg::load_from_data(data.as_slice(), cache_key).map_or_else(
|
||||
|svg_err| {
|
||||
crate::debug_log!("Error loading SVG: {}", svg_err);
|
||||
None
|
||||
},
|
||||
Some,
|
||||
)?,
|
||||
)));
|
||||
}
|
||||
|
||||
let format = std::str::from_utf8(format.as_slice())
|
||||
.ok()
|
||||
.and_then(image::ImageFormat::from_extension);
|
||||
let maybe_image = if let Some(format) = format {
|
||||
image::load_from_memory_with_format(data.as_slice(), format)
|
||||
} else {
|
||||
image::load_from_memory(data.as_slice())
|
||||
};
|
||||
|
||||
match maybe_image {
|
||||
Ok(image) => Some(ImageInner::EmbeddedImage {
|
||||
cache_key,
|
||||
buffer: dynamic_image_to_shared_image_buffer(image),
|
||||
}),
|
||||
Err(decode_err) => {
|
||||
crate::debug_log!("Error decoding embedded image: {}", decode_err);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn dynamic_image_to_shared_image_buffer(dynamic_image: image::DynamicImage) -> SharedImageBuffer {
|
||||
use rgb::AsPixels;
|
||||
|
||||
if dynamic_image.color().has_alpha() {
|
||||
let rgba8image = dynamic_image.to_rgba8();
|
||||
// Prefer pre-multiplied alpha so that smooth-scaling won't bleed the alpha when blending
|
||||
// in the renderers.
|
||||
SharedImageBuffer::RGBA8Premultiplied(SharedPixelBuffer {
|
||||
width: rgba8image.width(),
|
||||
height: rgba8image.height(),
|
||||
data: rgba8image
|
||||
.as_pixels()
|
||||
.iter()
|
||||
.map(|pixel| Image::rgba_to_premultiplied_rgba(*pixel))
|
||||
.collect(),
|
||||
})
|
||||
} else {
|
||||
let rgb8image = dynamic_image.to_rgb8();
|
||||
SharedImageBuffer::RGB8(SharedPixelBuffer::clone_from_slice(
|
||||
rgb8image.as_raw(),
|
||||
rgb8image.width(),
|
||||
rgb8image.height(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the cached image key with the given value
|
||||
pub fn replace_cached_image(key: ImageCacheKey, value: ImageInner) {
|
||||
if key == ImageCacheKey::Invalid {
|
||||
return;
|
||||
}
|
||||
let _ =
|
||||
IMAGE_CACHE.with(|global_cache| global_cache.borrow_mut().0.put_with_weight(key, value));
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "std"))]
|
||||
mod tests {
|
||||
use crate::graphics::Rgba8Pixel;
|
||||
|
||||
#[test]
|
||||
fn test_path_cache_invalidation() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let test_path = [temp_dir.path(), std::path::Path::new("testfile.png")]
|
||||
.iter()
|
||||
.collect::<std::path::PathBuf>();
|
||||
|
||||
let red_image = image::RgbImage::from_pixel(10, 10, image::Rgb([255, 0, 0]));
|
||||
red_image.save(&test_path).unwrap();
|
||||
let red_slint_image = crate::graphics::Image::load_from_path(&test_path).unwrap();
|
||||
let buffer = red_slint_image.to_rgba8().unwrap();
|
||||
assert!(
|
||||
buffer
|
||||
.as_slice()
|
||||
.iter()
|
||||
.all(|pixel| *pixel == Rgba8Pixel { r: 255, g: 0, b: 0, a: 255 })
|
||||
);
|
||||
|
||||
let green_image = image::RgbImage::from_pixel(10, 10, image::Rgb([0, 255, 0]));
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
|
||||
green_image.save(&test_path).unwrap();
|
||||
|
||||
/* Can't use this until we use Rust 1.78
|
||||
let mod_time = std::fs::metadata(&test_path).unwrap().modified().unwrap();
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.open(&test_path)
|
||||
.unwrap()
|
||||
.set_modified(mod_time.checked_add(std::time::Duration::from_secs(2)).unwrap())
|
||||
.unwrap();
|
||||
*/
|
||||
|
||||
let green_slint_image = crate::graphics::Image::load_from_path(&test_path).unwrap();
|
||||
let buffer = green_slint_image.to_rgba8().unwrap();
|
||||
assert!(
|
||||
buffer
|
||||
.as_slice()
|
||||
.iter()
|
||||
.all(|pixel| *pixel == Rgba8Pixel { r: 0, g: 255, b: 0, a: 255 })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use alloc::rc::Rc;
|
||||
|
||||
use super::ImageCacheKey;
|
||||
use crate::Property;
|
||||
use crate::graphics::IntSize;
|
||||
|
||||
pub struct HTMLImage {
|
||||
pub dom_element: web_sys::HtmlImageElement,
|
||||
/// If present, this boolean property indicates whether the image has been uploaded yet or
|
||||
/// if that operation is still pending. If not present, then the image *is* available. This is
|
||||
/// used for remote HTML image loading and the property will be used to correctly track dependencies
|
||||
/// to graphics items that query for the size.
|
||||
image_load_pending: core::pin::Pin<Rc<Property<bool>>>,
|
||||
}
|
||||
|
||||
impl HTMLImage {
|
||||
pub fn new(url: &str) -> Self {
|
||||
let dom_element = web_sys::HtmlImageElement::new().unwrap();
|
||||
|
||||
let image_load_pending = Rc::pin(Property::new(true));
|
||||
|
||||
dom_element.set_cross_origin(Some("anonymous"));
|
||||
dom_element.set_onload(Some(
|
||||
&wasm_bindgen::closure::Closure::once_into_js({
|
||||
let image_load_pending = image_load_pending.clone();
|
||||
move || {
|
||||
image_load_pending.as_ref().set(false);
|
||||
|
||||
// As you can paint on a HTML canvas at any point in time, request_redraw()
|
||||
// on a winit window only queues an additional internal event, that'll be
|
||||
// be dispatched as the next event. We are however not in an event loop
|
||||
// call, so we also need to wake up the event loop and redraw then.
|
||||
let _ = crate::api::invoke_from_event_loop(|| {});
|
||||
}
|
||||
})
|
||||
.into(),
|
||||
));
|
||||
dom_element.set_src(&url);
|
||||
|
||||
Self { dom_element, image_load_pending }
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Option<IntSize> {
|
||||
match self.image_load_pending.as_ref().get() {
|
||||
true => None,
|
||||
false => Some(IntSize::new(
|
||||
self.dom_element.natural_width(),
|
||||
self.dom_element.natural_height(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn source(&self) -> alloc::string::String {
|
||||
self.dom_element.src()
|
||||
}
|
||||
|
||||
pub fn is_svg(&self) -> bool {
|
||||
self.dom_element.current_src().ends_with(".svg")
|
||||
}
|
||||
}
|
||||
|
||||
impl super::OpaqueImage for HTMLImage {
|
||||
fn size(&self) -> IntSize {
|
||||
self.size().unwrap_or_default()
|
||||
}
|
||||
fn cache_key(&self) -> ImageCacheKey {
|
||||
ImageCacheKey::URL(self.source().into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use super::{ImageCacheKey, SharedImageBuffer, SharedPixelBuffer};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::SharedString;
|
||||
use crate::lengths::PhysicalPx;
|
||||
use resvg::{tiny_skia, usvg};
|
||||
|
||||
pub struct ParsedSVG {
|
||||
svg_tree: usvg::Tree,
|
||||
cache_key: ImageCacheKey,
|
||||
}
|
||||
|
||||
impl super::OpaqueImage for ParsedSVG {
|
||||
fn size(&self) -> crate::graphics::IntSize {
|
||||
self.size()
|
||||
}
|
||||
fn cache_key(&self) -> ImageCacheKey {
|
||||
self.cache_key.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for ParsedSVG {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_tuple("ParsedSVG").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParsedSVG {
|
||||
pub fn size(&self) -> crate::graphics::IntSize {
|
||||
let size = self.svg_tree.size().to_int_size();
|
||||
[size.width(), size.height()].into()
|
||||
}
|
||||
|
||||
pub fn cache_key(&self) -> ImageCacheKey {
|
||||
self.cache_key.clone()
|
||||
}
|
||||
|
||||
/// Renders the SVG with the specified size, if no size is specified, get the size from the image
|
||||
#[allow(clippy::unnecessary_cast)] // Coord
|
||||
pub fn render(
|
||||
&self,
|
||||
size: Option<euclid::Size2D<u32, PhysicalPx>>,
|
||||
) -> Result<SharedImageBuffer, usvg::Error> {
|
||||
let tree = &self.svg_tree;
|
||||
|
||||
let (target_size, transform) = match size {
|
||||
Some(size) => {
|
||||
let target_size = tiny_skia::IntSize::from_wh(size.width, size.height)
|
||||
.ok_or(usvg::Error::InvalidSize)?;
|
||||
let target_size = tree.size().to_int_size().scale_to(target_size);
|
||||
let target_size_f = target_size.to_size();
|
||||
|
||||
let transform = tiny_skia::Transform::from_scale(
|
||||
target_size_f.width() as f32 / tree.size().width() as f32,
|
||||
target_size_f.height() as f32 / tree.size().height() as f32,
|
||||
);
|
||||
(target_size, transform)
|
||||
}
|
||||
None => (tree.size().to_int_size(), tiny_skia::Transform::default()),
|
||||
};
|
||||
|
||||
let mut buffer = SharedPixelBuffer::new(target_size.width(), target_size.height());
|
||||
let mut skia_buffer = tiny_skia::PixmapMut::from_bytes(
|
||||
buffer.make_mut_bytes(),
|
||||
target_size.width(),
|
||||
target_size.height(),
|
||||
)
|
||||
.ok_or(usvg::Error::InvalidSize)?;
|
||||
|
||||
resvg::render(tree, transform, &mut skia_buffer);
|
||||
Ok(SharedImageBuffer::RGBA8Premultiplied(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn load_from_path(
|
||||
path: &SharedString,
|
||||
cache_key: ImageCacheKey,
|
||||
) -> Result<ParsedSVG, std::io::Error> {
|
||||
let svg_data = std::fs::read(std::path::Path::new(&path.as_str()))?;
|
||||
|
||||
let option = usvg::Options::default();
|
||||
usvg::Tree::from_data(&svg_data, &option)
|
||||
.map(|svg| ParsedSVG { svg_tree: svg, cache_key })
|
||||
.map_err(std::io::Error::other)
|
||||
}
|
||||
|
||||
pub fn load_from_data(slice: &[u8], cache_key: ImageCacheKey) -> Result<ParsedSVG, usvg::Error> {
|
||||
let option = usvg::Options::default();
|
||||
usvg::Tree::from_data(slice, &option).map(|svg| ParsedSVG { svg_tree: svg, cache_key })
|
||||
}
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains path related types and functions for the run-time library.
|
||||
*/
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use crate::debug_log;
|
||||
use crate::items::{ImageFit, PathEvent};
|
||||
#[cfg(feature = "rtti")]
|
||||
use crate::rtti::*;
|
||||
use auto_enums::auto_enum;
|
||||
use const_field_offset::FieldOffsets;
|
||||
use i_slint_core_macros::*;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
|
||||
#[pin]
|
||||
/// PathMoveTo describes the event of setting the cursor on the path to use as starting
|
||||
/// point for sub-sequent events, such as `LineTo`. Moving the cursor also implicitly closes
|
||||
/// sub-paths and therefore beings a new sub-path.
|
||||
pub struct PathMoveTo {
|
||||
#[rtti_field]
|
||||
/// The x coordinate where the current position should be.
|
||||
pub x: f32,
|
||||
#[rtti_field]
|
||||
/// The y coordinate where the current position should be.
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
|
||||
#[pin]
|
||||
/// PathLineTo describes the event of moving the cursor on the path to the specified location
|
||||
/// along a straight line.
|
||||
pub struct PathLineTo {
|
||||
#[rtti_field]
|
||||
/// The x coordinate where the line should go to.
|
||||
pub x: f32,
|
||||
#[rtti_field]
|
||||
/// The y coordinate where the line should go to.
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
|
||||
#[pin]
|
||||
/// PathArcTo describes the event of moving the cursor on the path across an arc to the specified
|
||||
/// x/y coordinates, with the specified x/y radius and additional properties.
|
||||
pub struct PathArcTo {
|
||||
#[rtti_field]
|
||||
/// The x coordinate where the arc should end up.
|
||||
pub x: f32,
|
||||
#[rtti_field]
|
||||
/// The y coordinate where the arc should end up.
|
||||
pub y: f32,
|
||||
#[rtti_field]
|
||||
/// The radius on the x-axis of the arc.
|
||||
pub radius_x: f32,
|
||||
#[rtti_field]
|
||||
/// The radius on the y-axis of the arc.
|
||||
pub radius_y: f32,
|
||||
#[rtti_field]
|
||||
/// The rotation along the x-axis of the arc in degrees.
|
||||
pub x_rotation: f32,
|
||||
#[rtti_field]
|
||||
/// large_arc indicates whether to take the long or the shorter path to complete the arc.
|
||||
pub large_arc: bool,
|
||||
#[rtti_field]
|
||||
/// sweep indicates the direction of the arc. If true, a clockwise direction is chosen,
|
||||
/// otherwise counter-clockwise.
|
||||
pub sweep: bool,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
|
||||
#[pin]
|
||||
/// PathCubicTo describes a smooth Bézier curve from the path's current position
|
||||
/// to the specified x/y location, using two control points.
|
||||
pub struct PathCubicTo {
|
||||
#[rtti_field]
|
||||
/// The x coordinate of the curve's end point.
|
||||
pub x: f32,
|
||||
#[rtti_field]
|
||||
/// The y coordinate of the curve's end point.
|
||||
pub y: f32,
|
||||
#[rtti_field]
|
||||
/// The x coordinate of the curve's first control point.
|
||||
pub control_1_x: f32,
|
||||
#[rtti_field]
|
||||
/// The y coordinate of the curve's first control point.
|
||||
pub control_1_y: f32,
|
||||
#[rtti_field]
|
||||
/// The x coordinate of the curve's second control point.
|
||||
pub control_2_x: f32,
|
||||
#[rtti_field]
|
||||
/// The y coordinate of the curve's second control point.
|
||||
pub control_2_y: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement, Clone, Debug, PartialEq)]
|
||||
#[pin]
|
||||
/// PathCubicTo describes a smooth Bézier curve from the path's current position
|
||||
/// to the specified x/y location, using one control points.
|
||||
pub struct PathQuadraticTo {
|
||||
#[rtti_field]
|
||||
/// The x coordinate of the curve's end point.
|
||||
pub x: f32,
|
||||
#[rtti_field]
|
||||
/// The y coordinate of the curve's end point.
|
||||
pub y: f32,
|
||||
#[rtti_field]
|
||||
/// The x coordinate of the curve's control point.
|
||||
pub control_x: f32,
|
||||
#[rtti_field]
|
||||
/// The y coordinate of the curve's control point.
|
||||
pub control_y: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, derive_more::From)]
|
||||
/// PathElement describes a single element on a path, such as move-to, line-to, etc.
|
||||
pub enum PathElement {
|
||||
/// The MoveTo variant sets the current position on the path.
|
||||
MoveTo(PathMoveTo),
|
||||
/// The LineTo variant describes a line.
|
||||
LineTo(PathLineTo),
|
||||
/// The PathArcTo variant describes an arc.
|
||||
ArcTo(PathArcTo),
|
||||
/// The CubicTo variant describes a Bézier curve with two control points.
|
||||
CubicTo(PathCubicTo),
|
||||
/// The QuadraticTo variant describes a Bézier curve with one control point.
|
||||
QuadraticTo(PathQuadraticTo),
|
||||
/// Indicates that the path should be closed now by connecting to the starting point.
|
||||
Close,
|
||||
}
|
||||
|
||||
struct ToLyonPathEventIterator<'a> {
|
||||
events_it: core::slice::Iter<'a, PathEvent>,
|
||||
coordinates_it: core::slice::Iter<'a, lyon_path::math::Point>,
|
||||
first: Option<&'a lyon_path::math::Point>,
|
||||
last: Option<&'a lyon_path::math::Point>,
|
||||
}
|
||||
|
||||
impl Iterator for ToLyonPathEventIterator<'_> {
|
||||
type Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use lyon_path::Event;
|
||||
|
||||
self.events_it.next().map(|event| match event {
|
||||
PathEvent::Begin => Event::Begin { at: *self.coordinates_it.next().unwrap() },
|
||||
PathEvent::Line => Event::Line {
|
||||
from: *self.coordinates_it.next().unwrap(),
|
||||
to: *self.coordinates_it.next().unwrap(),
|
||||
},
|
||||
PathEvent::Quadratic => Event::Quadratic {
|
||||
from: *self.coordinates_it.next().unwrap(),
|
||||
ctrl: *self.coordinates_it.next().unwrap(),
|
||||
to: *self.coordinates_it.next().unwrap(),
|
||||
},
|
||||
PathEvent::Cubic => Event::Cubic {
|
||||
from: *self.coordinates_it.next().unwrap(),
|
||||
ctrl1: *self.coordinates_it.next().unwrap(),
|
||||
ctrl2: *self.coordinates_it.next().unwrap(),
|
||||
to: *self.coordinates_it.next().unwrap(),
|
||||
},
|
||||
PathEvent::EndOpen => {
|
||||
Event::End { first: *self.first.unwrap(), last: *self.last.unwrap(), close: false }
|
||||
}
|
||||
PathEvent::EndClosed => {
|
||||
Event::End { first: *self.first.unwrap(), last: *self.last.unwrap(), close: true }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.events_it.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for ToLyonPathEventIterator<'_> {}
|
||||
|
||||
struct TransformedLyonPathIterator<EventIt> {
|
||||
it: EventIt,
|
||||
transform: lyon_path::math::Transform,
|
||||
}
|
||||
|
||||
impl<EventIt: Iterator<Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>>>
|
||||
Iterator for TransformedLyonPathIterator<EventIt>
|
||||
{
|
||||
type Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.it.next().map(|ev| ev.transformed(&self.transform))
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.it.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<EventIt: Iterator<Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>>>
|
||||
ExactSizeIterator for TransformedLyonPathIterator<EventIt>
|
||||
{
|
||||
}
|
||||
|
||||
/// PathDataIterator is a data structure that acts as starting point for iterating
|
||||
/// through the low-level events of a path. If the path was constructed from said
|
||||
/// events, then it is a very thin abstraction. If the path was created from higher-level
|
||||
/// elements, then an intermediate lyon path is required/built.
|
||||
pub struct PathDataIterator {
|
||||
it: LyonPathIteratorVariant,
|
||||
transform: lyon_path::math::Transform,
|
||||
}
|
||||
|
||||
enum LyonPathIteratorVariant {
|
||||
FromPath(lyon_path::Path),
|
||||
FromEvents(crate::SharedVector<PathEvent>, crate::SharedVector<lyon_path::math::Point>),
|
||||
}
|
||||
|
||||
impl PathDataIterator {
|
||||
/// Create a new iterator for path traversal.
|
||||
#[auto_enum(Iterator)]
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = lyon_path::Event<lyon_path::math::Point, lyon_path::math::Point>> + '_
|
||||
{
|
||||
match &self.it {
|
||||
LyonPathIteratorVariant::FromPath(path) => {
|
||||
TransformedLyonPathIterator { it: path.iter(), transform: self.transform }
|
||||
}
|
||||
LyonPathIteratorVariant::FromEvents(events, coordinates) => {
|
||||
TransformedLyonPathIterator {
|
||||
it: ToLyonPathEventIterator {
|
||||
events_it: events.iter(),
|
||||
coordinates_it: coordinates.iter(),
|
||||
first: coordinates.first(),
|
||||
last: coordinates.last(),
|
||||
},
|
||||
transform: self.transform,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a transformation on the elements this iterator provides that tries to fit everything
|
||||
/// into the specified width/height, respecting the provided viewbox. If no viewbox is specified,
|
||||
/// the bounding rectangle of the path is used.
|
||||
pub fn fit(
|
||||
&mut self,
|
||||
width: f32,
|
||||
height: f32,
|
||||
viewbox: Option<lyon_path::math::Box2D>,
|
||||
style: ImageFit,
|
||||
) {
|
||||
if width > 0. || height > 0. {
|
||||
let fit_style = match style {
|
||||
ImageFit::Contain => lyon_algorithms::fit::FitStyle::Min,
|
||||
ImageFit::Cover => lyon_algorithms::fit::FitStyle::Max,
|
||||
ImageFit::Fill => lyon_algorithms::fit::FitStyle::Stretch,
|
||||
ImageFit::Preserve => return,
|
||||
};
|
||||
let viewbox =
|
||||
viewbox.unwrap_or_else(|| lyon_algorithms::aabb::bounding_box(self.iter()));
|
||||
self.transform = lyon_algorithms::fit::fit_box(
|
||||
&viewbox,
|
||||
&lyon_path::math::Box2D::from_size(lyon_path::math::Size::new(width, height)),
|
||||
fit_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
/// PathData represents a path described by either high-level elements or low-level
|
||||
/// events and coordinates.
|
||||
#[derive(Default)]
|
||||
pub enum PathData {
|
||||
/// None is the variant when the path is empty.
|
||||
#[default]
|
||||
None,
|
||||
/// The Elements variant is used to make a Path from shared arrays of elements.
|
||||
Elements(crate::SharedVector<PathElement>),
|
||||
/// The Events variant describes the path as a series of low-level events and
|
||||
/// associated coordinates.
|
||||
Events(crate::SharedVector<PathEvent>, crate::SharedVector<lyon_path::math::Point>),
|
||||
/// The Commands variant describes the path as a series of SVG encoded path commands.
|
||||
#[cfg(feature = "std")]
|
||||
Commands(crate::SharedString),
|
||||
}
|
||||
|
||||
impl PathData {
|
||||
/// This function returns an iterator that allows traversing the path by means of lyon events.
|
||||
pub fn iter(self) -> Option<PathDataIterator> {
|
||||
PathDataIterator {
|
||||
it: match self {
|
||||
PathData::None => return None,
|
||||
PathData::Elements(elements) => LyonPathIteratorVariant::FromPath(
|
||||
PathData::build_path(elements.as_slice().iter()),
|
||||
),
|
||||
PathData::Events(events, coordinates) => {
|
||||
LyonPathIteratorVariant::FromEvents(events, coordinates)
|
||||
}
|
||||
#[cfg(feature = "std")]
|
||||
PathData::Commands(commands) => {
|
||||
let mut builder = lyon_path::Path::builder();
|
||||
let mut parser = lyon_extra::parser::PathParser::new();
|
||||
match parser.parse(
|
||||
&lyon_extra::parser::ParserOptions::DEFAULT,
|
||||
&mut lyon_extra::parser::Source::new(commands.chars()),
|
||||
&mut builder,
|
||||
) {
|
||||
Ok(()) => LyonPathIteratorVariant::FromPath(builder.build()),
|
||||
Err(e) => {
|
||||
debug_log!("Error while parsing path commands '{commands}': {e:?}");
|
||||
LyonPathIteratorVariant::FromPath(Default::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
transform: Default::default(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn build_path(element_it: core::slice::Iter<PathElement>) -> lyon_path::Path {
|
||||
use lyon_geom::SvgArc;
|
||||
use lyon_path::ArcFlags;
|
||||
use lyon_path::math::{Angle, Point, Vector};
|
||||
use lyon_path::traits::SvgPathBuilder;
|
||||
|
||||
let mut path_builder = lyon_path::Path::builder().with_svg();
|
||||
for element in element_it {
|
||||
match element {
|
||||
PathElement::MoveTo(PathMoveTo { x, y }) => {
|
||||
path_builder.move_to(Point::new(*x, *y));
|
||||
}
|
||||
PathElement::LineTo(PathLineTo { x, y }) => {
|
||||
path_builder.line_to(Point::new(*x, *y));
|
||||
}
|
||||
PathElement::ArcTo(PathArcTo {
|
||||
x,
|
||||
y,
|
||||
radius_x,
|
||||
radius_y,
|
||||
x_rotation,
|
||||
large_arc,
|
||||
sweep,
|
||||
}) => {
|
||||
let radii = Vector::new(*radius_x, *radius_y);
|
||||
let x_rotation = Angle::degrees(*x_rotation);
|
||||
let flags = ArcFlags { large_arc: *large_arc, sweep: *sweep };
|
||||
let to = Point::new(*x, *y);
|
||||
|
||||
let svg_arc = SvgArc {
|
||||
from: path_builder.current_position(),
|
||||
radii,
|
||||
x_rotation,
|
||||
flags,
|
||||
to,
|
||||
};
|
||||
|
||||
if svg_arc.is_straight_line() {
|
||||
path_builder.line_to(to);
|
||||
} else {
|
||||
path_builder.arc_to(radii, x_rotation, flags, to)
|
||||
}
|
||||
}
|
||||
PathElement::CubicTo(PathCubicTo {
|
||||
x,
|
||||
y,
|
||||
control_1_x,
|
||||
control_1_y,
|
||||
control_2_x,
|
||||
control_2_y,
|
||||
}) => {
|
||||
path_builder.cubic_bezier_to(
|
||||
Point::new(*control_1_x, *control_1_y),
|
||||
Point::new(*control_2_x, *control_2_y),
|
||||
Point::new(*x, *y),
|
||||
);
|
||||
}
|
||||
PathElement::QuadraticTo(PathQuadraticTo { x, y, control_x, control_y }) => {
|
||||
path_builder.quadratic_bezier_to(
|
||||
Point::new(*control_x, *control_y),
|
||||
Point::new(*x, *y),
|
||||
);
|
||||
}
|
||||
PathElement::Close => path_builder.close(),
|
||||
}
|
||||
}
|
||||
|
||||
path_builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use super::super::*;
|
||||
use super::*;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
type c_void = ();
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// This function is used for the low-level C++ interface to allocate the backing vector for a shared path element array.
|
||||
pub unsafe extern "C" fn slint_new_path_elements(
|
||||
out: *mut c_void,
|
||||
first_element: *const PathElement,
|
||||
count: usize,
|
||||
) {
|
||||
let arr =
|
||||
crate::SharedVector::from(unsafe { core::slice::from_raw_parts(first_element, count) });
|
||||
unsafe { core::ptr::write(out as *mut crate::SharedVector<PathElement>, arr) };
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// This function is used for the low-level C++ interface to allocate the backing vector for a shared path event array.
|
||||
pub unsafe extern "C" fn slint_new_path_events(
|
||||
out_events: *mut c_void,
|
||||
out_coordinates: *mut c_void,
|
||||
first_event: *const PathEvent,
|
||||
event_count: usize,
|
||||
first_coordinate: *const Point,
|
||||
coordinate_count: usize,
|
||||
) {
|
||||
let events = crate::SharedVector::from(unsafe {
|
||||
core::slice::from_raw_parts(first_event, event_count)
|
||||
});
|
||||
unsafe { core::ptr::write(out_events as *mut crate::SharedVector<PathEvent>, events) };
|
||||
let coordinates = crate::SharedVector::from(unsafe {
|
||||
core::slice::from_raw_parts(first_coordinate, coordinate_count)
|
||||
});
|
||||
unsafe {
|
||||
core::ptr::write(out_coordinates as *mut crate::SharedVector<Point>, coordinates)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains a simple helper type to measure the average number of frames rendered per second.
|
||||
*/
|
||||
|
||||
use crate::animations::Instant;
|
||||
use crate::debug_log;
|
||||
use crate::timers::{Timer, TimerMode};
|
||||
use alloc::format;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::cell::RefCell;
|
||||
|
||||
/// The method in which we refresh the window
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RefreshMode {
|
||||
/// render only when necessary (default)
|
||||
Lazy,
|
||||
/// continuously render to the screen
|
||||
FullSpeed,
|
||||
}
|
||||
|
||||
/// This struct is filled/provided by the ItemRenderer to return collected metrics
|
||||
/// during the rendering of the scene.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct RenderingMetrics {
|
||||
/// The number of layers that were created. None if the renderer does not create layers.
|
||||
pub layers_created: Option<usize>,
|
||||
|
||||
/// dirty_region
|
||||
pub dirty_region: Option<crate::partial_renderer::DirtyRegion>,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RenderingMetrics {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
if let Some(layer_count) = self.layers_created {
|
||||
write!(f, "[{layer_count} layers created]")?
|
||||
}
|
||||
if let Some(dirty_region) = &self.dirty_region {
|
||||
write!(f, "(dirty: {dirty_region:?})")?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct FrameData {
|
||||
timestamp: Instant,
|
||||
metrics: RenderingMetrics,
|
||||
}
|
||||
|
||||
/// Helper class that rendering backends can use to provide an FPS counter
|
||||
pub struct RenderingMetricsCollector {
|
||||
collected_frame_data_since_second_ago: RefCell<Vec<FrameData>>,
|
||||
update_timer: Timer,
|
||||
refresh_mode: RefreshMode,
|
||||
output_console: bool,
|
||||
output_overlay: bool,
|
||||
}
|
||||
|
||||
impl RenderingMetricsCollector {
|
||||
/// Returns a new instance of the counter if requested by the user (via `SLINT_DEBUG_PERFORMANCE` environment variable).
|
||||
/// The environment variable holds a comma separated list of options:
|
||||
/// * `refresh_lazy`: selects the lazy refresh mode, where measurements are only taken when a frame is rendered (due to user input or animations)
|
||||
/// * `refresh_full_speed`: frames are continuously rendered
|
||||
/// * `console`: the measurement is printed to the console
|
||||
/// * `overlay`: the measurement is drawn as overlay on top of the scene
|
||||
///
|
||||
/// If enabled, this will also print out some system information such as whether
|
||||
/// this is a debug or release build, as well as the provided winsys_info string.
|
||||
pub fn new(winsys_info: &str) -> Option<Rc<Self>> {
|
||||
#[cfg(feature = "std")]
|
||||
let options = std::env::var("SLINT_DEBUG_PERFORMANCE").ok()?;
|
||||
#[cfg(not(feature = "std"))]
|
||||
let options = option_env!("SLINT_DEBUG_PERFORMANCE")?;
|
||||
let mut output_console = false;
|
||||
let mut output_overlay = false;
|
||||
let mut refresh_mode = None;
|
||||
for option in options.split(',') {
|
||||
match option {
|
||||
"console" => output_console = true,
|
||||
"overlay" => output_overlay = true,
|
||||
"refresh_lazy" => refresh_mode = Some(RefreshMode::Lazy),
|
||||
"refresh_full_speed" => refresh_mode = Some(RefreshMode::FullSpeed),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(refresh_mode) = refresh_mode else {
|
||||
debug_log!(
|
||||
"Missing refresh mode in SLINT_DEBUG_PERFORMANCE. Please specify either refresh_full_speed or refresh_lazy"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
if !output_console && !output_overlay {
|
||||
debug_log!(
|
||||
"Missing output mode in SLINT_DEBUG_PERFORMANCE. Please specify either console or overlay (or both)"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let collector = Rc::new(Self {
|
||||
collected_frame_data_since_second_ago: Default::default(),
|
||||
update_timer: Default::default(),
|
||||
refresh_mode,
|
||||
output_console,
|
||||
output_overlay,
|
||||
});
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let build_config = "debug";
|
||||
#[cfg(not(debug_assertions))]
|
||||
let build_config = "release";
|
||||
|
||||
debug_log!("Slint: Build config: {}; Backend: {}", build_config, winsys_info);
|
||||
|
||||
let self_weak = Rc::downgrade(&collector);
|
||||
collector.update_timer.stop();
|
||||
collector.update_timer.start(
|
||||
TimerMode::Repeated,
|
||||
core::time::Duration::from_secs(1),
|
||||
move || {
|
||||
let this = match self_weak.upgrade() {
|
||||
Some(this) => this,
|
||||
None => return,
|
||||
};
|
||||
this.trim_frame_data_to_second_boundary();
|
||||
|
||||
let mut last_frame_details = String::new();
|
||||
if let Some(last_frame_data) =
|
||||
this.collected_frame_data_since_second_ago.borrow().last()
|
||||
{
|
||||
use core::fmt::Write;
|
||||
if write!(&mut last_frame_details, "{}", last_frame_data.metrics).is_ok()
|
||||
&& !last_frame_details.is_empty()
|
||||
{
|
||||
last_frame_details.insert_str(0, "details from last frame: ");
|
||||
}
|
||||
}
|
||||
|
||||
if this.output_console {
|
||||
debug_log!(
|
||||
"average frames per second: {} {}",
|
||||
this.collected_frame_data_since_second_ago.borrow().len(),
|
||||
last_frame_details
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Some(collector)
|
||||
}
|
||||
|
||||
fn trim_frame_data_to_second_boundary(self: &Rc<Self>) {
|
||||
let mut frame_times = self.collected_frame_data_since_second_ago.borrow_mut();
|
||||
let now = Instant::now();
|
||||
frame_times.retain(|frame| {
|
||||
now.duration_since(frame.timestamp) <= core::time::Duration::from_secs(1)
|
||||
});
|
||||
}
|
||||
|
||||
/// Call this function every time you've completed the rendering of a frame. The `renderer` parameter
|
||||
/// is used to collect additional data and is used to render an overlay if enabled.
|
||||
pub fn measure_frame_rendered(
|
||||
self: &Rc<Self>,
|
||||
renderer: &mut dyn crate::item_rendering::ItemRenderer,
|
||||
metrics: RenderingMetrics,
|
||||
) {
|
||||
self.collected_frame_data_since_second_ago
|
||||
.borrow_mut()
|
||||
.push(FrameData { timestamp: Instant::now(), metrics });
|
||||
if matches!(self.refresh_mode, RefreshMode::FullSpeed) {
|
||||
crate::animations::CURRENT_ANIMATION_DRIVER
|
||||
.with(|driver| driver.set_has_active_animations());
|
||||
}
|
||||
self.trim_frame_data_to_second_boundary();
|
||||
|
||||
if self.output_overlay {
|
||||
renderer.draw_string(
|
||||
&format!("FPS: {}", self.collected_frame_data_since_second_ago.borrow().len()),
|
||||
crate::Color::from_rgb_u8(0, 128, 128),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Always render the full screen.
|
||||
pub fn refresh_mode(&self) -> RefreshMode {
|
||||
self.refresh_mode
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
/*!
|
||||
This module contains types that are public and re-exported in the slint-rs as well as the slint-interpreter crate as public API,
|
||||
in particular the `BackendSelector` type, to configure the WGPU-based renderer(s).
|
||||
*/
|
||||
|
||||
use alloc::boxed::Box;
|
||||
|
||||
pub use wgpu_27 as wgpu;
|
||||
|
||||
#[cfg(feature = "unstable-wgpu-27")]
|
||||
pub mod api {
|
||||
/*!
|
||||
This module contains types that are public and re-exported in the slint-rs as well as the slint-interpreter crate as public API.
|
||||
*/
|
||||
|
||||
pub use super::wgpu;
|
||||
|
||||
/// This data structure provides settings for initializing WGPU renderers.
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct WGPUSettings {
|
||||
/// The backends to use for the WGPU instance.
|
||||
pub backends: wgpu_27::Backends,
|
||||
/// The different options that are given to the selected backends.
|
||||
pub backend_options: wgpu_27::BackendOptions,
|
||||
/// The flags to fine-tune behaviour of the WGPU instance.
|
||||
pub instance_flags: wgpu_27::InstanceFlags,
|
||||
/// Memory budget thresholds used by some backends.
|
||||
pub instance_memory_budget_thresholds: wgpu_27::MemoryBudgetThresholds,
|
||||
|
||||
/// The power preference is used to influence the WGPU adapter selection.
|
||||
pub power_preference: wgpu_27::PowerPreference,
|
||||
|
||||
/// The label for the device. This is used to identify the device in debugging tools.
|
||||
pub device_label: Option<std::borrow::Cow<'static, str>>,
|
||||
/// The required features for the device.
|
||||
pub device_required_features: wgpu_27::Features,
|
||||
/// The required limits for the device.
|
||||
pub device_required_limits: wgpu_27::Limits,
|
||||
/// The experimental features for the device.
|
||||
pub device_experimental_features: wgpu_27::ExperimentalFeatures,
|
||||
/// The memory hints for the device.
|
||||
pub device_memory_hints: wgpu_27::MemoryHints,
|
||||
}
|
||||
|
||||
impl Default for WGPUSettings {
|
||||
fn default() -> Self {
|
||||
let backends = wgpu_27::Backends::from_env().unwrap_or_default();
|
||||
let dx12_shader_compiler = wgpu_27::Dx12Compiler::from_env().unwrap_or_default();
|
||||
let dx12_presentation_system =
|
||||
wgpu::wgt::Dx12SwapchainKind::from_env().unwrap_or_default();
|
||||
let dx12_latency_waitable_object =
|
||||
wgpu::wgt::Dx12UseFrameLatencyWaitableObject::from_env().unwrap_or_default();
|
||||
let gles_minor_version = wgpu_27::Gles3MinorVersion::from_env().unwrap_or_default();
|
||||
|
||||
Self {
|
||||
backends,
|
||||
backend_options: wgpu_27::BackendOptions {
|
||||
dx12: wgpu_27::Dx12BackendOptions {
|
||||
shader_compiler: dx12_shader_compiler,
|
||||
presentation_system: dx12_presentation_system,
|
||||
latency_waitable_object: dx12_latency_waitable_object,
|
||||
},
|
||||
gl: wgpu_27::GlBackendOptions {
|
||||
gles_minor_version,
|
||||
fence_behavior: wgpu_27::GlFenceBehavior::default(),
|
||||
},
|
||||
noop: wgpu::NoopBackendOptions::default(),
|
||||
},
|
||||
instance_flags: wgpu_27::InstanceFlags::from_build_config().with_env(),
|
||||
instance_memory_budget_thresholds: wgpu_27::MemoryBudgetThresholds::default(),
|
||||
|
||||
power_preference: wgpu_27::PowerPreference::from_env().unwrap_or_default(),
|
||||
|
||||
device_label: None,
|
||||
device_required_features: wgpu_27::Features::empty(),
|
||||
device_required_limits: wgpu_27::Limits::downlevel_webgl2_defaults(),
|
||||
device_experimental_features: wgpu_27::ExperimentalFeatures::disabled(),
|
||||
device_memory_hints: wgpu_27::MemoryHints::MemoryUsage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum describes the different ways to configure WGPU for rendering.
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum WGPUConfiguration {
|
||||
/// Use `Manual` if you've initialized WGPU and want to supply the instance, adapter,
|
||||
/// device, and queue for use.
|
||||
Manual {
|
||||
/// The WGPU instance to use.
|
||||
instance: wgpu_27::Instance,
|
||||
/// The WGPU adapter to use.
|
||||
adapter: wgpu_27::Adapter,
|
||||
/// The WGPU device to use.
|
||||
device: wgpu_27::Device,
|
||||
/// The WGPU queue to use.
|
||||
queue: wgpu_27::Queue,
|
||||
},
|
||||
/// Use `Automatic` if you want to let Slint select the WGPU instance, adapter, and
|
||||
/// device, but fine-tune aspects such as memory limits or features.
|
||||
Automatic(WGPUSettings),
|
||||
}
|
||||
|
||||
impl Default for WGPUConfiguration {
|
||||
fn default() -> Self {
|
||||
Self::Automatic(WGPUSettings::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<wgpu_27::Texture> for super::super::Image {
|
||||
type Error = TextureImportError;
|
||||
|
||||
fn try_from(texture: wgpu_27::Texture) -> Result<Self, Self::Error> {
|
||||
if texture.format() != wgpu_27::TextureFormat::Rgba8Unorm
|
||||
&& texture.format() != wgpu_27::TextureFormat::Rgba8UnormSrgb
|
||||
{
|
||||
return Err(Self::Error::InvalidFormat);
|
||||
}
|
||||
let usages = texture.usage();
|
||||
if !usages.contains(wgpu_27::TextureUsages::TEXTURE_BINDING)
|
||||
|| !usages.contains(wgpu_27::TextureUsages::RENDER_ATTACHMENT)
|
||||
{
|
||||
return Err(Self::Error::InvalidUsage);
|
||||
}
|
||||
Ok(Self(super::super::ImageInner::WGPUTexture(
|
||||
super::super::WGPUTexture::WGPU27Texture(texture),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, derive_more::Error)]
|
||||
#[non_exhaustive]
|
||||
/// This enum describes the possible errors that can occur when importing a WGPU texture,
|
||||
/// via [`Image::try_from()`](super::super::Image::try_from()).
|
||||
pub enum TextureImportError {
|
||||
/// The texture format is not supported. The only supported format is Rgba8Unorm and Rgba8UnormSrgb.
|
||||
InvalidFormat,
|
||||
/// The texture usage must include TEXTURE_BINDING as well as RENDER_ATTACHMENT.
|
||||
InvalidUsage,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for TextureImportError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
TextureImportError::InvalidFormat => f.write_str(
|
||||
"The texture format is not supported. The only supported format is Rgba8Unorm and Rgba8UnormSrgb",
|
||||
),
|
||||
TextureImportError::InvalidUsage => f.write_str(
|
||||
"The texture usage must include TEXTURE_BINDING as well as RENDER_ATTACHMENT",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use super::RequestedGraphicsAPI;
|
||||
|
||||
/// Internal helper function see if there are any GPU adapters for hardware accelerated rendering.
|
||||
/// This is used to determine if we should fall back to software rendering (instead of using WGPU
|
||||
/// software rendering, such as DX12's Warp adapter)
|
||||
pub fn any_wgpu27_adapters_with_gpu(requested_graphics_api: Option<RequestedGraphicsAPI>) -> bool {
|
||||
let allow_cpu = std::env::var("SLINT_WGPU_CPU").is_ok();
|
||||
if allow_cpu {
|
||||
return true;
|
||||
}
|
||||
let (instance, backends) = match requested_graphics_api {
|
||||
#[cfg(feature = "unstable-wgpu-27")]
|
||||
Some(RequestedGraphicsAPI::WGPU27(api::WGPUConfiguration::Manual { instance, .. })) => {
|
||||
(instance, wgpu::Backends::all())
|
||||
}
|
||||
#[cfg(feature = "unstable-wgpu-27")]
|
||||
Some(RequestedGraphicsAPI::WGPU27(api::WGPUConfiguration::Automatic(wgpu27_settings))) => {
|
||||
if cfg!(target_family = "wasm") {
|
||||
return true;
|
||||
}
|
||||
(
|
||||
wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu27_settings.backends,
|
||||
flags: wgpu27_settings.instance_flags,
|
||||
backend_options: wgpu27_settings.backend_options,
|
||||
memory_budget_thresholds: wgpu27_settings.instance_memory_budget_thresholds,
|
||||
}),
|
||||
wgpu27_settings.backends,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
if cfg!(target_family = "wasm") {
|
||||
return true;
|
||||
}
|
||||
|
||||
let backends = wgpu::Backends::from_env().unwrap_or_default();
|
||||
let dx12_shader_compiler = wgpu::Dx12Compiler::from_env().unwrap_or_default();
|
||||
let dx12_presentation_system =
|
||||
wgpu::wgt::Dx12SwapchainKind::from_env().unwrap_or_default();
|
||||
let dx12_latency_waitable_object =
|
||||
wgpu::wgt::Dx12UseFrameLatencyWaitableObject::from_env().unwrap_or_default();
|
||||
let gles_minor_version = wgpu::Gles3MinorVersion::from_env().unwrap_or_default();
|
||||
|
||||
(
|
||||
wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends,
|
||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||
backend_options: wgpu::BackendOptions {
|
||||
dx12: wgpu::Dx12BackendOptions {
|
||||
shader_compiler: dx12_shader_compiler,
|
||||
presentation_system: dx12_presentation_system,
|
||||
latency_waitable_object: dx12_latency_waitable_object,
|
||||
},
|
||||
gl: wgpu::GlBackendOptions {
|
||||
gles_minor_version,
|
||||
fence_behavior: wgpu::GlFenceBehavior::default(),
|
||||
},
|
||||
noop: wgpu::NoopBackendOptions::default(),
|
||||
},
|
||||
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||
}),
|
||||
backends,
|
||||
)
|
||||
}
|
||||
Some(_) => return false,
|
||||
};
|
||||
instance
|
||||
.enumerate_adapters(backends)
|
||||
.into_iter()
|
||||
.any(|adapter| adapter.get_info().device_type != wgpu::DeviceType::Cpu)
|
||||
}
|
||||
|
||||
/// Enum to represent different surface targets for WGPU initialization.
|
||||
pub enum SurfaceTarget {
|
||||
/// Standard window handle for windowed rendering.
|
||||
WindowHandle(Box<dyn wgpu::WindowHandle + 'static>),
|
||||
/// DRM surface target for direct rendering on Linux KMS.
|
||||
Drm(wgpu::SurfaceTargetUnsafe),
|
||||
}
|
||||
|
||||
impl From<Box<dyn wgpu::WindowHandle + 'static>> for SurfaceTarget {
|
||||
fn from(handle: Box<dyn wgpu::WindowHandle + 'static>) -> Self {
|
||||
Self::WindowHandle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper function to initialize the wgpu instance/adapter/device/queue from either scratch or
|
||||
/// developer-provided config. This is called by any renderer intending to support WGPU.
|
||||
pub fn init_instance_adapter_device_queue_surface(
|
||||
surface_target: impl Into<SurfaceTarget>,
|
||||
requested_graphics_api: Option<RequestedGraphicsAPI>,
|
||||
backends_to_avoid: wgpu::Backends,
|
||||
) -> Result<
|
||||
(
|
||||
wgpu_27::Instance,
|
||||
wgpu_27::Adapter,
|
||||
wgpu_27::Device,
|
||||
wgpu_27::Queue,
|
||||
wgpu_27::Surface<'static>,
|
||||
),
|
||||
Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
> {
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
let surface_target = surface_target.into();
|
||||
|
||||
let create_surface = |instance: &wgpu::Instance| {
|
||||
match surface_target {
|
||||
SurfaceTarget::WindowHandle(window_handle) => instance.create_surface(window_handle),
|
||||
// Safety: The caller ensures the DRM file descriptor in the surface target
|
||||
// remains valid for the lifetime of the returned surface, by storing the
|
||||
// DrmOutput in the renderer adapter.
|
||||
SurfaceTarget::Drm(surface_target_unsafe) => unsafe {
|
||||
instance.create_surface_unsafe(surface_target_unsafe)
|
||||
},
|
||||
}
|
||||
.map_err(|e| {
|
||||
crate::api::PlatformError::from(alloc::format!(
|
||||
"Error creating wgpu window surface: {e}"
|
||||
))
|
||||
})
|
||||
};
|
||||
|
||||
let (instance, adapter, device, queue, surface) = match requested_graphics_api {
|
||||
#[cfg(feature = "unstable-wgpu-27")]
|
||||
Some(RequestedGraphicsAPI::WGPU27(api::WGPUConfiguration::Manual {
|
||||
instance,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
})) => {
|
||||
let surface = create_surface(&instance)?;
|
||||
(instance, adapter, device, queue, surface)
|
||||
}
|
||||
#[cfg(feature = "unstable-wgpu-27")]
|
||||
Some(RequestedGraphicsAPI::WGPU27(api::WGPUConfiguration::Automatic(wgpu27_settings))) => {
|
||||
// wgpu uses async here, but the returned future is ready on first poll on all platforms except WASM,
|
||||
// which we don't support right now.
|
||||
let instance = poll_once(async {
|
||||
wgpu::util::new_instance_with_webgpu_detection(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu27_settings.backends & !backends_to_avoid,
|
||||
flags: wgpu27_settings.instance_flags,
|
||||
backend_options: wgpu27_settings.backend_options,
|
||||
memory_budget_thresholds: wgpu27_settings.instance_memory_budget_thresholds,
|
||||
})
|
||||
.await
|
||||
})
|
||||
.expect("internal error: wgpu instance creation is not expected to be async");
|
||||
|
||||
let surface = create_surface(&instance)?;
|
||||
|
||||
// wgpu uses async here, but the returned future is ready on first poll on all platforms except WASM,
|
||||
// which we don't support right now.
|
||||
let adapter = poll_once(async {
|
||||
match wgpu::util::initialize_adapter_from_env(&instance, Some(&surface)) {
|
||||
Ok(adapter) => Ok(adapter),
|
||||
Err(_) => {
|
||||
instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu27_settings.power_preference,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: Some(&surface),
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
.expect("Failed to find an appropriate adapter")
|
||||
})
|
||||
.expect("internal error: wgpu adapter creation is not expected to be async");
|
||||
|
||||
let (device, queue) = poll_once(async {
|
||||
adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: wgpu27_settings.device_label.as_deref(),
|
||||
required_features: wgpu27_settings.device_required_features,
|
||||
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
|
||||
required_limits: wgpu27_settings
|
||||
.device_required_limits
|
||||
.using_resolution(adapter.limits()),
|
||||
experimental_features: wgpu27_settings.device_experimental_features,
|
||||
memory_hints: wgpu27_settings.device_memory_hints,
|
||||
trace: wgpu::Trace::default(),
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create device")
|
||||
})
|
||||
.expect("internal error: wgpu device creation is not expected to be async");
|
||||
|
||||
(instance, adapter, device, queue, surface)
|
||||
}
|
||||
None => {
|
||||
let backends = wgpu::Backends::from_env().unwrap_or_default() & !backends_to_avoid;
|
||||
let dx12_shader_compiler = wgpu::Dx12Compiler::from_env().unwrap_or_default();
|
||||
let dx12_presentation_system =
|
||||
wgpu::wgt::Dx12SwapchainKind::from_env().unwrap_or_default();
|
||||
let dx12_latency_waitable_object =
|
||||
wgpu::wgt::Dx12UseFrameLatencyWaitableObject::from_env().unwrap_or_default();
|
||||
let gles_minor_version = wgpu::Gles3MinorVersion::from_env().unwrap_or_default();
|
||||
|
||||
// wgpu uses async here, but the returned future is ready on first poll on all platforms except WASM,
|
||||
// which we don't support right now.
|
||||
let instance = poll_once(async {
|
||||
wgpu::util::new_instance_with_webgpu_detection(&wgpu::InstanceDescriptor {
|
||||
backends,
|
||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||
backend_options: wgpu::BackendOptions {
|
||||
dx12: wgpu::Dx12BackendOptions {
|
||||
shader_compiler: dx12_shader_compiler,
|
||||
presentation_system: dx12_presentation_system,
|
||||
latency_waitable_object: dx12_latency_waitable_object,
|
||||
},
|
||||
gl: wgpu::GlBackendOptions {
|
||||
gles_minor_version,
|
||||
fence_behavior: wgpu::GlFenceBehavior::default(),
|
||||
},
|
||||
noop: wgpu::NoopBackendOptions::default(),
|
||||
},
|
||||
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||
})
|
||||
.await
|
||||
})
|
||||
.expect("internal error: wgpu instance creation is not expected to be async");
|
||||
|
||||
let surface = create_surface(&instance)?;
|
||||
|
||||
// wgpu uses async here, but the returned future is ready on first poll on all platforms except WASM,
|
||||
// which we don't support right now.
|
||||
let adapter = poll_once(async {
|
||||
wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
|
||||
.await
|
||||
.expect("Failed to find an appropriate adapter")
|
||||
})
|
||||
.expect("internal error: wgpu adapter creation is not expected to be async");
|
||||
|
||||
let (device, queue) = poll_once(async {
|
||||
adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
// Request all non-experimental features the adapter supports,
|
||||
// so that embedders like Bevy can use full GPU capabilities.
|
||||
required_features: adapter.features()
|
||||
- wgpu::Features::all_experimental_mask(),
|
||||
required_limits: adapter.limits(),
|
||||
experimental_features: wgpu::ExperimentalFeatures::disabled(),
|
||||
memory_hints: wgpu::MemoryHints::MemoryUsage,
|
||||
trace: wgpu::Trace::default(),
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create device")
|
||||
})
|
||||
.expect("internal error: wgpu device creation is not expected to be async");
|
||||
(instance, adapter, device, queue, surface)
|
||||
}
|
||||
Some(_) => {
|
||||
return Err(
|
||||
"The FemtoVG WGPU renderer does not implement renderer selection by graphics API"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
Ok((instance, adapter, device, queue, surface))
|
||||
}
|
||||
|
||||
// Helper function to poll a future once. Remove once the suspension API uses async.
|
||||
fn poll_once<F: std::future::Future>(future: F) -> Option<F::Output> {
|
||||
struct DummyWaker();
|
||||
impl std::task::Wake for DummyWaker {
|
||||
fn wake(self: std::sync::Arc<Self>) {}
|
||||
}
|
||||
|
||||
let waker = std::sync::Arc::new(DummyWaker()).into();
|
||||
let mut ctx = std::task::Context::from_waker(&waker);
|
||||
|
||||
let future = std::pin::pin!(future);
|
||||
|
||||
match future.poll(&mut ctx) {
|
||||
std::task::Poll::Ready(result) => Some(result),
|
||||
std::task::Poll::Pending => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
/*!
|
||||
This module contains types that are public and re-exported in the slint-rs as well as the slint-interpreter crate as public API,
|
||||
in particular the `BackendSelector` type, to configure the WGPU-based renderer(s).
|
||||
*/
|
||||
|
||||
use alloc::boxed::Box;
|
||||
|
||||
pub use wgpu_28 as wgpu;
|
||||
|
||||
#[cfg(feature = "unstable-wgpu-28")]
|
||||
pub mod api {
|
||||
/*!
|
||||
This module contains types that are public and re-exported in the slint-rs as well as the slint-interpreter crate as public API.
|
||||
*/
|
||||
|
||||
pub use super::wgpu;
|
||||
|
||||
/// This data structure provides settings for initializing WGPU renderers.
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct WGPUSettings {
|
||||
/// The backends to use for the WGPU instance.
|
||||
pub backends: wgpu_28::Backends,
|
||||
/// The different options that are given to the selected backends.
|
||||
pub backend_options: wgpu_28::BackendOptions,
|
||||
/// The flags to fine-tune behaviour of the WGPU instance.
|
||||
pub instance_flags: wgpu_28::InstanceFlags,
|
||||
/// Memory budget thresholds used by some backends.
|
||||
pub instance_memory_budget_thresholds: wgpu_28::MemoryBudgetThresholds,
|
||||
|
||||
/// The power preference is used to influence the WGPU adapter selection.
|
||||
pub power_preference: wgpu_28::PowerPreference,
|
||||
|
||||
/// The label for the device. This is used to identify the device in debugging tools.
|
||||
pub device_label: Option<std::borrow::Cow<'static, str>>,
|
||||
/// The required features for the device.
|
||||
pub device_required_features: wgpu_28::Features,
|
||||
/// The required limits for the device.
|
||||
pub device_required_limits: wgpu_28::Limits,
|
||||
/// The experimental features for the device.
|
||||
pub device_experimental_features: wgpu_28::ExperimentalFeatures,
|
||||
/// The memory hints for the device.
|
||||
pub device_memory_hints: wgpu_28::MemoryHints,
|
||||
}
|
||||
|
||||
impl Default for WGPUSettings {
|
||||
fn default() -> Self {
|
||||
let backends = wgpu_28::Backends::from_env().unwrap_or_default();
|
||||
let dx12_shader_compiler = wgpu_28::Dx12Compiler::from_env().unwrap_or_default();
|
||||
let dx12_presentation_system =
|
||||
wgpu::wgt::Dx12SwapchainKind::from_env().unwrap_or_default();
|
||||
let dx12_latency_waitable_object =
|
||||
wgpu::wgt::Dx12UseFrameLatencyWaitableObject::from_env().unwrap_or_default();
|
||||
let gles_minor_version = wgpu_28::Gles3MinorVersion::from_env().unwrap_or_default();
|
||||
|
||||
Self {
|
||||
backends,
|
||||
backend_options: wgpu_28::BackendOptions {
|
||||
dx12: wgpu_28::Dx12BackendOptions {
|
||||
shader_compiler: dx12_shader_compiler,
|
||||
presentation_system: dx12_presentation_system,
|
||||
latency_waitable_object: dx12_latency_waitable_object,
|
||||
},
|
||||
gl: wgpu_28::GlBackendOptions {
|
||||
gles_minor_version,
|
||||
fence_behavior: wgpu_28::GlFenceBehavior::default(),
|
||||
},
|
||||
noop: wgpu::NoopBackendOptions::default(),
|
||||
},
|
||||
instance_flags: wgpu_28::InstanceFlags::from_build_config().with_env(),
|
||||
instance_memory_budget_thresholds: wgpu_28::MemoryBudgetThresholds::default(),
|
||||
|
||||
power_preference: wgpu_28::PowerPreference::from_env().unwrap_or_default(),
|
||||
|
||||
device_label: None,
|
||||
device_required_features: wgpu_28::Features::empty(),
|
||||
device_required_limits: wgpu_28::Limits::downlevel_webgl2_defaults(),
|
||||
device_experimental_features: wgpu_28::ExperimentalFeatures::disabled(),
|
||||
device_memory_hints: wgpu_28::MemoryHints::MemoryUsage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum describes the different ways to configure WGPU for rendering.
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum WGPUConfiguration {
|
||||
/// Use `Manual` if you've initialized WGPU and want to supply the instance, adapter,
|
||||
/// device, and queue for use.
|
||||
Manual {
|
||||
/// The WGPU instance to use.
|
||||
instance: wgpu_28::Instance,
|
||||
/// The WGPU adapter to use.
|
||||
adapter: wgpu_28::Adapter,
|
||||
/// The WGPU device to use.
|
||||
device: wgpu_28::Device,
|
||||
/// The WGPU queue to use.
|
||||
queue: wgpu_28::Queue,
|
||||
},
|
||||
/// Use `Automatic` if you want to let Slint select the WGPU instance, adapter, and
|
||||
/// device, but fine-tune aspects such as memory limits or features.
|
||||
Automatic(WGPUSettings),
|
||||
}
|
||||
|
||||
impl Default for WGPUConfiguration {
|
||||
fn default() -> Self {
|
||||
Self::Automatic(WGPUSettings::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<wgpu_28::Texture> for super::super::Image {
|
||||
type Error = TextureImportError;
|
||||
|
||||
fn try_from(texture: wgpu_28::Texture) -> Result<Self, Self::Error> {
|
||||
if texture.format() != wgpu_28::TextureFormat::Rgba8Unorm
|
||||
&& texture.format() != wgpu_28::TextureFormat::Rgba8UnormSrgb
|
||||
{
|
||||
return Err(Self::Error::InvalidFormat);
|
||||
}
|
||||
let usages = texture.usage();
|
||||
if !usages.contains(wgpu_28::TextureUsages::TEXTURE_BINDING)
|
||||
|| !usages.contains(wgpu_28::TextureUsages::RENDER_ATTACHMENT)
|
||||
{
|
||||
return Err(Self::Error::InvalidUsage);
|
||||
}
|
||||
Ok(Self(super::super::ImageInner::WGPUTexture(
|
||||
super::super::WGPUTexture::WGPU28Texture(texture),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, derive_more::Error)]
|
||||
#[non_exhaustive]
|
||||
/// This enum describes the possible errors that can occur when importing a WGPU texture,
|
||||
/// via [`Image::try_from()`](super::super::Image::try_from()).
|
||||
pub enum TextureImportError {
|
||||
/// The texture format is not supported. The only supported format is Rgba8Unorm and Rgba8UnormSrgb.
|
||||
InvalidFormat,
|
||||
/// The texture usage must include TEXTURE_BINDING as well as RENDER_ATTACHMENT.
|
||||
InvalidUsage,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for TextureImportError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
TextureImportError::InvalidFormat => f.write_str(
|
||||
"The texture format is not supported. The only supported format is Rgba8Unorm and Rgba8UnormSrgb",
|
||||
),
|
||||
TextureImportError::InvalidUsage => f.write_str(
|
||||
"The texture usage must include TEXTURE_BINDING as well as RENDER_ATTACHMENT",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use super::RequestedGraphicsAPI;
|
||||
|
||||
/// Internal helper function see if there are any GPU adapters for hardware accelerated rendering.
|
||||
/// This is used to determine if we should fall back to software rendering (instead of using WGPU
|
||||
/// software rendering, such as DX12's Warp adapter)
|
||||
pub fn any_wgpu28_adapters_with_gpu(requested_graphics_api: Option<RequestedGraphicsAPI>) -> bool {
|
||||
let allow_cpu = std::env::var("SLINT_WGPU_CPU").is_ok();
|
||||
if allow_cpu {
|
||||
return true;
|
||||
}
|
||||
let (instance, backends) = match requested_graphics_api {
|
||||
#[cfg(feature = "unstable-wgpu-28")]
|
||||
Some(RequestedGraphicsAPI::WGPU28(api::WGPUConfiguration::Manual { instance, .. })) => {
|
||||
(instance, wgpu::Backends::all())
|
||||
}
|
||||
#[cfg(feature = "unstable-wgpu-28")]
|
||||
Some(RequestedGraphicsAPI::WGPU28(api::WGPUConfiguration::Automatic(wgpu28_settings))) => {
|
||||
if cfg!(target_family = "wasm") {
|
||||
return true;
|
||||
}
|
||||
(
|
||||
wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu28_settings.backends,
|
||||
flags: wgpu28_settings.instance_flags,
|
||||
backend_options: wgpu28_settings.backend_options,
|
||||
memory_budget_thresholds: wgpu28_settings.instance_memory_budget_thresholds,
|
||||
}),
|
||||
wgpu28_settings.backends,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
if cfg!(target_family = "wasm") {
|
||||
return true;
|
||||
}
|
||||
|
||||
let backends = wgpu::Backends::from_env().unwrap_or_default();
|
||||
let dx12_shader_compiler = wgpu::Dx12Compiler::from_env().unwrap_or_default();
|
||||
let dx12_presentation_system =
|
||||
wgpu::wgt::Dx12SwapchainKind::from_env().unwrap_or_default();
|
||||
let dx12_latency_waitable_object =
|
||||
wgpu::wgt::Dx12UseFrameLatencyWaitableObject::from_env().unwrap_or_default();
|
||||
let gles_minor_version = wgpu::Gles3MinorVersion::from_env().unwrap_or_default();
|
||||
|
||||
(
|
||||
wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends,
|
||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||
backend_options: wgpu::BackendOptions {
|
||||
dx12: wgpu::Dx12BackendOptions {
|
||||
shader_compiler: dx12_shader_compiler,
|
||||
presentation_system: dx12_presentation_system,
|
||||
latency_waitable_object: dx12_latency_waitable_object,
|
||||
},
|
||||
gl: wgpu::GlBackendOptions {
|
||||
gles_minor_version,
|
||||
fence_behavior: wgpu::GlFenceBehavior::default(),
|
||||
},
|
||||
noop: wgpu::NoopBackendOptions::default(),
|
||||
},
|
||||
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||
}),
|
||||
backends,
|
||||
)
|
||||
}
|
||||
Some(_) => return false,
|
||||
};
|
||||
poll_once(instance.enumerate_adapters(backends))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.any(|adapter| adapter.get_info().device_type != wgpu::DeviceType::Cpu)
|
||||
}
|
||||
|
||||
/// Enum to represent different surface targets for WGPU initialization.
|
||||
pub enum SurfaceTarget {
|
||||
/// Standard window handle for windowed rendering.
|
||||
WindowHandle(Box<dyn wgpu::WindowHandle + 'static>),
|
||||
/// DRM surface target for direct rendering on Linux KMS.
|
||||
Drm(wgpu::SurfaceTargetUnsafe),
|
||||
}
|
||||
|
||||
impl From<Box<dyn wgpu::WindowHandle + 'static>> for SurfaceTarget {
|
||||
fn from(handle: Box<dyn wgpu::WindowHandle + 'static>) -> Self {
|
||||
Self::WindowHandle(handle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper function to initialize the wgpu instance/adapter/device/queue from either scratch or
|
||||
/// developer-provided config. This is called by any renderer intending to support WGPU.
|
||||
pub fn init_instance_adapter_device_queue_surface(
|
||||
surface_target: impl Into<SurfaceTarget>,
|
||||
requested_graphics_api: Option<RequestedGraphicsAPI>,
|
||||
backends_to_avoid: wgpu::Backends,
|
||||
) -> Result<
|
||||
(
|
||||
wgpu_28::Instance,
|
||||
wgpu_28::Adapter,
|
||||
wgpu_28::Device,
|
||||
wgpu_28::Queue,
|
||||
wgpu_28::Surface<'static>,
|
||||
),
|
||||
Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
> {
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
let surface_target = surface_target.into();
|
||||
|
||||
let create_surface = |instance: &wgpu::Instance| {
|
||||
match surface_target {
|
||||
SurfaceTarget::WindowHandle(window_handle) => instance.create_surface(window_handle),
|
||||
// Safety: The caller ensures the DRM file descriptor in the surface target
|
||||
// remains valid for the lifetime of the returned surface, by storing the
|
||||
// DrmOutput in the renderer adapter.
|
||||
SurfaceTarget::Drm(surface_target_unsafe) => unsafe {
|
||||
instance.create_surface_unsafe(surface_target_unsafe)
|
||||
},
|
||||
}
|
||||
.map_err(|e| {
|
||||
crate::api::PlatformError::from(alloc::format!(
|
||||
"Error creating wgpu window surface: {e}"
|
||||
))
|
||||
})
|
||||
};
|
||||
|
||||
let (instance, adapter, device, queue, surface) = match requested_graphics_api {
|
||||
#[cfg(feature = "unstable-wgpu-28")]
|
||||
Some(RequestedGraphicsAPI::WGPU28(api::WGPUConfiguration::Manual {
|
||||
instance,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
})) => {
|
||||
let surface = create_surface(&instance)?;
|
||||
(instance, adapter, device, queue, surface)
|
||||
}
|
||||
#[cfg(feature = "unstable-wgpu-28")]
|
||||
Some(RequestedGraphicsAPI::WGPU28(api::WGPUConfiguration::Automatic(wgpu28_settings))) => {
|
||||
// wgpu uses async here, but the returned future is ready on first poll on all platforms except WASM,
|
||||
// which we don't support right now.
|
||||
let instance = poll_once(async {
|
||||
wgpu::util::new_instance_with_webgpu_detection(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu28_settings.backends & !backends_to_avoid,
|
||||
flags: wgpu28_settings.instance_flags,
|
||||
backend_options: wgpu28_settings.backend_options,
|
||||
memory_budget_thresholds: wgpu28_settings.instance_memory_budget_thresholds,
|
||||
})
|
||||
.await
|
||||
})
|
||||
.expect("internal error: wgpu instance creation is not expected to be async");
|
||||
|
||||
let surface = create_surface(&instance)?;
|
||||
|
||||
// wgpu uses async here, but the returned future is ready on first poll on all platforms except WASM,
|
||||
// which we don't support right now.
|
||||
let adapter = poll_once(async {
|
||||
match wgpu::util::initialize_adapter_from_env(&instance, Some(&surface)).await {
|
||||
Ok(adapter) => Ok(adapter),
|
||||
Err(_) => {
|
||||
instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu28_settings.power_preference,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: Some(&surface),
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
.expect("Failed to find an appropriate adapter")
|
||||
})
|
||||
.expect("internal error: wgpu adapter creation is not expected to be async");
|
||||
|
||||
let (device, queue) = poll_once(async {
|
||||
adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: wgpu28_settings.device_label.as_deref(),
|
||||
required_features: wgpu28_settings.device_required_features,
|
||||
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
|
||||
required_limits: wgpu28_settings
|
||||
.device_required_limits
|
||||
.using_resolution(adapter.limits()),
|
||||
experimental_features: wgpu28_settings.device_experimental_features,
|
||||
memory_hints: wgpu28_settings.device_memory_hints,
|
||||
trace: wgpu::Trace::default(),
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create device")
|
||||
})
|
||||
.expect("internal error: wgpu device creation is not expected to be async");
|
||||
|
||||
(instance, adapter, device, queue, surface)
|
||||
}
|
||||
None => {
|
||||
let backends = wgpu::Backends::from_env().unwrap_or_default() & !backends_to_avoid;
|
||||
let dx12_shader_compiler = wgpu::Dx12Compiler::from_env().unwrap_or_default();
|
||||
let dx12_presentation_system =
|
||||
wgpu::wgt::Dx12SwapchainKind::from_env().unwrap_or_default();
|
||||
let dx12_latency_waitable_object =
|
||||
wgpu::wgt::Dx12UseFrameLatencyWaitableObject::from_env().unwrap_or_default();
|
||||
let gles_minor_version = wgpu::Gles3MinorVersion::from_env().unwrap_or_default();
|
||||
|
||||
// wgpu uses async here, but the returned future is ready on first poll on all platforms except WASM,
|
||||
// which we don't support right now.
|
||||
let instance = poll_once(async {
|
||||
wgpu::util::new_instance_with_webgpu_detection(&wgpu::InstanceDescriptor {
|
||||
backends,
|
||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||
backend_options: wgpu::BackendOptions {
|
||||
dx12: wgpu::Dx12BackendOptions {
|
||||
shader_compiler: dx12_shader_compiler,
|
||||
presentation_system: dx12_presentation_system,
|
||||
latency_waitable_object: dx12_latency_waitable_object,
|
||||
},
|
||||
gl: wgpu::GlBackendOptions {
|
||||
gles_minor_version,
|
||||
fence_behavior: wgpu::GlFenceBehavior::default(),
|
||||
},
|
||||
noop: wgpu::NoopBackendOptions::default(),
|
||||
},
|
||||
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||
})
|
||||
.await
|
||||
})
|
||||
.expect("internal error: wgpu instance creation is not expected to be async");
|
||||
|
||||
let surface = create_surface(&instance)?;
|
||||
|
||||
// wgpu uses async here, but the returned future is ready on first poll on all platforms except WASM,
|
||||
// which we don't support right now.
|
||||
let adapter = poll_once(async {
|
||||
wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
|
||||
.await
|
||||
.expect("Failed to find an appropriate adapter")
|
||||
})
|
||||
.expect("internal error: wgpu adapter creation is not expected to be async");
|
||||
|
||||
let (device, queue) = poll_once(async {
|
||||
adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
// Request all non-experimental features the adapter supports,
|
||||
// so that embedders like Bevy can use full GPU capabilities.
|
||||
required_features: adapter.features()
|
||||
- wgpu::Features::all_experimental_mask(),
|
||||
required_limits: adapter.limits(),
|
||||
experimental_features: wgpu::ExperimentalFeatures::disabled(),
|
||||
memory_hints: wgpu::MemoryHints::MemoryUsage,
|
||||
trace: wgpu::Trace::default(),
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create device")
|
||||
})
|
||||
.expect("internal error: wgpu device creation is not expected to be async");
|
||||
(instance, adapter, device, queue, surface)
|
||||
}
|
||||
Some(_) => {
|
||||
return Err(
|
||||
"The FemtoVG WGPU renderer does not implement renderer selection by graphics API"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
Ok((instance, adapter, device, queue, surface))
|
||||
}
|
||||
|
||||
// Helper function to poll a future once. Remove once the suspension API uses async.
|
||||
fn poll_once<F: std::future::Future>(future: F) -> Option<F::Output> {
|
||||
struct DummyWaker();
|
||||
impl std::task::Wake for DummyWaker {
|
||||
fn wake(self: std::sync::Arc<Self>) {}
|
||||
}
|
||||
|
||||
let waker = std::sync::Arc::new(DummyWaker()).into();
|
||||
let mut ctx = std::task::Context::from_waker(&waker);
|
||||
|
||||
let future = std::pin::pin!(future);
|
||||
|
||||
match future.poll(&mut ctx) {
|
||||
std::task::Poll::Ready(result) => Some(result),
|
||||
std::task::Poll::Pending => None,
|
||||
}
|
||||
}
|
||||
+2435
File diff suppressed because it is too large.
Load diff
+298
@@ -0,0 +1,298 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
// cSpell: ignore nesw
|
||||
|
||||
/*!
|
||||
This module contains the code moving the keyboard focus between items
|
||||
*/
|
||||
|
||||
use crate::item_tree::ItemTreeNodeArray;
|
||||
|
||||
pub fn step_out_of_node(
|
||||
index: u32,
|
||||
item_tree: &crate::item_tree::ItemTreeNodeArray,
|
||||
) -> Option<u32> {
|
||||
let mut self_or_ancestor = index;
|
||||
loop {
|
||||
if let Some(sibling) = item_tree.next_sibling(self_or_ancestor) {
|
||||
return Some(sibling);
|
||||
}
|
||||
if let Some(ancestor) = item_tree.parent(self_or_ancestor) {
|
||||
self_or_ancestor = ancestor;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_next_in_local_focus_chain(
|
||||
index: u32,
|
||||
item_tree: &crate::item_tree::ItemTreeNodeArray,
|
||||
) -> Option<u32> {
|
||||
if let Some(child) = item_tree.first_child(index) {
|
||||
return Some(child);
|
||||
}
|
||||
|
||||
step_out_of_node(index, item_tree)
|
||||
}
|
||||
|
||||
fn step_into_node(item_tree: &ItemTreeNodeArray, index: u32) -> u32 {
|
||||
let mut node = index;
|
||||
loop {
|
||||
if let Some(last_child) = item_tree.last_child(node) {
|
||||
node = last_child;
|
||||
} else {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_previous_in_local_focus_chain(
|
||||
index: u32,
|
||||
item_tree: &crate::item_tree::ItemTreeNodeArray,
|
||||
) -> Option<u32> {
|
||||
if let Some(previous) = item_tree.previous_sibling(index) {
|
||||
Some(step_into_node(item_tree, previous))
|
||||
} else {
|
||||
item_tree.parent(index)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::vec;
|
||||
|
||||
use crate::item_tree::ItemTreeNode;
|
||||
|
||||
fn validate_focus_chains(item_tree: ItemTreeNodeArray<'_>) {
|
||||
let forward_chain = {
|
||||
let mut tmp = alloc::vec::Vec::with_capacity(item_tree.node_count());
|
||||
let mut node = 0;
|
||||
|
||||
loop {
|
||||
tmp.push(node);
|
||||
if let Some(next_node) = default_next_in_local_focus_chain(node, &item_tree) {
|
||||
node = next_node;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tmp
|
||||
};
|
||||
let reverse_backward_chain = {
|
||||
let mut tmp = alloc::vec::Vec::with_capacity(item_tree.node_count());
|
||||
let mut node = step_into_node(&item_tree, 0);
|
||||
|
||||
loop {
|
||||
tmp.push(node);
|
||||
if let Some(next_node) = default_previous_in_local_focus_chain(node, &item_tree) {
|
||||
node = next_node;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tmp.reverse();
|
||||
tmp
|
||||
};
|
||||
|
||||
assert_eq!(forward_chain, reverse_backward_chain);
|
||||
assert_eq!(forward_chain.len(), item_tree.node_count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_chain_root_only() {
|
||||
let nodes = vec![ItemTreeNode::Item {
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 1,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
}];
|
||||
|
||||
let tree: ItemTreeNodeArray = (nodes.as_slice()).into();
|
||||
validate_focus_chains(tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_chain_one_child() {
|
||||
let nodes = vec![
|
||||
ItemTreeNode::Item {
|
||||
is_accessible: false,
|
||||
children_count: 1,
|
||||
children_index: 1,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 2,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
];
|
||||
|
||||
let tree: ItemTreeNodeArray = (nodes.as_slice()).into();
|
||||
validate_focus_chains(tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_chain_three_children() {
|
||||
let nodes = vec![
|
||||
ItemTreeNode::Item {
|
||||
is_accessible: false,
|
||||
children_count: 3,
|
||||
children_index: 1,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 4,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 4,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 4,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
];
|
||||
|
||||
let tree: ItemTreeNodeArray = (nodes.as_slice()).into();
|
||||
validate_focus_chains(tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_chain_complex_tree() {
|
||||
let nodes = vec![
|
||||
ItemTreeNode::Item {
|
||||
// 0
|
||||
is_accessible: false,
|
||||
children_count: 2,
|
||||
children_index: 1,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 1
|
||||
is_accessible: false,
|
||||
children_count: 2,
|
||||
children_index: 3,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 2
|
||||
is_accessible: false,
|
||||
children_count: 1,
|
||||
children_index: 11,
|
||||
parent_index: 0,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 3
|
||||
is_accessible: false,
|
||||
children_count: 1,
|
||||
children_index: 5,
|
||||
parent_index: 1,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 4
|
||||
is_accessible: false,
|
||||
children_count: 2,
|
||||
children_index: 6,
|
||||
parent_index: 1,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 5
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 0,
|
||||
parent_index: 3,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 6
|
||||
is_accessible: false,
|
||||
children_count: 2,
|
||||
children_index: 8,
|
||||
parent_index: 4,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 7
|
||||
is_accessible: false,
|
||||
children_count: 1,
|
||||
children_index: 10,
|
||||
parent_index: 4,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 8
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 0,
|
||||
parent_index: 6,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 9
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 0,
|
||||
parent_index: 6,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 10
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 0,
|
||||
parent_index: 7,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 11
|
||||
is_accessible: false,
|
||||
children_count: 2,
|
||||
children_index: 12,
|
||||
parent_index: 2,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 12
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 0,
|
||||
parent_index: 11,
|
||||
item_array_index: 0,
|
||||
},
|
||||
ItemTreeNode::Item {
|
||||
// 13
|
||||
is_accessible: false,
|
||||
children_count: 0,
|
||||
children_index: 0,
|
||||
parent_index: 11,
|
||||
item_array_index: 0,
|
||||
},
|
||||
];
|
||||
|
||||
let tree: ItemTreeNodeArray = (nodes.as_slice()).into();
|
||||
validate_focus_chains(tree);
|
||||
}
|
||||
}
|
||||
+610
@@ -0,0 +1,610 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
#![warn(missing_docs)]
|
||||
//! module for rendering the tree of items
|
||||
|
||||
use super::items::*;
|
||||
use crate::graphics::{Color, FontRequest, Image, IntRect};
|
||||
use crate::item_tree::ItemTreeRc;
|
||||
use crate::item_tree::{ItemVisitor, ItemVisitorVTable, VisitChildrenResult};
|
||||
use crate::lengths::{
|
||||
LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector,
|
||||
};
|
||||
pub use crate::partial_renderer::CachedRenderingData;
|
||||
use crate::window::WindowAdapterRc;
|
||||
use crate::{Brush, SharedString};
|
||||
#[cfg(feature = "std")]
|
||||
use alloc::boxed::Box;
|
||||
#[cfg(feature = "std")]
|
||||
use core::cell::RefCell;
|
||||
use core::pin::Pin;
|
||||
#[cfg(feature = "std")]
|
||||
use std::collections::HashMap;
|
||||
use vtable::VRc;
|
||||
|
||||
/// A per-item cache.
|
||||
///
|
||||
/// Cache rendering information for a given item.
|
||||
///
|
||||
/// Use [`ItemCache::get_or_update_cache_entry`] to get or update the items, the
|
||||
/// cache is automatically invalided when the property gets dirty.
|
||||
/// [`ItemCache::component_destroyed`] must be called to clear the cache for that
|
||||
/// component.
|
||||
#[cfg(feature = "std")]
|
||||
pub struct ItemCache<T> {
|
||||
/// The pointer is a pointer to a component
|
||||
map: RefCell<HashMap<*const vtable::Dyn, HashMap<u32, crate::graphics::CachedGraphicsData<T>>>>,
|
||||
/// Track if the window scale factor changes; used to clear the cache if necessary.
|
||||
window_scale_factor_tracker: Pin<Box<crate::properties::PropertyTracker>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T> Default for ItemCache<T> {
|
||||
fn default() -> Self {
|
||||
Self { map: Default::default(), window_scale_factor_tracker: Box::pin(Default::default()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: Clone> ItemCache<T> {
|
||||
/// Returns the cached value associated to the `item_rc` if it is still valid.
|
||||
/// Otherwise call the `update_fn` to compute that value, and track property access
|
||||
/// so it is automatically invalided when property becomes dirty.
|
||||
pub fn get_or_update_cache_entry(&self, item_rc: &ItemRc, update_fn: impl FnOnce() -> T) -> T {
|
||||
let component = &(**item_rc.item_tree()) as *const _;
|
||||
let mut borrowed = self.map.borrow_mut();
|
||||
match borrowed.entry(component).or_default().entry(item_rc.index()) {
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
let mut tracker = entry.get_mut().dependency_tracker.take();
|
||||
drop(borrowed);
|
||||
let maybe_new_data = tracker
|
||||
.get_or_insert_with(|| Box::pin(Default::default()))
|
||||
.as_ref()
|
||||
.evaluate_if_dirty(update_fn);
|
||||
let mut borrowed = self.map.borrow_mut();
|
||||
let e = borrowed.get_mut(&component).unwrap().get_mut(&item_rc.index()).unwrap();
|
||||
e.dependency_tracker = tracker;
|
||||
if let Some(new_data) = maybe_new_data {
|
||||
e.data = new_data.clone();
|
||||
new_data
|
||||
} else {
|
||||
e.data.clone()
|
||||
}
|
||||
}
|
||||
std::collections::hash_map::Entry::Vacant(_) => {
|
||||
drop(borrowed);
|
||||
let new_entry = crate::graphics::CachedGraphicsData::new(update_fn);
|
||||
let data = new_entry.data.clone();
|
||||
self.map
|
||||
.borrow_mut()
|
||||
.get_mut(&component)
|
||||
.unwrap()
|
||||
.insert(item_rc.index(), new_entry);
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T> ItemCache<T> {
|
||||
/// Returns the cached value associated with the `item_rc` if it is in the cache
|
||||
/// and still valid.
|
||||
pub fn with_entry<U>(
|
||||
&self,
|
||||
item_rc: &ItemRc,
|
||||
callback: impl FnOnce(&T) -> Option<U>,
|
||||
) -> Option<U> {
|
||||
let component = &(**item_rc.item_tree()) as *const _;
|
||||
self.map
|
||||
.borrow()
|
||||
.get(&component)
|
||||
.and_then(|per_component_entries| per_component_entries.get(&item_rc.index()))
|
||||
.and_then(|entry| callback(&entry.data))
|
||||
}
|
||||
|
||||
/// Clears the cache if the window's scale factor has changed since the last call.
|
||||
pub fn clear_cache_if_scale_factor_changed(&self, window: &crate::api::Window) {
|
||||
if self.window_scale_factor_tracker.is_dirty() {
|
||||
self.window_scale_factor_tracker
|
||||
.as_ref()
|
||||
.evaluate_as_dependency_root(|| window.scale_factor());
|
||||
self.clear_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// free the whole cache
|
||||
pub fn clear_all(&self) {
|
||||
self.map.borrow_mut().clear();
|
||||
}
|
||||
|
||||
/// Function that must be called when a component is destroyed.
|
||||
///
|
||||
/// Usually can be called from [`crate::window::WindowAdapterInternal::unregister_item_tree`]
|
||||
pub fn component_destroyed(&self, component: crate::item_tree::ItemTreeRef) {
|
||||
let component_ptr: *const _ =
|
||||
crate::item_tree::ItemTreeRef::as_ptr(component).cast().as_ptr();
|
||||
self.map.borrow_mut().remove(&component_ptr);
|
||||
}
|
||||
|
||||
/// free the cache for a given item
|
||||
pub fn release(&self, item_rc: &ItemRc) {
|
||||
let component = &(**item_rc.item_tree()) as *const _;
|
||||
if let Some(sub) = self.map.borrow_mut().get_mut(&component) {
|
||||
sub.remove(&item_rc.index());
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if there are no entries in the cache; false otherwise.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.borrow().is_empty()
|
||||
}
|
||||
|
||||
/// Returns a [`RefMut`](std::cell::RefMut) referencing the cached value associated with
|
||||
/// `item_rc`, updating the cache entry first if necessary using `update_fn`.
|
||||
///
|
||||
/// Unlike [`get_or_update_cache_entry`](Self::get_or_update_cache_entry), this method does
|
||||
/// not require `T: Clone` and returns a mutable reference into the cache, which permits
|
||||
/// in-place modification or temporary extraction of the cached value (e.g., via
|
||||
/// [`std::mem::take`]).
|
||||
pub fn get_or_update_cache_entry_ref(
|
||||
&self,
|
||||
item_rc: &ItemRc,
|
||||
update_fn: impl FnOnce() -> T,
|
||||
) -> std::cell::RefMut<'_, T> {
|
||||
let component = &(**item_rc.item_tree()) as *const _;
|
||||
let index = item_rc.index();
|
||||
|
||||
{
|
||||
let mut borrowed = self.map.borrow_mut();
|
||||
match borrowed.entry(component).or_default().entry(index) {
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
let mut tracker = entry.get_mut().dependency_tracker.take();
|
||||
drop(borrowed);
|
||||
let maybe_new_data = tracker
|
||||
.get_or_insert_with(|| Box::pin(Default::default()))
|
||||
.as_ref()
|
||||
.evaluate_if_dirty(update_fn);
|
||||
let mut borrowed = self.map.borrow_mut();
|
||||
let e = borrowed.get_mut(&component).unwrap().get_mut(&index).unwrap();
|
||||
e.dependency_tracker = tracker;
|
||||
if let Some(new_data) = maybe_new_data {
|
||||
e.data = new_data;
|
||||
}
|
||||
}
|
||||
std::collections::hash_map::Entry::Vacant(_) => {
|
||||
drop(borrowed);
|
||||
let new_entry = crate::graphics::CachedGraphicsData::new(update_fn);
|
||||
self.map.borrow_mut().get_mut(&component).unwrap().insert(index, new_entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cell::RefMut::map(self.map.borrow_mut(), |map| {
|
||||
&mut map.get_mut(&component).unwrap().get_mut(&index).unwrap().data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the children of the item with the specified index into the renderer.
|
||||
pub fn render_item_children(
|
||||
renderer: &mut dyn ItemRenderer,
|
||||
component: &ItemTreeRc,
|
||||
index: isize,
|
||||
window_adapter: &WindowAdapterRc,
|
||||
) {
|
||||
let mut actual_visitor =
|
||||
|component: &ItemTreeRc, index: u32, item: Pin<ItemRef>| -> VisitChildrenResult {
|
||||
renderer.save_state();
|
||||
let item_rc = ItemRc::new(component.clone(), index);
|
||||
|
||||
let (do_draw, item_geometry) = renderer.filter_item(&item_rc, window_adapter);
|
||||
|
||||
let item_origin = item_geometry.origin;
|
||||
renderer.translate(item_origin.to_vector());
|
||||
|
||||
// Don't render items that are clipped, with the exception of the Clip or Flickable since
|
||||
// they themselves clip their content.
|
||||
let render_result = if do_draw
|
||||
|| item.as_ref().clips_children()
|
||||
// HACK, the geometry of the box shadow does not include the shadow, because when the shadow is the root for repeated elements it would translate the children
|
||||
|| ItemRef::downcast_pin::<BoxShadow>(item).is_some()
|
||||
// Transform and Opacity should also be applied regardless if the item itself is clipped or not
|
||||
|| ItemRef::downcast_pin::<Transform>(item).is_some()
|
||||
|| ItemRef::downcast_pin::<Opacity>(item).is_some()
|
||||
|| ItemRef::downcast_pin::<Layer>(item).is_some()
|
||||
{
|
||||
item.as_ref().render(
|
||||
&mut (renderer as &mut dyn ItemRenderer),
|
||||
&item_rc,
|
||||
item_geometry.size,
|
||||
)
|
||||
} else {
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
};
|
||||
|
||||
if matches!(render_result, RenderingResult::ContinueRenderingChildren) {
|
||||
render_item_children(renderer, component, index as isize, window_adapter);
|
||||
}
|
||||
renderer.restore_state();
|
||||
VisitChildrenResult::CONTINUE
|
||||
};
|
||||
vtable::new_vref!(let mut actual_visitor : VRefMut<ItemVisitorVTable> for ItemVisitor = &mut actual_visitor);
|
||||
VRc::borrow_pin(component).as_ref().visit_children_item(
|
||||
index,
|
||||
crate::item_tree::TraversalOrder::BackToFront,
|
||||
actual_visitor,
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders the tree of items that component holds, using the specified renderer. Rendering is done
|
||||
/// relative to the specified origin.
|
||||
pub fn render_component_items(
|
||||
component: &ItemTreeRc,
|
||||
renderer: &mut dyn ItemRenderer,
|
||||
origin: LogicalPoint,
|
||||
window_adapter: &WindowAdapterRc,
|
||||
) {
|
||||
renderer.save_state();
|
||||
renderer.translate(origin.to_vector());
|
||||
|
||||
render_item_children(renderer, component, -1, window_adapter);
|
||||
|
||||
renderer.restore_state();
|
||||
}
|
||||
|
||||
/// Compute the bounding rect of all children. This does /not/ include item's own bounding rect. Remember to run this
|
||||
/// via `evaluate_no_tracking`.
|
||||
pub fn item_children_bounding_rect(
|
||||
item_rc: &ItemRc,
|
||||
window_adapter: &WindowAdapterRc,
|
||||
) -> LogicalRect {
|
||||
item_children_bounding_rect_transformed(item_rc, window_adapter, Default::default())
|
||||
}
|
||||
|
||||
fn item_children_bounding_rect_transformed(
|
||||
item_rc: &ItemRc,
|
||||
window_adapter: &WindowAdapterRc,
|
||||
transform: crate::lengths::ItemTransform,
|
||||
) -> LogicalRect {
|
||||
let mut bounding_rect = LogicalRect::zero();
|
||||
|
||||
let mut actual_visitor =
|
||||
|component: &ItemTreeRc, index: u32, _ref: Pin<ItemRef>| -> VisitChildrenResult {
|
||||
let item_rc = ItemRc::new(component.clone(), index);
|
||||
let bounds_with_children =
|
||||
item_with_children_bounding_rect_transformed(&item_rc, window_adapter, transform);
|
||||
|
||||
bounding_rect = bounding_rect.union(&bounds_with_children);
|
||||
|
||||
VisitChildrenResult::CONTINUE
|
||||
};
|
||||
|
||||
vtable::new_vref!(let mut actual_visitor : VRefMut<ItemVisitorVTable> for ItemVisitor = &mut actual_visitor);
|
||||
VRc::borrow_pin(item_rc.item_tree()).as_ref().visit_children_item(
|
||||
item_rc.index() as isize,
|
||||
crate::item_tree::TraversalOrder::BackToFront,
|
||||
actual_visitor,
|
||||
);
|
||||
|
||||
bounding_rect
|
||||
}
|
||||
|
||||
fn item_with_children_bounding_rect_transformed(
|
||||
item_rc: &ItemRc,
|
||||
window_adapter: &WindowAdapterRc,
|
||||
transform: crate::lengths::ItemTransform,
|
||||
) -> LogicalRect {
|
||||
let item_geom = item_rc.geometry();
|
||||
|
||||
if item_rc.borrow().as_ref().clips_children() {
|
||||
transform.outer_transformed_rect(&item_geom.cast()).cast()
|
||||
} else {
|
||||
let bounding = item_rc.bounding_rect(&item_geom, window_adapter);
|
||||
let bounding = transform.outer_transformed_rect(&bounding.cast());
|
||||
let children_relative_transform = item_rc
|
||||
.children_transform()
|
||||
.unwrap_or_default()
|
||||
.then_translate(item_geom.origin.to_vector().cast());
|
||||
|
||||
let children_absolute_transform = transform.then(&children_relative_transform);
|
||||
|
||||
item_children_bounding_rect_transformed(
|
||||
item_rc,
|
||||
window_adapter,
|
||||
children_absolute_transform,
|
||||
)
|
||||
.union(&bounding.cast())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for an item that represent a Rectangle to the Renderer
|
||||
#[allow(missing_docs)]
|
||||
pub trait RenderRectangle {
|
||||
fn background(self: Pin<&Self>) -> Brush;
|
||||
}
|
||||
|
||||
/// Trait for an item that represent a Rectangle with a border to the Renderer
|
||||
#[allow(missing_docs)]
|
||||
pub trait RenderBorderRectangle {
|
||||
fn background(self: Pin<&Self>) -> Brush;
|
||||
fn border_width(self: Pin<&Self>) -> LogicalLength;
|
||||
fn border_radius(self: Pin<&Self>) -> LogicalBorderRadius;
|
||||
fn border_color(self: Pin<&Self>) -> Brush;
|
||||
}
|
||||
|
||||
/// Trait for an item that represents an Image towards the renderer
|
||||
#[allow(missing_docs)]
|
||||
pub trait RenderImage {
|
||||
fn target_size(self: Pin<&Self>) -> LogicalSize;
|
||||
fn source(self: Pin<&Self>) -> Image;
|
||||
fn source_clip(self: Pin<&Self>) -> Option<IntRect>;
|
||||
fn image_fit(self: Pin<&Self>) -> ImageFit;
|
||||
fn rendering(self: Pin<&Self>) -> ImageRendering;
|
||||
fn colorize(self: Pin<&Self>) -> Brush;
|
||||
fn alignment(self: Pin<&Self>) -> (ImageHorizontalAlignment, ImageVerticalAlignment);
|
||||
fn tiling(self: Pin<&Self>) -> (ImageTiling, ImageTiling);
|
||||
}
|
||||
|
||||
/// Trait for an item has font properties
|
||||
#[allow(missing_docs)]
|
||||
pub trait HasFont {
|
||||
fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest;
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub enum PlainOrStyledText {
|
||||
Plain(SharedString),
|
||||
Styled(crate::styled_text::StyledText),
|
||||
}
|
||||
|
||||
/// Trait for an item that represents an string towards the renderer
|
||||
#[allow(missing_docs)]
|
||||
pub trait RenderString: HasFont {
|
||||
fn text(self: Pin<&Self>) -> PlainOrStyledText;
|
||||
}
|
||||
|
||||
/// Trait for an item that represents an Text towards the renderer
|
||||
#[allow(missing_docs)]
|
||||
pub trait RenderText: RenderString {
|
||||
fn target_size(self: Pin<&Self>) -> LogicalSize;
|
||||
fn color(self: Pin<&Self>) -> Brush;
|
||||
fn alignment(self: Pin<&Self>) -> (TextHorizontalAlignment, TextVerticalAlignment);
|
||||
fn wrap(self: Pin<&Self>) -> TextWrap;
|
||||
fn overflow(self: Pin<&Self>) -> TextOverflow;
|
||||
fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle);
|
||||
fn is_markdown(self: Pin<&Self>) -> bool;
|
||||
fn link_color(self: Pin<&Self>) -> Color;
|
||||
}
|
||||
|
||||
impl HasFont for (SharedString, Brush) {
|
||||
fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest {
|
||||
crate::items::WindowItem::resolved_font_request(
|
||||
self_rc,
|
||||
SharedString::default(),
|
||||
0,
|
||||
LogicalLength::default(),
|
||||
LogicalLength::default(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderString for (SharedString, Brush) {
|
||||
fn text(self: Pin<&Self>) -> PlainOrStyledText {
|
||||
PlainOrStyledText::Plain(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderText for (SharedString, Brush) {
|
||||
fn target_size(self: Pin<&Self>) -> LogicalSize {
|
||||
LogicalSize::default()
|
||||
}
|
||||
|
||||
fn color(self: Pin<&Self>) -> Brush {
|
||||
self.1.clone()
|
||||
}
|
||||
|
||||
fn link_color(self: Pin<&Self>) -> Color {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn alignment(
|
||||
self: Pin<&Self>,
|
||||
) -> (crate::items::TextHorizontalAlignment, crate::items::TextVerticalAlignment) {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn wrap(self: Pin<&Self>) -> crate::items::TextWrap {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn overflow(self: Pin<&Self>) -> crate::items::TextOverflow {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle) {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn is_markdown(self: Pin<&Self>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait used to render each items.
|
||||
///
|
||||
/// The item needs to be rendered relative to its (x,y) position. For example,
|
||||
/// draw_rectangle should draw a rectangle in `(pos.x + rect.x, pos.y + rect.y)`
|
||||
#[allow(missing_docs)]
|
||||
pub trait ItemRenderer {
|
||||
fn draw_rectangle(
|
||||
&mut self,
|
||||
rect: Pin<&dyn RenderRectangle>,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
_cache: &CachedRenderingData,
|
||||
);
|
||||
fn draw_border_rectangle(
|
||||
&mut self,
|
||||
rect: Pin<&dyn RenderBorderRectangle>,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
_cache: &CachedRenderingData,
|
||||
);
|
||||
fn draw_window_background(
|
||||
&mut self,
|
||||
rect: Pin<&dyn RenderRectangle>,
|
||||
self_rc: &ItemRc,
|
||||
size: LogicalSize,
|
||||
cache: &CachedRenderingData,
|
||||
);
|
||||
fn draw_image(
|
||||
&mut self,
|
||||
image: Pin<&dyn RenderImage>,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
_cache: &CachedRenderingData,
|
||||
);
|
||||
fn draw_text(
|
||||
&mut self,
|
||||
text: Pin<&dyn RenderText>,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
_cache: &CachedRenderingData,
|
||||
);
|
||||
fn draw_text_input(
|
||||
&mut self,
|
||||
text_input: Pin<&TextInput>,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
);
|
||||
#[cfg(feature = "path")]
|
||||
fn draw_path(&mut self, path: Pin<&Path>, _self_rc: &ItemRc, _size: LogicalSize);
|
||||
fn draw_box_shadow(
|
||||
&mut self,
|
||||
box_shadow: Pin<&BoxShadow>,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
);
|
||||
fn visit_opacity(
|
||||
&mut self,
|
||||
opacity_item: Pin<&Opacity>,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
self.apply_opacity(opacity_item.opacity());
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
fn visit_layer(
|
||||
&mut self,
|
||||
_layer_item: Pin<&Layer>,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
// Not supported
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
// Apply the bounds of the Clip element, if enabled. The default implementation calls
|
||||
// combine_clip, but the render may choose an alternate way of implementing the clip.
|
||||
// For example the GL backend uses a layered rendering approach.
|
||||
fn visit_clip(
|
||||
&mut self,
|
||||
clip_item: Pin<&Clip>,
|
||||
_item_rc: &ItemRc,
|
||||
size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
if clip_item.clip() {
|
||||
let clip_region_valid = self.combine_clip(
|
||||
LogicalRect::new(LogicalPoint::default(), size),
|
||||
clip_item.logical_border_radius(),
|
||||
clip_item.border_width(),
|
||||
);
|
||||
|
||||
// If clipping is enabled but the clip element is outside the visible range, then we don't
|
||||
// need to bother doing anything, not even rendering the children.
|
||||
if !clip_region_valid {
|
||||
return RenderingResult::ContinueRenderingWithoutChildren;
|
||||
}
|
||||
}
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
/// Clip the further call until restore_state.
|
||||
/// radius/border_width can be used for border rectangle clip.
|
||||
/// (FIXME: consider removing radius/border_width and have another function that take a path instead)
|
||||
/// Returns a boolean indicating the state of the new clip region: true if the clip region covers
|
||||
/// an area; false if the clip region is empty.
|
||||
fn combine_clip(
|
||||
&mut self,
|
||||
rect: LogicalRect,
|
||||
radius: LogicalBorderRadius,
|
||||
border_width: LogicalLength,
|
||||
) -> bool;
|
||||
/// Get the current clip bounding box in the current transformed coordinate.
|
||||
fn get_current_clip(&self) -> LogicalRect;
|
||||
|
||||
fn translate(&mut self, distance: LogicalVector);
|
||||
/// Returns the accumulated local-to-screen transform, including
|
||||
/// translate, scale, and rotate.
|
||||
fn current_transform(&self) -> crate::lengths::ItemTransform {
|
||||
todo!("this renderer does not track transforms for partial rendering")
|
||||
}
|
||||
fn rotate(&mut self, angle_in_degrees: f32);
|
||||
fn scale(&mut self, scale_x_factor: f32, scale_y_factor: f32);
|
||||
/// Apply the opacity (between 0 and 1) for all following items until the next call to restore_state.
|
||||
fn apply_opacity(&mut self, opacity: f32);
|
||||
|
||||
fn save_state(&mut self);
|
||||
fn restore_state(&mut self);
|
||||
|
||||
/// Returns the scale factor
|
||||
fn scale_factor(&self) -> f32;
|
||||
|
||||
/// Draw a pixmap in position indicated by the `pos`.
|
||||
/// The pixmap will be taken from cache if the cache is valid, otherwise, update_fn will be called
|
||||
/// with a callback that need to be called once with `fn (width, height, data)` where data are the
|
||||
/// RGBA premultiplied pixel values
|
||||
fn draw_cached_pixmap(
|
||||
&mut self,
|
||||
item_cache: &ItemRc,
|
||||
update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
|
||||
);
|
||||
|
||||
/// Draw the given string with the specified color at current (0, 0) with the default font. Mainly
|
||||
/// used by the performance counter overlay.
|
||||
fn draw_string(&mut self, string: &str, color: crate::Color);
|
||||
|
||||
fn draw_image_direct(&mut self, image: crate::graphics::Image);
|
||||
|
||||
/// This is called before it is being rendered (before the draw_* function).
|
||||
/// Returns
|
||||
/// - if the item needs to be drawn (false means it is clipped or doesn't need to be drawn)
|
||||
/// - the geometry of the item
|
||||
fn filter_item(
|
||||
&mut self,
|
||||
item: &ItemRc,
|
||||
window_adapter: &WindowAdapterRc,
|
||||
) -> (bool, LogicalRect) {
|
||||
let item_geometry = item.geometry();
|
||||
// Query bounding rect untracked, as properties that affect the bounding rect are already tracked
|
||||
// when rendering the item.
|
||||
let bounding_rect = crate::properties::evaluate_no_tracking(|| {
|
||||
item.bounding_rect(&item_geometry, window_adapter)
|
||||
});
|
||||
(self.get_current_clip().intersects(&bounding_rect), item_geometry)
|
||||
}
|
||||
|
||||
fn window(&self) -> &crate::window::WindowInner;
|
||||
|
||||
/// Return the internal renderer
|
||||
fn as_any(&mut self) -> Option<&mut dyn core::any::Any>;
|
||||
}
|
||||
|
||||
/// Helper trait to express the features of an item renderer.
|
||||
pub trait ItemRendererFeatures {
|
||||
/// The renderer supports applying 2D transformations to items.
|
||||
const SUPPORTS_TRANSFORMATIONS: bool;
|
||||
}
|
||||
+2863
File diff suppressed because it is too large.
Load diff
+1917
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,278 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains the builtin `ComponentContainer` and related items
|
||||
|
||||
When adding an item or a property, it needs to be kept in sync with different place.
|
||||
Lookup the [`crate::items`] module documentation.
|
||||
*/
|
||||
use super::{Item, ItemConsts, ItemRc, RenderingResult};
|
||||
use crate::Coord;
|
||||
use crate::component_factory::{ComponentFactory, FactoryContext};
|
||||
use crate::input::{
|
||||
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
|
||||
KeyEventResult, MouseEvent,
|
||||
};
|
||||
use crate::item_rendering::{CachedRenderingData, RenderRectangle};
|
||||
use crate::item_tree::{IndexRange, ItemTreeRc, ItemTreeWeak, ItemWeak};
|
||||
use crate::item_tree::{ItemTreeNode, ItemVisitorVTable, TraversalOrder, VisitChildrenResult};
|
||||
use crate::layout::{LayoutInfo, Orientation};
|
||||
use crate::lengths::{LogicalLength, LogicalRect, LogicalSize};
|
||||
use crate::properties::{Property, PropertyTracker};
|
||||
#[cfg(feature = "rtti")]
|
||||
use crate::rtti::*;
|
||||
use crate::window::WindowAdapter;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use const_field_offset::FieldOffsets;
|
||||
use core::cell::RefCell;
|
||||
use core::pin::Pin;
|
||||
use i_slint_core_macros::*;
|
||||
use once_cell::unsync::OnceCell;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement)]
|
||||
#[pin]
|
||||
/// The implementation of the `ComponentContainer` element
|
||||
pub struct ComponentContainer {
|
||||
pub width: Property<LogicalLength>,
|
||||
pub height: Property<LogicalLength>,
|
||||
pub component_factory: Property<ComponentFactory>,
|
||||
pub has_component: Property<bool>,
|
||||
|
||||
pub cached_rendering_data: CachedRenderingData,
|
||||
|
||||
component_tracker: OnceCell<Pin<Box<PropertyTracker>>>,
|
||||
item_tree: RefCell<Option<ItemTreeRc>>,
|
||||
|
||||
my_component: OnceCell<ItemTreeWeak>,
|
||||
embedding_item_tree_index: OnceCell<u32>,
|
||||
self_weak: OnceCell<ItemWeak>,
|
||||
}
|
||||
|
||||
impl ComponentContainer {
|
||||
pub fn ensure_updated(self: Pin<&Self>) {
|
||||
let factory = self
|
||||
.component_tracker
|
||||
.get()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.evaluate_if_dirty(|| self.component_factory());
|
||||
|
||||
let Some(factory) = factory else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut window = None;
|
||||
if let Some(parent) = self.my_component.get().and_then(|x| x.upgrade()) {
|
||||
vtable::VRc::borrow_pin(&parent).as_ref().window_adapter(false, &mut window);
|
||||
}
|
||||
let prevent_focus_change =
|
||||
window.as_ref().is_some_and(|w| w.window().0.prevent_focus_change.replace(true));
|
||||
|
||||
let factory_context = FactoryContext {
|
||||
parent_item_tree: self.my_component.get().unwrap().clone(),
|
||||
parent_item_tree_index: *self.embedding_item_tree_index.get().unwrap(),
|
||||
};
|
||||
|
||||
let product = factory.build(factory_context);
|
||||
|
||||
if let Some(w) = window {
|
||||
w.window().0.prevent_focus_change.set(prevent_focus_change);
|
||||
}
|
||||
|
||||
if let Some(item_tree) = product.clone() {
|
||||
let item_tree = vtable::VRc::borrow_pin(&item_tree);
|
||||
let root_item = item_tree.as_ref().get_item_ref(0);
|
||||
if let Some(window_item) =
|
||||
crate::items::ItemRef::downcast_pin::<crate::items::WindowItem>(root_item)
|
||||
{
|
||||
// Do _not_ use a two-way binding: That causes evaluations of width and height to
|
||||
// assert on recursive property evaluation.
|
||||
let weak = self.self_weak.get().unwrap().clone();
|
||||
window_item.width.set_binding(Box::new(move || {
|
||||
if let Some(self_rc) = weak.upgrade() {
|
||||
let self_pin = self_rc.borrow();
|
||||
if let Some(self_cc) = crate::items::ItemRef::downcast_pin::<Self>(self_pin)
|
||||
{
|
||||
return self_cc.width();
|
||||
}
|
||||
}
|
||||
Default::default()
|
||||
}));
|
||||
let weak = self.self_weak.get().unwrap().clone();
|
||||
window_item.height.set_binding(Box::new(move || {
|
||||
if let Some(self_rc) = weak.upgrade() {
|
||||
let self_pin = self_rc.borrow();
|
||||
if let Some(self_cc) = crate::items::ItemRef::downcast_pin::<Self>(self_pin)
|
||||
{
|
||||
return self_cc.height();
|
||||
}
|
||||
}
|
||||
Default::default()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
self.has_component.set(product.is_some());
|
||||
|
||||
self.item_tree.replace(product);
|
||||
}
|
||||
|
||||
pub fn subtree_range(self: Pin<&Self>) -> IndexRange {
|
||||
IndexRange { start: 0, end: if self.item_tree.borrow().is_some() { 1 } else { 0 } }
|
||||
}
|
||||
|
||||
pub fn subtree_component(self: Pin<&Self>) -> ItemTreeWeak {
|
||||
self.item_tree.borrow().as_ref().map_or(ItemTreeWeak::default(), vtable::VRc::downgrade)
|
||||
}
|
||||
|
||||
pub fn visit_children_item(
|
||||
self: Pin<&Self>,
|
||||
_index: isize,
|
||||
order: TraversalOrder,
|
||||
visitor: vtable::VRefMut<ItemVisitorVTable>,
|
||||
) -> VisitChildrenResult {
|
||||
let rc = self.item_tree.borrow().clone();
|
||||
if let Some(rc) = &rc {
|
||||
vtable::VRc::borrow_pin(rc).as_ref().visit_children_item(-1, order, visitor)
|
||||
} else {
|
||||
VisitChildrenResult::CONTINUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for ComponentContainer {
|
||||
fn init(self: Pin<&Self>, self_rc: &ItemRc) {
|
||||
let rc = self_rc.item_tree();
|
||||
|
||||
self.my_component.set(vtable::VRc::downgrade(rc)).ok().unwrap();
|
||||
|
||||
// Find my embedding item_tree_index:
|
||||
let pin_rc = vtable::VRc::borrow_pin(rc);
|
||||
let item_tree = pin_rc.as_ref().get_item_tree();
|
||||
let ItemTreeNode::Item { children_index, children_count, .. } =
|
||||
item_tree[self_rc.index() as usize]
|
||||
else {
|
||||
panic!("ComponentContainer not found in item tree");
|
||||
};
|
||||
|
||||
assert_eq!(children_count, 1);
|
||||
assert!(matches!(item_tree[children_index as usize], ItemTreeNode::DynamicTree { .. }));
|
||||
|
||||
self.embedding_item_tree_index.set(children_index).ok().unwrap();
|
||||
|
||||
self.component_tracker.set(Box::pin(PropertyTracker::default())).ok().unwrap();
|
||||
self.self_weak.set(self_rc.downgrade()).ok().unwrap();
|
||||
}
|
||||
|
||||
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
||||
|
||||
fn layout_info(
|
||||
self: Pin<&Self>,
|
||||
orientation: Orientation,
|
||||
_cross_axis_constraint: Coord,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> LayoutInfo {
|
||||
self.ensure_updated();
|
||||
if let Some(rc) = self.item_tree.borrow().clone() {
|
||||
vtable::VRc::borrow_pin(&rc).as_ref().layout_info(orientation)
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn input_event_filter_before_children(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventFilterResult {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
|
||||
fn input_event(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventResult {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn focus_event(
|
||||
self: Pin<&Self>,
|
||||
_: &FocusEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> FocusEventResult {
|
||||
FocusEventResult::FocusIgnored
|
||||
}
|
||||
|
||||
fn capture_key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn render(
|
||||
self: Pin<&Self>,
|
||||
backend: &mut super::ItemRendererRef,
|
||||
item_rc: &ItemRc,
|
||||
size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
backend.draw_rectangle(self, item_rc, size, &self.cached_rendering_data);
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
fn bounding_rect(
|
||||
self: core::pin::Pin<&Self>,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
geometry: LogicalRect,
|
||||
) -> LogicalRect {
|
||||
geometry
|
||||
}
|
||||
|
||||
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderRectangle for ComponentContainer {
|
||||
fn background(self: Pin<&Self>) -> crate::Brush {
|
||||
self.item_tree
|
||||
.borrow()
|
||||
.clone()
|
||||
.and_then(|item_tree| {
|
||||
let item_tree = vtable::VRc::borrow_pin(&item_tree);
|
||||
let root_item = item_tree.as_ref().get_item_ref(0);
|
||||
crate::items::ItemRef::downcast_pin::<crate::items::WindowItem>(root_item)
|
||||
.map(|window_item| window_item.background())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemConsts for ComponentContainer {
|
||||
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
||||
ComponentContainer,
|
||||
CachedRenderingData,
|
||||
> = ComponentContainer::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use super::{
|
||||
DropEvent, Item, ItemConsts, ItemRc, MouseCursor, PointerEventButton, RenderingResult,
|
||||
};
|
||||
use crate::Coord;
|
||||
use crate::input::{
|
||||
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
|
||||
KeyEventResult, MouseEvent,
|
||||
};
|
||||
use crate::item_rendering::{CachedRenderingData, ItemRenderer};
|
||||
use crate::layout::{LayoutInfo, Orientation};
|
||||
use crate::lengths::{LogicalPoint, LogicalRect, LogicalSize};
|
||||
#[cfg(feature = "rtti")]
|
||||
use crate::rtti::*;
|
||||
use crate::window::WindowAdapter;
|
||||
use crate::{Callback, Property, SharedString};
|
||||
use alloc::rc::Rc;
|
||||
use const_field_offset::FieldOffsets;
|
||||
use core::cell::Cell;
|
||||
use core::pin::Pin;
|
||||
use i_slint_core_macros::*;
|
||||
|
||||
pub type DropEventArg = (DropEvent,);
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement)]
|
||||
#[pin]
|
||||
/// The implementation of the `DragArea` element
|
||||
pub struct DragArea {
|
||||
pub enabled: Property<bool>,
|
||||
pub mime_type: Property<SharedString>,
|
||||
pub data: Property<SharedString>,
|
||||
pressed: Cell<bool>,
|
||||
pressed_position: Cell<LogicalPoint>,
|
||||
pub cached_rendering_data: CachedRenderingData,
|
||||
}
|
||||
|
||||
impl Item for DragArea {
|
||||
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
||||
|
||||
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
||||
|
||||
fn layout_info(
|
||||
self: Pin<&Self>,
|
||||
_: Orientation,
|
||||
_cross_axis_constraint: Coord,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> LayoutInfo {
|
||||
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
||||
}
|
||||
|
||||
fn input_event_filter_before_children(
|
||||
self: Pin<&Self>,
|
||||
event: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut MouseCursor,
|
||||
) -> InputEventFilterResult {
|
||||
if !self.enabled() {
|
||||
self.cancel();
|
||||
return InputEventFilterResult::ForwardAndIgnore;
|
||||
}
|
||||
|
||||
match event {
|
||||
MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
|
||||
self.pressed_position.set(*position);
|
||||
self.pressed.set(true);
|
||||
InputEventFilterResult::ForwardAndInterceptGrab
|
||||
}
|
||||
MouseEvent::Exit => {
|
||||
self.cancel();
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
MouseEvent::Released { button: PointerEventButton::Left, .. } => {
|
||||
self.pressed.set(false);
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
|
||||
MouseEvent::Moved { position, .. } => {
|
||||
if !self.pressed.get() {
|
||||
InputEventFilterResult::ForwardEvent
|
||||
} else {
|
||||
let pressed_pos = self.pressed_position.get();
|
||||
let dx = (position.x - pressed_pos.x).abs();
|
||||
let dy = (position.y - pressed_pos.y).abs();
|
||||
let threshold = super::flickable::DISTANCE_THRESHOLD.get();
|
||||
if dy > threshold || dx > threshold {
|
||||
InputEventFilterResult::Intercept
|
||||
} else {
|
||||
InputEventFilterResult::ForwardAndInterceptGrab
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseEvent::Wheel { .. } => InputEventFilterResult::ForwardAndIgnore,
|
||||
// Not the left button
|
||||
MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn input_event(
|
||||
self: Pin<&Self>,
|
||||
event: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut MouseCursor,
|
||||
) -> InputEventResult {
|
||||
match event {
|
||||
MouseEvent::Pressed { .. } => InputEventResult::EventAccepted,
|
||||
MouseEvent::Exit => {
|
||||
self.cancel();
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
MouseEvent::Released { .. } => {
|
||||
self.cancel();
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
MouseEvent::Moved { position, .. } => {
|
||||
if !self.pressed.get() || !self.enabled() {
|
||||
return InputEventResult::EventIgnored;
|
||||
}
|
||||
let pressed_pos = self.pressed_position.get();
|
||||
let dx = (position.x - pressed_pos.x).abs();
|
||||
let dy = (position.y - pressed_pos.y).abs();
|
||||
let threshold = super::flickable::DISTANCE_THRESHOLD.get();
|
||||
let start_drag = dx > threshold || dy > threshold;
|
||||
if start_drag {
|
||||
self.pressed.set(false);
|
||||
InputEventResult::StartDrag
|
||||
} else {
|
||||
InputEventResult::EventAccepted
|
||||
}
|
||||
}
|
||||
MouseEvent::Wheel { .. } => InputEventResult::EventIgnored,
|
||||
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn focus_event(
|
||||
self: Pin<&Self>,
|
||||
_: &FocusEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> FocusEventResult {
|
||||
FocusEventResult::FocusIgnored
|
||||
}
|
||||
|
||||
fn render(
|
||||
self: Pin<&Self>,
|
||||
_: &mut &mut dyn ItemRenderer,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
fn bounding_rect(
|
||||
self: core::pin::Pin<&Self>,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
mut geometry: LogicalRect,
|
||||
) -> LogicalRect {
|
||||
geometry.size = LogicalSize::zero();
|
||||
geometry
|
||||
}
|
||||
|
||||
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemConsts for DragArea {
|
||||
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
||||
DragArea,
|
||||
CachedRenderingData,
|
||||
> = DragArea::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
||||
}
|
||||
|
||||
impl DragArea {
|
||||
fn cancel(self: Pin<&Self>) {
|
||||
self.pressed.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement)]
|
||||
#[pin]
|
||||
/// The implementation of the `DropArea` element
|
||||
pub struct DropArea {
|
||||
pub enabled: Property<bool>,
|
||||
pub contains_drag: Property<bool>,
|
||||
pub can_drop: Callback<DropEventArg, bool>,
|
||||
pub dropped: Callback<DropEventArg>,
|
||||
|
||||
pub cached_rendering_data: CachedRenderingData,
|
||||
}
|
||||
|
||||
impl Item for DropArea {
|
||||
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
||||
|
||||
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
||||
|
||||
fn layout_info(
|
||||
self: Pin<&Self>,
|
||||
_: Orientation,
|
||||
_cross_axis_constraint: Coord,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> LayoutInfo {
|
||||
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
||||
}
|
||||
|
||||
fn input_event_filter_before_children(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut MouseCursor,
|
||||
) -> InputEventFilterResult {
|
||||
InputEventFilterResult::ForwardEvent
|
||||
}
|
||||
|
||||
fn input_event(
|
||||
self: Pin<&Self>,
|
||||
event: &MouseEvent,
|
||||
_: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
cursor: &mut MouseCursor,
|
||||
) -> InputEventResult {
|
||||
if !self.enabled() {
|
||||
return InputEventResult::EventIgnored;
|
||||
}
|
||||
match event {
|
||||
MouseEvent::DragMove(event) => {
|
||||
let r = Self::FIELD_OFFSETS.can_drop().apply_pin(self).call(&(event.clone(),));
|
||||
if r {
|
||||
self.contains_drag.set(true);
|
||||
*cursor = MouseCursor::Copy;
|
||||
InputEventResult::EventAccepted
|
||||
} else {
|
||||
self.contains_drag.set(false);
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
}
|
||||
MouseEvent::Drop(event) => {
|
||||
self.contains_drag.set(false);
|
||||
Self::FIELD_OFFSETS.dropped().apply_pin(self).call(&(event.clone(),));
|
||||
InputEventResult::EventAccepted
|
||||
}
|
||||
MouseEvent::Exit => {
|
||||
self.contains_drag.set(false);
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
_ => InputEventResult::EventIgnored,
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn focus_event(
|
||||
self: Pin<&Self>,
|
||||
_: &FocusEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> FocusEventResult {
|
||||
FocusEventResult::FocusIgnored
|
||||
}
|
||||
|
||||
fn render(
|
||||
self: Pin<&Self>,
|
||||
_: &mut &mut dyn ItemRenderer,
|
||||
_self_rc: &ItemRc,
|
||||
_size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
fn bounding_rect(
|
||||
self: core::pin::Pin<&Self>,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
mut geometry: LogicalRect,
|
||||
) -> LogicalRect {
|
||||
geometry.size = LogicalSize::zero();
|
||||
geometry
|
||||
}
|
||||
|
||||
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemConsts for DropArea {
|
||||
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
||||
DropArea,
|
||||
CachedRenderingData,
|
||||
> = DropArea::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
||||
}
|
||||
@@ -0,0 +1,829 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! The implementation details behind the Flickable
|
||||
|
||||
//! The `Flickable` item
|
||||
|
||||
use super::{
|
||||
Item, ItemConsts, ItemRc, ItemRendererRef, KeyEventResult, PointerEventButton, RenderingResult,
|
||||
VoidArg,
|
||||
};
|
||||
use crate::animations::Instant;
|
||||
use crate::animations::physics_simulation;
|
||||
use crate::input::InternalKeyEvent;
|
||||
use crate::input::{
|
||||
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, MouseEvent, TouchPhase,
|
||||
};
|
||||
use crate::item_rendering::CachedRenderingData;
|
||||
use crate::layout::{LayoutInfo, Orientation};
|
||||
use crate::lengths::{
|
||||
LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector,
|
||||
PointLengths, RectLengths,
|
||||
};
|
||||
#[cfg(feature = "rtti")]
|
||||
use crate::rtti::*;
|
||||
use crate::window::WindowAdapter;
|
||||
use crate::{Callback, Coord, Property};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use const_field_offset::FieldOffsets;
|
||||
use core::cell::RefCell;
|
||||
use core::pin::Pin;
|
||||
use core::time::Duration;
|
||||
#[allow(unused)]
|
||||
use euclid::num::Ceil;
|
||||
use euclid::num::Zero;
|
||||
use i_slint_core_macros::*;
|
||||
#[allow(unused)]
|
||||
use num_traits::Float;
|
||||
mod data_ringbuffer;
|
||||
use data_ringbuffer::PositionTimeRingBuffer;
|
||||
|
||||
/// Deceleration during the animation. It slows down the initial velocity of the simulation
|
||||
/// so that the simulation stops at some point if it didn't reach the limit
|
||||
/// The unit is: LogicalPixel/s^2
|
||||
const DECELERATION: f32 = 2000.;
|
||||
/// The maximum duration between a move and a release event to start an animation
|
||||
/// If the duration is larger than this value, no animation will be executed because
|
||||
/// it is not desired
|
||||
const MAX_DURATION: Duration = Duration::from_millis(100);
|
||||
|
||||
/// The implementation of the `Flickable` element
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement)]
|
||||
#[pin]
|
||||
pub struct Flickable {
|
||||
pub viewport_x: Property<LogicalLength>,
|
||||
pub viewport_y: Property<LogicalLength>,
|
||||
pub viewport_width: Property<LogicalLength>,
|
||||
pub viewport_height: Property<LogicalLength>,
|
||||
|
||||
pub interactive: Property<bool>,
|
||||
|
||||
pub flicked: Callback<VoidArg>,
|
||||
|
||||
data: FlickableDataBox,
|
||||
|
||||
/// FIXME: remove this
|
||||
pub cached_rendering_data: CachedRenderingData,
|
||||
}
|
||||
|
||||
impl Item for Flickable {
|
||||
fn init(self: Pin<&Self>, self_rc: &ItemRc) {
|
||||
self.data.in_bound_change_handler.init_delayed(
|
||||
self_rc.downgrade(),
|
||||
// Binding that returns if the Flickable is out of bounds:
|
||||
|self_weak| {
|
||||
let Some(flick_rc) = self_weak.upgrade() else {
|
||||
return (false, false);
|
||||
};
|
||||
let Some(flick) = flick_rc.downcast::<Flickable>() else {
|
||||
return (false, false);
|
||||
};
|
||||
let flick = flick.as_pin_ref();
|
||||
let geo = Self::geometry_without_virtual_keyboard(&flick_rc);
|
||||
|
||||
let zero = LogicalLength::zero();
|
||||
let vpx = flick.viewport_x();
|
||||
let vpy = flick.viewport_y();
|
||||
let x_out_of_bounds =
|
||||
vpx > zero || vpx < (geo.width_length() - flick.viewport_width()).min(zero);
|
||||
let y_out_of_bounds =
|
||||
vpy > zero || vpy < (geo.height_length() - flick.viewport_height()).min(zero);
|
||||
|
||||
(x_out_of_bounds, y_out_of_bounds)
|
||||
},
|
||||
// Change event handler that puts the Flickable in bounds if it's not already
|
||||
|self_weak, (x_out_of_bounds, y_out_of_bounds)| {
|
||||
let Some(flick_rc) = self_weak.upgrade() else { return };
|
||||
let Some(flick) = flick_rc.downcast::<Flickable>() else { return };
|
||||
let flick = flick.as_pin_ref();
|
||||
let vpx = flick.viewport_x();
|
||||
let vpy = flick.viewport_y();
|
||||
let p = ensure_in_bound(flick, LogicalPoint::from_lengths(vpx, vpy), &flick_rc);
|
||||
|
||||
let x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
|
||||
if *x_out_of_bounds && !x.has_binding() {
|
||||
x.set(p.x_length());
|
||||
}
|
||||
|
||||
let y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
|
||||
if *y_out_of_bounds && !y.has_binding() {
|
||||
y.set(p.y_length());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
||||
|
||||
fn layout_info(
|
||||
self: Pin<&Self>,
|
||||
_orientation: Orientation,
|
||||
_cross_axis_constraint: Coord,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> LayoutInfo {
|
||||
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
||||
}
|
||||
|
||||
fn input_event_filter_before_children(
|
||||
self: Pin<&Self>,
|
||||
event: &MouseEvent,
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventFilterResult {
|
||||
if let Some(pos) = event.position() {
|
||||
let geometry = Self::geometry_without_virtual_keyboard(self_rc);
|
||||
|
||||
if (pos.x < 0 as _
|
||||
|| pos.y < 0 as _
|
||||
|| pos.x_length() > geometry.width_length()
|
||||
|| pos.y_length() > geometry.height_length())
|
||||
&& self.data.inner.borrow().pressed_time.is_none()
|
||||
{
|
||||
return InputEventFilterResult::Intercept;
|
||||
}
|
||||
}
|
||||
if !self.interactive() && !matches!(event, MouseEvent::Wheel { .. }) {
|
||||
return InputEventFilterResult::ForwardAndIgnore;
|
||||
}
|
||||
self.data.handle_mouse_filter(self, event, window_adapter, self_rc)
|
||||
}
|
||||
|
||||
fn input_event(
|
||||
self: Pin<&Self>,
|
||||
event: &MouseEvent,
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventResult {
|
||||
if !self.interactive() && !matches!(event, MouseEvent::Wheel { .. }) {
|
||||
return InputEventResult::EventIgnored;
|
||||
}
|
||||
if let Some(pos) = event.position() {
|
||||
let geometry = Self::geometry_without_virtual_keyboard(self_rc);
|
||||
if matches!(event, MouseEvent::Wheel { .. } | MouseEvent::Pressed { .. })
|
||||
&& (pos.x < 0 as _
|
||||
|| pos.y < 0 as _
|
||||
|| pos.x_length() > geometry.width_length()
|
||||
|| pos.y_length() > geometry.height_length())
|
||||
{
|
||||
return InputEventResult::EventIgnored;
|
||||
}
|
||||
}
|
||||
|
||||
self.data.handle_mouse(self, event, window_adapter, self_rc)
|
||||
}
|
||||
|
||||
fn capture_key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn focus_event(
|
||||
self: Pin<&Self>,
|
||||
_: &FocusEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> FocusEventResult {
|
||||
FocusEventResult::FocusIgnored
|
||||
}
|
||||
|
||||
fn render(
|
||||
self: Pin<&Self>,
|
||||
backend: &mut ItemRendererRef,
|
||||
_self_rc: &ItemRc,
|
||||
size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
(*backend).combine_clip(
|
||||
LogicalRect::new(LogicalPoint::default(), size),
|
||||
LogicalBorderRadius::zero(),
|
||||
LogicalLength::zero(),
|
||||
);
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
fn bounding_rect(
|
||||
self: core::pin::Pin<&Self>,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
geometry: LogicalRect,
|
||||
) -> LogicalRect {
|
||||
geometry
|
||||
}
|
||||
|
||||
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemConsts for Flickable {
|
||||
const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
|
||||
Self::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
||||
}
|
||||
|
||||
impl Flickable {
|
||||
fn choose_min_move(
|
||||
current_view_start: Coord, // vx or vy
|
||||
view_len: Coord, // w or h
|
||||
content_len: Coord, // vw or vh
|
||||
points: impl Iterator<Item = Coord>,
|
||||
) -> Coord {
|
||||
// Feasible translations t such that for all p: vx+t <= p <= vx+t+w
|
||||
// -> t in [max_i(p_i - (vx + w)), min_i(p_i - vx)]
|
||||
let zero = 0 as Coord;
|
||||
let mut lower = Coord::MIN;
|
||||
let mut upper = Coord::MAX;
|
||||
|
||||
for p in points {
|
||||
lower = lower.max(p - (current_view_start + view_len));
|
||||
upper = upper.min(p - current_view_start);
|
||||
}
|
||||
|
||||
if lower > upper {
|
||||
// No translation can include all points simultaneously; pick nearest bound direction.
|
||||
// This happens only with NaNs; guard anyway.
|
||||
return zero;
|
||||
}
|
||||
|
||||
// Allowed translation interval due to scroll limits
|
||||
let max_scroll = (content_len - view_len).max(zero);
|
||||
let tmin = -current_view_start; // cannot scroll before 0
|
||||
let tmax = max_scroll - current_view_start; // cannot scroll past max
|
||||
|
||||
let i_min = lower.max(tmin);
|
||||
let i_max = upper.min(tmax);
|
||||
|
||||
if i_min <= i_max {
|
||||
if zero < i_min {
|
||||
i_min
|
||||
} else if zero > i_max {
|
||||
i_max
|
||||
} else {
|
||||
zero
|
||||
}
|
||||
// Intervals disjoint: choose closest allowed translation to feasible interval
|
||||
// either entirely left or right
|
||||
} else if tmax < lower {
|
||||
tmax
|
||||
} else {
|
||||
tmin
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll the Flickable so that all of the points are visible at the same time (if possible).
|
||||
/// The points have to be in the parent's coordinate space.
|
||||
pub(crate) fn reveal_points(self: Pin<&Self>, self_rc: &ItemRc, pts: &[LogicalPoint]) {
|
||||
if pts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// visible viewport size from base Item
|
||||
let geo = Self::geometry_without_virtual_keyboard(self_rc);
|
||||
|
||||
// content extents and current viewport origin (content coords)
|
||||
let vw = Self::FIELD_OFFSETS.viewport_width().apply_pin(self).get().0;
|
||||
let vh = Self::FIELD_OFFSETS.viewport_height().apply_pin(self).get().0;
|
||||
let vx = -Self::FIELD_OFFSETS.viewport_x().apply_pin(self).get().0;
|
||||
let vy = -Self::FIELD_OFFSETS.viewport_y().apply_pin(self).get().0;
|
||||
|
||||
// choose minimal translation along each axis
|
||||
let tx = Self::choose_min_move(vx, geo.width(), vw, pts.iter().map(|p| p.x));
|
||||
let ty = Self::choose_min_move(vy, geo.height(), vh, pts.iter().map(|p| p.y));
|
||||
|
||||
let new_vx = vx + tx;
|
||||
let new_vy = vy + ty;
|
||||
|
||||
Self::FIELD_OFFSETS.viewport_x().apply_pin(self).set(euclid::Length::new(-new_vx));
|
||||
Self::FIELD_OFFSETS.viewport_y().apply_pin(self).set(euclid::Length::new(-new_vy));
|
||||
}
|
||||
|
||||
fn geometry_without_virtual_keyboard(self_rc: &ItemRc) -> LogicalRect {
|
||||
let mut geometry = self_rc.geometry();
|
||||
|
||||
// subtract keyboard rect if needed
|
||||
if let Some(keyboard_rect) = self_rc.window_adapter().and_then(|window_adapter| {
|
||||
window_adapter.window().virtual_keyboard(crate::InternalToken)
|
||||
}) {
|
||||
let keyboard_top_left = self_rc.map_from_window(keyboard_rect.0.to_euclid());
|
||||
if keyboard_top_left.y > geometry.origin.y {
|
||||
geometry.size.height = keyboard_top_left.y - geometry.origin.y;
|
||||
}
|
||||
}
|
||||
geometry
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
/// Wraps the internal data structure for the Flickable
|
||||
pub struct FlickableDataBox(core::ptr::NonNull<FlickableData>);
|
||||
|
||||
impl Default for FlickableDataBox {
|
||||
fn default() -> Self {
|
||||
FlickableDataBox(Box::leak(Box::<FlickableData>::default()).into())
|
||||
}
|
||||
}
|
||||
impl Drop for FlickableDataBox {
|
||||
fn drop(&mut self) {
|
||||
// Safety: the self.0 was constructed from a Box::leak in FlickableDataBox::default
|
||||
drop(unsafe { Box::from_raw(self.0.as_ptr()) });
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Deref for FlickableDataBox {
|
||||
type Target = FlickableData;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
// Safety: initialized in FlickableDataBox::default
|
||||
unsafe { self.0.as_ref() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The distance required before it starts flicking if there is another item intercepting the mouse.
|
||||
pub(super) const DISTANCE_THRESHOLD: LogicalLength = LogicalLength::new(8 as _);
|
||||
/// Time required before we stop caring about child event if the mouse hasn't been moved
|
||||
pub(super) const DURATION_THRESHOLD: Duration = Duration::from_millis(500);
|
||||
/// The delay to which press are forwarded to the inner item
|
||||
pub(super) const FORWARD_DELAY: Duration = Duration::from_millis(100);
|
||||
/// Duration to filter scroll events from children after receiving a scroll event
|
||||
/// Note: This needs to be rather long, as that makes it more intuitive when scrolling with the
|
||||
/// mouse in concrete steps.
|
||||
/// The user can always override this by moving the mouse
|
||||
/// The value was tuned by hand, could be adjusted with further user feedback
|
||||
pub(super) const SCROLL_FILTER_DURATION: Duration = Duration::from_millis(800);
|
||||
/// Short duration for scroll event filtering, used when the end of the flickable is reached.
|
||||
pub(super) const SHORT_SCROLL_FILTER_DURATION: Duration =
|
||||
Duration::from_millis(SCROLL_FILTER_DURATION.as_millis() as u64 / 2);
|
||||
/// How far the user has to move the mouse to stop filtering scroll event from children after receiving a scroll event
|
||||
pub(super) const SCROLL_FILTER_DISTANCE_SQUARED: LogicalLength = LogicalLength::new(4 as _);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum CaptureEvents {
|
||||
MouseOrTouchScreen,
|
||||
MouseWheel,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct FlickableDataInner {
|
||||
/// The position in which the press was made
|
||||
pressed_pos: LogicalPoint,
|
||||
pressed_time: Option<Instant>,
|
||||
pressed_viewport_pos: LogicalPoint,
|
||||
pressed_viewport_size: LogicalSize,
|
||||
/// Set to true if the flickable is flicking and capturing all mouse event, not forwarding back to the children
|
||||
capture_events: Option<CaptureEvents>,
|
||||
/// Heuristics for filtering scroll events from children after we have scrolled ourselves.
|
||||
/// We want to filter those to prevent the case where the user scrolls with the mouse wheel,
|
||||
/// but the mouse now moves over a child item, and that item captures the scroll event.
|
||||
/// We use two heurstics: First, a timeout after we received a scroll event, and second, if the mouse moves we
|
||||
/// stop filtering scroll event until the next scroll event.
|
||||
last_scroll_event: Option<(Instant, LogicalPoint)>,
|
||||
|
||||
/// Ringbuffer to store the last move events. From those data the velocity can be
|
||||
/// calculated required for the animation after the release event
|
||||
position_time_rb: PositionTimeRingBuffer<5>,
|
||||
}
|
||||
|
||||
impl FlickableDataInner {
|
||||
fn should_capture_scroll(&self, timeout: Duration, position: LogicalPoint) -> bool {
|
||||
self.last_scroll_event.is_some_and(|(last_time, last_position)| {
|
||||
// Note: Squared length for MCU support, which use i32 coords.
|
||||
crate::animations::current_tick() - last_time < timeout
|
||||
&& LogicalLength::new((last_position - position).square_length().abs())
|
||||
< SCROLL_FILTER_DISTANCE_SQUARED
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the delta is a scroll in a orthogonal direction than what is allowed by the Flickable
|
||||
#[allow(clippy::nonminimal_bool)] // more readable this way
|
||||
fn is_allowed_scroll_direction(
|
||||
flick: Pin<&Flickable>,
|
||||
delta: LogicalVector,
|
||||
flick_rc: &ItemRc,
|
||||
) -> bool {
|
||||
let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
|
||||
!(delta.x == 0 as Coord && flick.viewport_height() <= geo.height_length())
|
||||
&& !(delta.y == 0 as Coord && flick.viewport_width() <= geo.width_length())
|
||||
}
|
||||
|
||||
fn process_wheel_event(
|
||||
&mut self,
|
||||
flick: Pin<&Flickable>,
|
||||
delta: LogicalVector,
|
||||
position: LogicalPoint,
|
||||
phase: TouchPhase,
|
||||
flick_rc: &ItemRc,
|
||||
) -> InputEventResult {
|
||||
let old_pos = LogicalPoint::from_lengths(
|
||||
(Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick).get(),
|
||||
(Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick).get(),
|
||||
);
|
||||
let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc);
|
||||
|
||||
let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
|
||||
let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
|
||||
let old_pos = (viewport_x.get(), viewport_y.get());
|
||||
|
||||
match phase {
|
||||
TouchPhase::Cancelled => {
|
||||
if !Self::is_allowed_scroll_direction(flick, delta, flick_rc) {
|
||||
// Release the capture immediately, this event is not meant for this Flickable.
|
||||
self.last_scroll_event = None;
|
||||
return InputEventResult::EventIgnored;
|
||||
}
|
||||
|
||||
viewport_x.set(new_pos.x_length());
|
||||
viewport_y.set(new_pos.y_length());
|
||||
self.last_scroll_event = Some((crate::animations::current_tick(), position));
|
||||
}
|
||||
TouchPhase::Started => {
|
||||
self.position_time_rb = PositionTimeRingBuffer::default();
|
||||
self.capture_events = Some(CaptureEvents::MouseWheel);
|
||||
self.last_scroll_event = Some((crate::animations::current_tick(), position));
|
||||
}
|
||||
TouchPhase::Moved => {
|
||||
if !Self::is_allowed_scroll_direction(flick, delta, flick_rc) {
|
||||
// Release the capture immediately, this event is not meant for this Flickable.
|
||||
self.last_scroll_event = None;
|
||||
return InputEventResult::EventIgnored;
|
||||
}
|
||||
|
||||
self.position_time_rb.push(crate::animations::current_tick(), new_pos);
|
||||
viewport_x.set(new_pos.x_length());
|
||||
viewport_y.set(new_pos.y_length());
|
||||
self.last_scroll_event = Some((crate::animations::current_tick(), position));
|
||||
}
|
||||
TouchPhase::Ended => {
|
||||
self.animate(flick, flick_rc);
|
||||
self.capture_events = None;
|
||||
}
|
||||
}
|
||||
|
||||
let flicked = old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length();
|
||||
if flicked {
|
||||
(Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&());
|
||||
InputEventResult::EventAccepted
|
||||
} else if self.should_capture_scroll(SHORT_SCROLL_FILTER_DURATION, position) {
|
||||
// After reaching the end, keep accepting the input event for a while longer, then time
|
||||
// out (by not updating the last_scroll_event)
|
||||
InputEventResult::EventAccepted
|
||||
} else {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
}
|
||||
|
||||
fn animate(&self, flick: Pin<&Flickable>, flick_rc: &ItemRc) {
|
||||
if let Some(last_time) = self.position_time_rb.last_time() {
|
||||
let (time, dist) = self.position_time_rb.diff();
|
||||
let millis = time.as_millis();
|
||||
|
||||
if self.capture_events.is_some()
|
||||
&& dist.square_length() > (DISTANCE_THRESHOLD.get() * DISTANCE_THRESHOLD.get()) as _
|
||||
&& millis > 0
|
||||
&& crate::animations::current_tick().duration_since(last_time) < MAX_DURATION
|
||||
{
|
||||
let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
|
||||
let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
|
||||
let vw = (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get();
|
||||
let vh = (Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get();
|
||||
let limit_x =
|
||||
if dist.x < 0 as Coord { -vw } else { euclid::Length::new(Coord::default()) };
|
||||
let limit_y =
|
||||
if dist.y < 0 as Coord { -vh } else { euclid::Length::new(Coord::default()) };
|
||||
|
||||
let limit =
|
||||
ensure_in_bound(flick, LogicalPoint::from_lengths(limit_x, limit_y), flick_rc);
|
||||
{
|
||||
let simulation = physics_simulation::ConstantDecelerationParameters::new(
|
||||
dist.x as f32 / (millis as f32 / 1000.),
|
||||
DECELERATION,
|
||||
);
|
||||
viewport_x.set_physic_animation_value(limit.x_length(), simulation);
|
||||
}
|
||||
|
||||
{
|
||||
let animation_y = physics_simulation::ConstantDecelerationParameters::new(
|
||||
dist.y as f32 / (millis as f32 / 1000.),
|
||||
DECELERATION,
|
||||
);
|
||||
viewport_y.set_physic_animation_value(limit.y_length(), animation_y);
|
||||
}
|
||||
|
||||
if dist.x != 0 as Coord || dist.y != 0 as Coord {
|
||||
(Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct FlickableData {
|
||||
inner: RefCell<FlickableDataInner>,
|
||||
/// Tracker that tracks the property to make sure that the flickable is in bounds
|
||||
in_bound_change_handler: crate::properties::ChangeTracker,
|
||||
}
|
||||
|
||||
impl FlickableData {
|
||||
fn scroll_delta(
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
delta_x: Coord,
|
||||
delta_y: Coord,
|
||||
) -> LogicalVector {
|
||||
if window_adapter.window().0.context().0.modifiers.get().shift()
|
||||
&& !cfg!(target_os = "macos")
|
||||
{
|
||||
// Shift invert coordinate for the purpose of scrolling.
|
||||
// But not on macOs because there the OS already take care of the change
|
||||
LogicalVector::new(delta_y, delta_x)
|
||||
} else {
|
||||
LogicalVector::new(delta_x, delta_y)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_filter(
|
||||
&self,
|
||||
flick: Pin<&Flickable>,
|
||||
event: &MouseEvent,
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
flick_rc: &ItemRc,
|
||||
) -> InputEventFilterResult {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
match event {
|
||||
MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
|
||||
inner.position_time_rb = PositionTimeRingBuffer::default();
|
||||
inner.pressed_pos = *position;
|
||||
inner.pressed_time = Some(crate::animations::current_tick());
|
||||
inner.pressed_viewport_pos = LogicalPoint::from_lengths(
|
||||
(Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick).get(),
|
||||
(Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick).get(),
|
||||
);
|
||||
inner.pressed_viewport_size = LogicalSize::from_lengths(
|
||||
(Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get(),
|
||||
(Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get(),
|
||||
);
|
||||
let x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
|
||||
x.set(x.get()); // Stop animation by removing the binding
|
||||
let y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
|
||||
y.set(y.get()); // Stop animation by removing the binding
|
||||
|
||||
if inner.capture_events.is_some() {
|
||||
InputEventFilterResult::Intercept
|
||||
} else {
|
||||
InputEventFilterResult::DelayForwarding(FORWARD_DELAY.as_millis() as _)
|
||||
}
|
||||
}
|
||||
MouseEvent::Exit | MouseEvent::Released { button: PointerEventButton::Left, .. } => {
|
||||
inner.pressed_time = None;
|
||||
if inner.capture_events.is_some() {
|
||||
InputEventFilterResult::Intercept
|
||||
} else {
|
||||
InputEventFilterResult::ForwardEvent
|
||||
}
|
||||
}
|
||||
MouseEvent::Moved { position, .. } => {
|
||||
let do_intercept = inner.capture_events.is_some()
|
||||
|| inner.pressed_time.is_some_and(|pressed_time| {
|
||||
if crate::animations::current_tick() - pressed_time > DURATION_THRESHOLD {
|
||||
return false;
|
||||
}
|
||||
// Check if the mouse was moved more than the DISTANCE_THRESHOLD in a
|
||||
// direction in which the flickable can flick
|
||||
let diff = *position - inner.pressed_pos;
|
||||
let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
|
||||
let w = geo.width_length();
|
||||
let h = geo.height_length();
|
||||
let vw = (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get();
|
||||
let vh =
|
||||
(Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get();
|
||||
let x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick).get();
|
||||
let y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick).get();
|
||||
let zero = LogicalLength::zero();
|
||||
((vw > w || x != zero) && abs(diff.x_length()) > DISTANCE_THRESHOLD)
|
||||
|| ((vh > h || y != zero) && abs(diff.y_length()) > DISTANCE_THRESHOLD)
|
||||
});
|
||||
if do_intercept {
|
||||
InputEventFilterResult::Intercept
|
||||
} else if inner.pressed_time.is_some() {
|
||||
InputEventFilterResult::ForwardAndInterceptGrab
|
||||
} else {
|
||||
InputEventFilterResult::ForwardEvent
|
||||
}
|
||||
}
|
||||
MouseEvent::Wheel { position, delta_x, delta_y, phase } => {
|
||||
match phase {
|
||||
TouchPhase::Cancelled => {
|
||||
// Qt sends the Cancelled Phase
|
||||
// If we recently handled a wheel event, intercept it to prevent children from grabbing
|
||||
// the scroll event
|
||||
let delta = Self::scroll_delta(window_adapter, *delta_x, *delta_y);
|
||||
if FlickableDataInner::is_allowed_scroll_direction(flick, delta, flick_rc)
|
||||
&& inner.should_capture_scroll(SCROLL_FILTER_DURATION, *position)
|
||||
{
|
||||
InputEventFilterResult::Intercept
|
||||
} else {
|
||||
inner.last_scroll_event = None;
|
||||
InputEventFilterResult::ForwardEvent
|
||||
}
|
||||
}
|
||||
TouchPhase::Started => InputEventFilterResult::Intercept,
|
||||
TouchPhase::Moved => {
|
||||
if inner.capture_events.is_some() {
|
||||
InputEventFilterResult::Intercept
|
||||
} else {
|
||||
// If we recently handled a wheel event, intercept it to prevent children from grabbing
|
||||
// the scroll event
|
||||
let delta = Self::scroll_delta(window_adapter, *delta_x, *delta_y);
|
||||
if FlickableDataInner::is_allowed_scroll_direction(
|
||||
flick, delta, flick_rc,
|
||||
) && inner.should_capture_scroll(SCROLL_FILTER_DURATION, *position)
|
||||
{
|
||||
InputEventFilterResult::Intercept
|
||||
} else {
|
||||
inner.last_scroll_event = None;
|
||||
InputEventFilterResult::ForwardEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
TouchPhase::Ended => {
|
||||
if inner.capture_events.is_some() {
|
||||
InputEventFilterResult::Intercept
|
||||
} else {
|
||||
InputEventFilterResult::ForwardEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Not the left button
|
||||
MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
|
||||
InputEventFilterResult::ForwardEvent
|
||||
}
|
||||
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse(
|
||||
&self,
|
||||
flick: Pin<&Flickable>,
|
||||
event: &MouseEvent,
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
flick_rc: &ItemRc,
|
||||
) -> InputEventResult {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
match event {
|
||||
MouseEvent::Pressed { .. } => {
|
||||
inner.capture_events = Some(CaptureEvents::MouseOrTouchScreen);
|
||||
InputEventResult::GrabMouse
|
||||
}
|
||||
MouseEvent::Exit | MouseEvent::Released { .. } => {
|
||||
if inner.capture_events.is_some_and(|f| f == CaptureEvents::MouseOrTouchScreen) {
|
||||
let was_capturing = true;
|
||||
inner.animate(flick, flick_rc);
|
||||
inner.capture_events = None;
|
||||
inner.pressed_time = None;
|
||||
if was_capturing {
|
||||
InputEventResult::EventAccepted
|
||||
} else {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
} else if inner.capture_events.is_none() {
|
||||
inner.pressed_time = None;
|
||||
InputEventResult::EventIgnored
|
||||
} else {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
}
|
||||
MouseEvent::Moved { position, .. } => {
|
||||
if inner.pressed_time.is_some() {
|
||||
inner.position_time_rb.push(crate::animations::current_tick(), *position);
|
||||
let current_viewport_size = LogicalSize::from_lengths(
|
||||
(Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get(),
|
||||
(Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get(),
|
||||
);
|
||||
|
||||
// Update reference points when the size of the viewport changes to
|
||||
// avoid 'jumping' during scrolling.
|
||||
// This happens when the height estimate of a ListView changes after
|
||||
// new items are loaded.
|
||||
if current_viewport_size != inner.pressed_viewport_size {
|
||||
inner.pressed_viewport_size = current_viewport_size;
|
||||
|
||||
inner.pressed_viewport_pos = LogicalPoint::from_lengths(
|
||||
(Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick).get(),
|
||||
(Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick).get(),
|
||||
);
|
||||
|
||||
inner.pressed_pos = *position;
|
||||
};
|
||||
|
||||
let new_pos = inner.pressed_viewport_pos + (*position - inner.pressed_pos);
|
||||
|
||||
let x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick);
|
||||
let y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick);
|
||||
let should_capture = || {
|
||||
let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
|
||||
let w = geo.width_length();
|
||||
let h = geo.height_length();
|
||||
let vw = (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get();
|
||||
let vh =
|
||||
(Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get();
|
||||
let zero = LogicalLength::zero();
|
||||
((vw > w || x.get() != zero)
|
||||
&& abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD)
|
||||
|| ((vh > h || y.get() != zero)
|
||||
&& abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD)
|
||||
};
|
||||
|
||||
if inner.capture_events.is_some_and(|f| f == CaptureEvents::MouseOrTouchScreen)
|
||||
|| should_capture()
|
||||
{
|
||||
let new_pos = ensure_in_bound(flick, new_pos, flick_rc);
|
||||
|
||||
let old_pos = (x.get(), y.get());
|
||||
x.set(new_pos.x_length());
|
||||
y.set(new_pos.y_length());
|
||||
if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() {
|
||||
(Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&());
|
||||
}
|
||||
|
||||
inner.capture_events = Some(CaptureEvents::MouseOrTouchScreen);
|
||||
InputEventResult::GrabMouse
|
||||
} else if abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD
|
||||
|| abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD
|
||||
{
|
||||
// drag in a unsupported direction gives up the grab
|
||||
InputEventResult::EventIgnored
|
||||
} else {
|
||||
InputEventResult::EventAccepted
|
||||
}
|
||||
} else {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
}
|
||||
MouseEvent::Wheel { delta_x, delta_y, position, phase } => {
|
||||
let delta = Self::scroll_delta(window_adapter, *delta_x, *delta_y);
|
||||
inner.process_wheel_event(flick, delta, *position, *phase, flick_rc)
|
||||
}
|
||||
MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
MouseEvent::DragMove(..) | MouseEvent::Drop(..) => InputEventResult::EventIgnored,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn abs(l: LogicalLength) -> LogicalLength {
|
||||
LogicalLength::new(l.get().abs())
|
||||
}
|
||||
|
||||
/// Make sure that the point is within the bounds
|
||||
fn ensure_in_bound(flick: Pin<&Flickable>, p: LogicalPoint, flick_rc: &ItemRc) -> LogicalPoint {
|
||||
let geo = Flickable::geometry_without_virtual_keyboard(flick_rc);
|
||||
let w = geo.width_length();
|
||||
let h = geo.height_length();
|
||||
let vw = (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get();
|
||||
let vh = (Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get();
|
||||
|
||||
let min = LogicalPoint::from_lengths(w - vw, h - vh);
|
||||
let max = LogicalPoint::default();
|
||||
p.max(min).min(max)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
|
||||
/// hold a FlickableDataBox
|
||||
#[cfg(feature = "ffi")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_flickable_data_init(data: *mut FlickableDataBox) {
|
||||
unsafe { core::ptr::write(data, FlickableDataBox::default()) };
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// This must be called using a non-null pointer pointing to an initialized FlickableDataBox
|
||||
#[cfg(feature = "ffi")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_flickable_data_free(data: *mut FlickableDataBox) {
|
||||
unsafe {
|
||||
core::ptr::drop_in_place(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! This module contains a simple ringbuffer to store time and location tuples. It is used in the flickable to
|
||||
//! determine the initial velocity of the animation
|
||||
|
||||
use crate::Coord;
|
||||
use crate::animations::Instant;
|
||||
use crate::lengths::LogicalPoint;
|
||||
use crate::lengths::LogicalPx;
|
||||
use core::time::Duration;
|
||||
use euclid::Vector2D;
|
||||
|
||||
/// Simple ringbuffer storing time and position tuples
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PositionTimeRingBuffer<const N: usize> {
|
||||
/// Pointing to the next free element
|
||||
curr_index: usize,
|
||||
/// Indicates if the buffer is full
|
||||
full: bool,
|
||||
values: [(Instant, LogicalPoint); N],
|
||||
}
|
||||
|
||||
impl<const N: usize> Default for PositionTimeRingBuffer<N> {
|
||||
fn default() -> Self {
|
||||
Self { curr_index: 0, full: false, values: [(Instant::now(), LogicalPoint::default()); N] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> PositionTimeRingBuffer<N> {
|
||||
/// Indicates if the buffer is empty
|
||||
pub fn empty(&self) -> bool {
|
||||
!(self.full || self.curr_index > 0)
|
||||
}
|
||||
|
||||
/// Add a new element to the ringbuffer
|
||||
pub fn push(&mut self, time: Instant, value: LogicalPoint) {
|
||||
if self.curr_index < self.values.len() {
|
||||
self.values[self.curr_index] = (time, value);
|
||||
}
|
||||
self.curr_index += 1;
|
||||
if self.curr_index >= N {
|
||||
self.full = true;
|
||||
self.curr_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Index of the most recent added value
|
||||
fn latest_index(&self) -> usize {
|
||||
if self.curr_index > 0 { self.curr_index - 1 } else { N - 1 }
|
||||
}
|
||||
|
||||
/// Returns the last time value added to the buffer if not empty otherwise None
|
||||
pub fn last_time(&self) -> Option<Instant> {
|
||||
if !self.empty() { Some(self.values[self.latest_index()].0) } else { None }
|
||||
}
|
||||
|
||||
/// Returns the difference between the oldest and the newest point
|
||||
pub fn diff(&self) -> (Duration, Vector2D<Coord, LogicalPx>) {
|
||||
if self.full {
|
||||
let oldest = self.values[self.curr_index];
|
||||
let newest = if self.curr_index > 0 {
|
||||
self.values[self.curr_index - 1]
|
||||
} else {
|
||||
self.values[self.values.len() - 1]
|
||||
};
|
||||
(newest.0.duration_since(oldest.0), newest.1 - oldest.1)
|
||||
} else {
|
||||
let oldest = self.values[0];
|
||||
let newest = self.values[usize::max(0, self.curr_index - 1)];
|
||||
(newest.0.duration_since(oldest.0), newest.1 - oldest.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::animations::Instant;
|
||||
use crate::lengths::LogicalPoint;
|
||||
use core::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_empty_buffer() {
|
||||
let buffer: PositionTimeRingBuffer<5> = PositionTimeRingBuffer::default();
|
||||
assert!(buffer.empty());
|
||||
assert_eq!(buffer.curr_index, 0);
|
||||
assert!(!buffer.full);
|
||||
assert_eq!(buffer.last_time(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_single_element() {
|
||||
let mut buffer: PositionTimeRingBuffer<5> = PositionTimeRingBuffer::default();
|
||||
let time = Instant::now();
|
||||
let point = LogicalPoint::new(10.0, 20.0);
|
||||
|
||||
buffer.push(time, point);
|
||||
|
||||
assert!(!buffer.empty());
|
||||
assert_eq!(buffer.curr_index, 1);
|
||||
assert!(!buffer.full);
|
||||
assert_eq!(buffer.latest_index(), 0);
|
||||
assert_eq!(buffer.last_time(), Some(time));
|
||||
|
||||
assert_eq!(buffer.diff(), (Duration::from_millis(0), Vector2D::new(0., 0.)));
|
||||
}
|
||||
|
||||
/// Buffer not complete full
|
||||
#[test]
|
||||
fn test_push_two_elements() {
|
||||
let mut buffer: PositionTimeRingBuffer<5> = PositionTimeRingBuffer::default();
|
||||
let time = Instant::now();
|
||||
|
||||
buffer.push(time, LogicalPoint::new(10.0, 20.0));
|
||||
buffer.push(time + Duration::from_millis(13), LogicalPoint::new(13.0, -5.0));
|
||||
|
||||
assert!(!buffer.empty());
|
||||
assert_eq!(buffer.curr_index, 2);
|
||||
assert!(!buffer.full);
|
||||
assert_eq!(buffer.latest_index(), 1);
|
||||
assert_eq!(buffer.last_time(), Some(time + Duration::from_millis(13)));
|
||||
|
||||
assert_eq!(buffer.diff(), (Duration::from_millis(13), Vector2D::new(3., -25.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_until_full() {
|
||||
let mut buffer: PositionTimeRingBuffer<5> = PositionTimeRingBuffer::default();
|
||||
let base_time = Instant::now();
|
||||
|
||||
// Push elements to fill the buffer
|
||||
for i in 0..5 {
|
||||
let time = base_time + Duration::from_millis(i * 3 as u64);
|
||||
let point = LogicalPoint::new(i as f32, -2. * i as f32);
|
||||
buffer.push(time, point);
|
||||
}
|
||||
|
||||
assert!(!buffer.empty());
|
||||
assert_eq!(buffer.curr_index, 0);
|
||||
assert!(buffer.full);
|
||||
assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(4 * 3)));
|
||||
assert_eq!(buffer.latest_index(), 4);
|
||||
|
||||
assert_eq!(buffer.diff(), (Duration::from_millis(12), Vector2D::new(4., -8.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_beyond_capacity() {
|
||||
const CAP: usize = 5;
|
||||
let mut buffer: PositionTimeRingBuffer<CAP> = PositionTimeRingBuffer::default();
|
||||
let base_time = Instant::now();
|
||||
|
||||
// Push more than capacity
|
||||
for i in 0..(CAP + 2) {
|
||||
let time = base_time + Duration::from_millis(i as u64);
|
||||
let point = LogicalPoint::new(i as f32, i as f32 * 2. + 100.);
|
||||
buffer.push(time, point);
|
||||
}
|
||||
|
||||
assert!(!buffer.empty());
|
||||
assert!(buffer.full);
|
||||
assert_eq!(buffer.curr_index, 2);
|
||||
assert_eq!(buffer.latest_index(), 1);
|
||||
assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(6)));
|
||||
|
||||
assert_eq!(buffer.diff(), (Duration::from_millis(4), Vector2D::new(4., 4. * 2.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_beyond_capacity_wrap_back() {
|
||||
const CAP: usize = 5;
|
||||
let mut buffer: PositionTimeRingBuffer<CAP> = PositionTimeRingBuffer::default();
|
||||
let base_time = Instant::now();
|
||||
|
||||
// Push more than capacity
|
||||
for i in 0..CAP {
|
||||
let time = base_time + Duration::from_millis(i as u64);
|
||||
let point = LogicalPoint::new(i as f32 * 3., i as f32 * -2. + 100.);
|
||||
buffer.push(time, point);
|
||||
}
|
||||
|
||||
assert!(!buffer.empty());
|
||||
assert!(buffer.full);
|
||||
assert_eq!(buffer.curr_index, 0);
|
||||
assert_eq!(buffer.latest_index(), CAP - 1);
|
||||
assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(4)));
|
||||
|
||||
// Wrapping back must be done
|
||||
assert_eq!(buffer.diff(), (Duration::from_millis(4), Vector2D::new(4. * 3., 4. * -2.)));
|
||||
}
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains the builtin image related items.
|
||||
|
||||
When adding an item or a property, it needs to be kept in sync with different place.
|
||||
Lookup the [`crate::items`] module documentation.
|
||||
*/
|
||||
use super::{
|
||||
ImageFit, ImageHorizontalAlignment, ImageRendering, ImageTiling, ImageVerticalAlignment, Item,
|
||||
ItemConsts, ItemRc, RenderingResult,
|
||||
};
|
||||
use crate::input::{
|
||||
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
|
||||
KeyEventResult, MouseEvent,
|
||||
};
|
||||
use crate::item_rendering::ItemRenderer;
|
||||
use crate::item_rendering::{CachedRenderingData, RenderImage};
|
||||
use crate::layout::{LayoutInfo, Orientation};
|
||||
use crate::lengths::{LogicalLength, LogicalRect, LogicalSize};
|
||||
#[cfg(feature = "rtti")]
|
||||
use crate::rtti::*;
|
||||
use crate::window::WindowAdapter;
|
||||
use crate::{Brush, Coord, Property};
|
||||
use alloc::rc::Rc;
|
||||
use const_field_offset::FieldOffsets;
|
||||
use core::pin::Pin;
|
||||
use i_slint_core_macros::*;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement)]
|
||||
#[pin]
|
||||
/// The implementation of the `Image` element
|
||||
pub struct ImageItem {
|
||||
pub source: Property<crate::graphics::Image>,
|
||||
pub width: Property<LogicalLength>,
|
||||
pub height: Property<LogicalLength>,
|
||||
pub image_fit: Property<ImageFit>,
|
||||
pub image_rendering: Property<ImageRendering>,
|
||||
pub colorize: Property<Brush>,
|
||||
pub cached_rendering_data: CachedRenderingData,
|
||||
}
|
||||
|
||||
impl Item for ImageItem {
|
||||
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
||||
|
||||
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
||||
|
||||
fn layout_info(
|
||||
self: Pin<&Self>,
|
||||
orientation: Orientation,
|
||||
cross_axis_constraint: Coord,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> LayoutInfo {
|
||||
let natural_size = self.source().size();
|
||||
LayoutInfo {
|
||||
preferred: match orientation {
|
||||
_ if natural_size.width == 0 || natural_size.height == 0 => 0 as Coord,
|
||||
Orientation::Horizontal => natural_size.width as Coord,
|
||||
Orientation::Vertical => {
|
||||
let w = if cross_axis_constraint >= 0 as Coord {
|
||||
cross_axis_constraint
|
||||
} else {
|
||||
self.width().get()
|
||||
};
|
||||
natural_size.height as Coord * w / natural_size.width as Coord
|
||||
}
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn input_event_filter_before_children(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventFilterResult {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
|
||||
fn input_event(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventResult {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn capture_key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn focus_event(
|
||||
self: Pin<&Self>,
|
||||
_: &FocusEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> FocusEventResult {
|
||||
FocusEventResult::FocusIgnored
|
||||
}
|
||||
|
||||
fn render(
|
||||
self: Pin<&Self>,
|
||||
backend: &mut &mut dyn ItemRenderer,
|
||||
self_rc: &ItemRc,
|
||||
size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
(*backend).draw_image(self, self_rc, size, &self.cached_rendering_data);
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
fn bounding_rect(
|
||||
self: core::pin::Pin<&Self>,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
geometry: LogicalRect,
|
||||
) -> LogicalRect {
|
||||
geometry
|
||||
}
|
||||
|
||||
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderImage for ImageItem {
|
||||
fn target_size(self: Pin<&Self>) -> LogicalSize {
|
||||
LogicalSize::from_lengths(self.width(), self.height())
|
||||
}
|
||||
|
||||
fn source(self: Pin<&Self>) -> crate::graphics::Image {
|
||||
self.source()
|
||||
}
|
||||
|
||||
fn source_clip(self: Pin<&Self>) -> Option<crate::graphics::IntRect> {
|
||||
None
|
||||
}
|
||||
|
||||
fn image_fit(self: Pin<&Self>) -> ImageFit {
|
||||
self.image_fit()
|
||||
}
|
||||
|
||||
fn rendering(self: Pin<&Self>) -> ImageRendering {
|
||||
self.image_rendering()
|
||||
}
|
||||
|
||||
fn colorize(self: Pin<&Self>) -> Brush {
|
||||
self.colorize()
|
||||
}
|
||||
|
||||
fn alignment(self: Pin<&Self>) -> (ImageHorizontalAlignment, ImageVerticalAlignment) {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn tiling(self: Pin<&Self>) -> (ImageTiling, ImageTiling) {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemConsts for ImageItem {
|
||||
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
||||
ImageItem,
|
||||
CachedRenderingData,
|
||||
> = ImageItem::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement)]
|
||||
#[pin]
|
||||
/// The implementation of the `ClippedImage` element
|
||||
pub struct ClippedImage {
|
||||
pub source: Property<crate::graphics::Image>,
|
||||
pub width: Property<LogicalLength>,
|
||||
pub height: Property<LogicalLength>,
|
||||
pub image_fit: Property<ImageFit>,
|
||||
pub image_rendering: Property<ImageRendering>,
|
||||
pub colorize: Property<Brush>,
|
||||
pub source_clip_x: Property<i32>,
|
||||
pub source_clip_y: Property<i32>,
|
||||
pub source_clip_width: Property<i32>,
|
||||
pub source_clip_height: Property<i32>,
|
||||
|
||||
pub horizontal_alignment: Property<ImageHorizontalAlignment>,
|
||||
pub vertical_alignment: Property<ImageVerticalAlignment>,
|
||||
pub horizontal_tiling: Property<ImageTiling>,
|
||||
pub vertical_tiling: Property<ImageTiling>,
|
||||
|
||||
pub cached_rendering_data: CachedRenderingData,
|
||||
}
|
||||
|
||||
impl Item for ClippedImage {
|
||||
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
||||
|
||||
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
||||
|
||||
fn layout_info(
|
||||
self: Pin<&Self>,
|
||||
orientation: Orientation,
|
||||
cross_axis_constraint: Coord,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> LayoutInfo {
|
||||
LayoutInfo {
|
||||
preferred: match orientation {
|
||||
Orientation::Horizontal => self.source_clip_width() as Coord,
|
||||
Orientation::Vertical => {
|
||||
let source_clip_width = self.source_clip_width();
|
||||
if source_clip_width == 0 {
|
||||
0 as Coord
|
||||
} else {
|
||||
let w = if cross_axis_constraint >= 0 as Coord {
|
||||
cross_axis_constraint
|
||||
} else {
|
||||
self.width().get()
|
||||
};
|
||||
self.source_clip_height() as Coord * w / source_clip_width as Coord
|
||||
}
|
||||
}
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn input_event_filter_before_children(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventFilterResult {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
|
||||
fn input_event(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventResult {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn capture_key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn focus_event(
|
||||
self: Pin<&Self>,
|
||||
_: &FocusEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> FocusEventResult {
|
||||
FocusEventResult::FocusIgnored
|
||||
}
|
||||
|
||||
fn render(
|
||||
self: Pin<&Self>,
|
||||
backend: &mut &mut dyn ItemRenderer,
|
||||
self_rc: &ItemRc,
|
||||
size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
(*backend).draw_image(self, self_rc, size, &self.cached_rendering_data);
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
fn bounding_rect(
|
||||
self: core::pin::Pin<&Self>,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
geometry: LogicalRect,
|
||||
) -> LogicalRect {
|
||||
geometry
|
||||
}
|
||||
|
||||
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderImage for ClippedImage {
|
||||
fn target_size(self: Pin<&Self>) -> LogicalSize {
|
||||
LogicalSize::from_lengths(self.width(), self.height())
|
||||
}
|
||||
|
||||
fn source(self: Pin<&Self>) -> crate::graphics::Image {
|
||||
self.source()
|
||||
}
|
||||
|
||||
fn source_clip(self: Pin<&Self>) -> Option<crate::graphics::IntRect> {
|
||||
Some(euclid::rect(
|
||||
self.source_clip_x(),
|
||||
self.source_clip_y(),
|
||||
self.source_clip_width(),
|
||||
self.source_clip_height(),
|
||||
))
|
||||
}
|
||||
|
||||
fn image_fit(self: Pin<&Self>) -> ImageFit {
|
||||
self.image_fit()
|
||||
}
|
||||
|
||||
fn rendering(self: Pin<&Self>) -> ImageRendering {
|
||||
self.image_rendering()
|
||||
}
|
||||
|
||||
fn colorize(self: Pin<&Self>) -> Brush {
|
||||
self.colorize()
|
||||
}
|
||||
|
||||
fn alignment(self: Pin<&Self>) -> (ImageHorizontalAlignment, ImageVerticalAlignment) {
|
||||
(self.horizontal_alignment(), self.vertical_alignment())
|
||||
}
|
||||
|
||||
fn tiling(self: Pin<&Self>) -> (ImageTiling, ImageTiling) {
|
||||
(self.horizontal_tiling(), self.vertical_tiling())
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemConsts for ClippedImage {
|
||||
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
||||
ClippedImage,
|
||||
CachedRenderingData,
|
||||
> = ClippedImage::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
||||
}
|
||||
+1201
File diff suppressed because it is too large.
Load diff
+199
@@ -0,0 +1,199 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module contains the builtin Path related items.
|
||||
|
||||
When adding an item or a property, it needs to be kept in sync with different place.
|
||||
Lookup the [`crate::items`] module documentation.
|
||||
*/
|
||||
|
||||
use super::{
|
||||
FillRule, Item, ItemConsts, ItemRc, ItemRendererRef, LineCap, LineJoin, RenderingResult,
|
||||
};
|
||||
use crate::graphics::{Brush, PathData, PathDataIterator};
|
||||
use crate::input::{
|
||||
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
|
||||
KeyEventResult, MouseEvent,
|
||||
};
|
||||
use crate::item_rendering::CachedRenderingData;
|
||||
|
||||
use crate::items::ImageFit;
|
||||
use crate::layout::{LayoutInfo, Orientation};
|
||||
use crate::lengths::{
|
||||
LogicalBorderRadius, LogicalLength, LogicalRect, LogicalSize, LogicalVector, RectLengths,
|
||||
};
|
||||
#[cfg(feature = "rtti")]
|
||||
use crate::rtti::*;
|
||||
use crate::window::WindowAdapter;
|
||||
use crate::{Coord, Property};
|
||||
use alloc::rc::Rc;
|
||||
use const_field_offset::FieldOffsets;
|
||||
use core::pin::Pin;
|
||||
use euclid::num::Zero;
|
||||
use i_slint_core_macros::*;
|
||||
|
||||
/// The implementation of the `Path` element
|
||||
#[repr(C)]
|
||||
#[derive(FieldOffsets, Default, SlintElement)]
|
||||
#[pin]
|
||||
pub struct Path {
|
||||
pub elements: Property<PathData>,
|
||||
pub fill: Property<Brush>,
|
||||
pub fill_rule: Property<FillRule>,
|
||||
pub stroke: Property<Brush>,
|
||||
pub stroke_width: Property<LogicalLength>,
|
||||
pub stroke_line_cap: Property<LineCap>,
|
||||
pub stroke_line_join: Property<LineJoin>,
|
||||
pub stroke_miter_limit: Property<f32>,
|
||||
pub viewbox_x: Property<f32>,
|
||||
pub viewbox_y: Property<f32>,
|
||||
pub viewbox_width: Property<f32>,
|
||||
pub viewbox_height: Property<f32>,
|
||||
pub fit: Property<ImageFit>,
|
||||
pub clip: Property<bool>,
|
||||
pub anti_alias: Property<bool>,
|
||||
pub cached_rendering_data: CachedRenderingData,
|
||||
}
|
||||
|
||||
impl Item for Path {
|
||||
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
||||
|
||||
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
||||
|
||||
fn layout_info(
|
||||
self: Pin<&Self>,
|
||||
_orientation: Orientation,
|
||||
_cross_axis_constraint: Coord,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> LayoutInfo {
|
||||
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
||||
}
|
||||
|
||||
fn input_event_filter_before_children(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventFilterResult {
|
||||
InputEventFilterResult::ForwardAndIgnore
|
||||
}
|
||||
|
||||
fn input_event(
|
||||
self: Pin<&Self>,
|
||||
_: &MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut super::MouseCursor,
|
||||
) -> InputEventResult {
|
||||
InputEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn capture_key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> KeyEventResult {
|
||||
KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn focus_event(
|
||||
self: Pin<&Self>,
|
||||
_: &FocusEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> FocusEventResult {
|
||||
FocusEventResult::FocusIgnored
|
||||
}
|
||||
|
||||
fn render(
|
||||
self: Pin<&Self>,
|
||||
backend: &mut ItemRendererRef,
|
||||
self_rc: &ItemRc,
|
||||
size: LogicalSize,
|
||||
) -> RenderingResult {
|
||||
let clip = self.clip();
|
||||
if clip {
|
||||
(*backend).save_state();
|
||||
(*backend).combine_clip(
|
||||
size.into(),
|
||||
LogicalBorderRadius::zero(),
|
||||
LogicalLength::zero(),
|
||||
);
|
||||
}
|
||||
(*backend).draw_path(self, self_rc, size);
|
||||
if clip {
|
||||
(*backend).restore_state();
|
||||
}
|
||||
RenderingResult::ContinueRenderingChildren
|
||||
}
|
||||
|
||||
fn bounding_rect(
|
||||
self: core::pin::Pin<&Self>,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
geometry: LogicalRect,
|
||||
) -> LogicalRect {
|
||||
geometry
|
||||
}
|
||||
|
||||
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Path {
|
||||
/// Returns an iterator of the events of the path and an offset, so that the
|
||||
/// shape fits into the width/height of the path while respecting the stroke
|
||||
/// width.
|
||||
pub fn fitted_path_events(
|
||||
self: Pin<&Self>,
|
||||
self_rc: &ItemRc,
|
||||
) -> Option<(LogicalVector, PathDataIterator)> {
|
||||
let mut elements_iter = self.elements().iter()?;
|
||||
|
||||
let stroke_width = self.stroke_width();
|
||||
let geometry = self_rc.geometry();
|
||||
let bounds_width = (geometry.width_length() - stroke_width).max(LogicalLength::zero());
|
||||
let bounds_height = (geometry.height_length() - stroke_width).max(LogicalLength::zero());
|
||||
let offset =
|
||||
LogicalVector::from_lengths(stroke_width / 2 as Coord, stroke_width / 2 as Coord);
|
||||
|
||||
let viewbox_width = self.viewbox_width();
|
||||
let viewbox_height = self.viewbox_height();
|
||||
|
||||
let maybe_viewbox = if viewbox_width > 0. && viewbox_height > 0. {
|
||||
Some(
|
||||
euclid::rect(self.viewbox_x(), self.viewbox_y(), viewbox_width, viewbox_height)
|
||||
.to_box2d(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
elements_iter.fit(
|
||||
bounds_width.get() as _,
|
||||
bounds_height.get() as _,
|
||||
maybe_viewbox,
|
||||
self.fit(),
|
||||
);
|
||||
(offset, elements_iter).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemConsts for Path {
|
||||
const cached_rendering_data_offset: const_field_offset::FieldOffset<Path, CachedRenderingData> =
|
||||
Path::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
||||
}
|
||||
+2421
File diff suppressed because it is too large.
Load diff
+2771
File diff suppressed because it is too large.
Load diff
+209
@@ -0,0 +1,209 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use crate::BorderRadius;
|
||||
use crate::Coord;
|
||||
/// This type is used as a tagging type for use with [`euclid::Scale`] to convert
|
||||
/// between physical and logical pixels.
|
||||
pub struct PhysicalPx;
|
||||
|
||||
/// This type is used as a tagging type for use with [`euclid::Scale`] to convert
|
||||
/// between physical and logical pixels.
|
||||
pub struct LogicalPx;
|
||||
pub type LogicalLength = euclid::Length<Coord, LogicalPx>;
|
||||
pub type LogicalRect = euclid::Rect<Coord, LogicalPx>;
|
||||
pub type LogicalPoint = euclid::Point2D<Coord, LogicalPx>;
|
||||
pub type LogicalSize = euclid::Size2D<Coord, LogicalPx>;
|
||||
pub type LogicalVector = euclid::Vector2D<Coord, LogicalPx>;
|
||||
pub type LogicalBorderRadius = BorderRadius<Coord, LogicalPx>;
|
||||
pub type ItemTransform = euclid::Transform2D<f32, LogicalPx, LogicalPx>;
|
||||
|
||||
pub type ScaleFactor = euclid::Scale<f32, LogicalPx, PhysicalPx>;
|
||||
|
||||
pub trait SizeLengths {
|
||||
type LengthType;
|
||||
fn width_length(&self) -> Self::LengthType;
|
||||
fn height_length(&self) -> Self::LengthType;
|
||||
}
|
||||
|
||||
impl<T: Copy, U> SizeLengths for euclid::Size2D<T, U> {
|
||||
type LengthType = euclid::Length<T, U>;
|
||||
fn width_length(&self) -> Self::LengthType {
|
||||
euclid::Length::new(self.width)
|
||||
}
|
||||
fn height_length(&self) -> Self::LengthType {
|
||||
euclid::Length::new(self.height)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PointLengths {
|
||||
type LengthType;
|
||||
fn x_length(&self) -> Self::LengthType;
|
||||
fn y_length(&self) -> Self::LengthType;
|
||||
}
|
||||
|
||||
impl<T: Copy, U> PointLengths for euclid::Point2D<T, U> {
|
||||
type LengthType = euclid::Length<T, U>;
|
||||
fn x_length(&self) -> Self::LengthType {
|
||||
euclid::Length::new(self.x)
|
||||
}
|
||||
fn y_length(&self) -> Self::LengthType {
|
||||
euclid::Length::new(self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy, U> PointLengths for euclid::Vector2D<T, U> {
|
||||
type LengthType = euclid::Length<T, U>;
|
||||
fn x_length(&self) -> Self::LengthType {
|
||||
euclid::Length::new(self.x)
|
||||
}
|
||||
fn y_length(&self) -> Self::LengthType {
|
||||
euclid::Length::new(self.y)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RectLengths {
|
||||
type SizeType;
|
||||
type LengthType;
|
||||
fn size_length(&self) -> Self::SizeType;
|
||||
fn width_length(&self) -> Self::LengthType;
|
||||
fn height_length(&self) -> Self::LengthType;
|
||||
}
|
||||
|
||||
impl<T: Copy, U> RectLengths for euclid::Rect<T, U> {
|
||||
type LengthType = euclid::Length<T, U>;
|
||||
type SizeType = euclid::Size2D<T, U>;
|
||||
fn size_length(&self) -> Self::SizeType {
|
||||
euclid::Size2D::new(self.size.width, self.size.height)
|
||||
}
|
||||
fn width_length(&self) -> Self::LengthType {
|
||||
self.size_length().width_length()
|
||||
}
|
||||
fn height_length(&self) -> Self::LengthType {
|
||||
self.size_length().height_length()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from the api size to the internal size
|
||||
/// (This doesn't use the `From` trait because it would expose the conversion to euclid in the public API)
|
||||
pub fn logical_size_from_api(size: crate::api::LogicalSize) -> LogicalSize {
|
||||
size.to_euclid()
|
||||
}
|
||||
|
||||
pub fn logical_point_from_api(position: crate::api::LogicalPosition) -> LogicalPoint {
|
||||
position.to_euclid()
|
||||
}
|
||||
|
||||
pub fn logical_position_to_api(pos: LogicalPoint) -> crate::api::LogicalPosition {
|
||||
crate::api::LogicalPosition::from_euclid(pos)
|
||||
}
|
||||
|
||||
pub fn logical_size_to_api(size: LogicalSize) -> crate::api::LogicalSize {
|
||||
crate::api::LogicalSize::from_euclid(size)
|
||||
}
|
||||
|
||||
/// Four distances from the edges of a rectangle represented in the coordinate space of logical pixels.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct LogicalEdges {
|
||||
/// The top distance in logical pixels.
|
||||
pub top: f32,
|
||||
/// The bottom distance in logical pixels.
|
||||
pub bottom: f32,
|
||||
/// The left distance in logical pixels.
|
||||
pub left: f32,
|
||||
/// The right distance in logical pixels.
|
||||
pub right: f32,
|
||||
}
|
||||
|
||||
impl LogicalEdges {
|
||||
/// Construct a new logical edges struct from the given border values, that are assumed to be
|
||||
/// in the logical coordinate space.
|
||||
pub const fn new(top: f32, bottom: f32, left: f32, right: f32) -> Self {
|
||||
Self { top, bottom, left, right }
|
||||
}
|
||||
|
||||
/// Converts the top edge to logical pixels.
|
||||
#[inline]
|
||||
pub const fn top(&self) -> LogicalLength {
|
||||
LogicalLength::new(self.top as crate::Coord)
|
||||
}
|
||||
/// Converts the bottom edge to logical pixels.
|
||||
#[inline]
|
||||
pub const fn bottom(&self) -> LogicalLength {
|
||||
LogicalLength::new(self.bottom as crate::Coord)
|
||||
}
|
||||
/// Converts the left edge to logical pixels.
|
||||
#[inline]
|
||||
pub const fn left(&self) -> LogicalLength {
|
||||
LogicalLength::new(self.left as crate::Coord)
|
||||
}
|
||||
/// Converts the right edge to logical pixels.
|
||||
#[inline]
|
||||
pub const fn right(&self) -> LogicalLength {
|
||||
LogicalLength::new(self.right as crate::Coord)
|
||||
}
|
||||
}
|
||||
|
||||
/// Four distances from the edges of a rectangle represented in the coordinate space of physical pixels.
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PhysicalEdges {
|
||||
/// The top edge in physical pixels.
|
||||
pub top: i32,
|
||||
/// The bottom edge in physical pixels.
|
||||
pub bottom: i32,
|
||||
/// The left edge in physical pixels.
|
||||
pub left: i32,
|
||||
/// The right edge in physical pixels.
|
||||
pub right: i32,
|
||||
}
|
||||
|
||||
impl PhysicalEdges {
|
||||
/// Construct a new physical edges struct from the given border values, that are assumed to be
|
||||
/// in the physical coordinate space.
|
||||
pub const fn new(top: i32, bottom: i32, left: i32, right: i32) -> Self {
|
||||
Self { top, bottom, left, right }
|
||||
}
|
||||
|
||||
/// Convert a given logical edges to a physical edges by dividing the lengths by the
|
||||
/// specified scale factor.
|
||||
#[inline]
|
||||
pub const fn to_logical(&self, scale_factor: f32) -> LogicalEdges {
|
||||
LogicalEdges::new(
|
||||
self.top_to_logical(scale_factor).0 as f32,
|
||||
self.bottom_to_logical(scale_factor).0 as f32,
|
||||
self.left_to_logical(scale_factor).0 as f32,
|
||||
self.right_to_logical(scale_factor).0 as f32,
|
||||
)
|
||||
}
|
||||
|
||||
/// Convert the top logical edge to a physical edge by dividing the length by the
|
||||
/// specified scale factor.
|
||||
#[inline]
|
||||
pub const fn top_to_logical(&self, scale_factor: f32) -> LogicalLength {
|
||||
LogicalLength::new((self.top as f32 / scale_factor) as crate::Coord)
|
||||
}
|
||||
|
||||
/// Convert the bottom logical edge to a physical edge by dividing the length by the
|
||||
/// specified scale factor.
|
||||
#[inline]
|
||||
pub const fn bottom_to_logical(&self, scale_factor: f32) -> LogicalLength {
|
||||
LogicalLength::new((self.bottom as f32 / scale_factor) as crate::Coord)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Convert the left logical edge to a physical edge by dividing the length by the
|
||||
/// specified scale factor.
|
||||
pub const fn left_to_logical(&self, scale_factor: f32) -> LogicalLength {
|
||||
LogicalLength::new((self.left as f32 / scale_factor) as crate::Coord)
|
||||
}
|
||||
|
||||
/// Convert the right logical edge to a physical edge by dividing the length by the
|
||||
/// specified scale factor.
|
||||
#[inline]
|
||||
pub const fn right_to_logical(&self, scale_factor: f32) -> LogicalLength {
|
||||
LogicalLength::new((self.right as f32 / scale_factor) as crate::Coord)
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
// cSpell: ignore sharedvector textlayout
|
||||
|
||||
#![doc = include_str!("README.md")]
|
||||
#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![deny(unsafe_code)]
|
||||
#![allow(clippy::missing_safety_doc)] // FFI surface has many exported unsafe entry points
|
||||
#![cfg_attr(slint_nightly_test, feature(non_exhaustive_omitted_patterns_lint))]
|
||||
#![cfg_attr(slint_nightly_test, warn(non_exhaustive_omitted_patterns))]
|
||||
#![no_std]
|
||||
#![debugger_visualizer(gdb_script_file = "gdb_pretty_printers.py")]
|
||||
|
||||
extern crate alloc;
|
||||
#[cfg(feature = "std")]
|
||||
extern crate std;
|
||||
|
||||
#[cfg(all(not(feature = "std"), feature = "unsafe-single-threaded"))]
|
||||
pub mod unsafe_single_threaded;
|
||||
#[cfg(all(not(feature = "std"), not(feature = "unsafe-single-threaded")))]
|
||||
compile_error!(
|
||||
"At least one of the following feature need to be enabled: `std` or `unsafe-single-threaded`"
|
||||
);
|
||||
pub use crate::items::OperatingSystemType;
|
||||
#[cfg(all(not(feature = "std"), feature = "unsafe-single-threaded"))]
|
||||
pub use crate::unsafe_single_threaded::thread_local;
|
||||
#[cfg(feature = "std")]
|
||||
pub use std::thread_local;
|
||||
|
||||
pub mod accessibility;
|
||||
pub mod animations;
|
||||
pub mod api;
|
||||
pub mod callbacks;
|
||||
pub mod component_factory;
|
||||
pub mod context;
|
||||
pub mod date_time;
|
||||
pub mod future;
|
||||
pub mod graphics;
|
||||
pub mod input;
|
||||
pub mod item_focus;
|
||||
pub mod item_rendering;
|
||||
pub mod item_tree;
|
||||
pub mod items;
|
||||
pub mod layout;
|
||||
pub mod lengths;
|
||||
pub mod menus;
|
||||
pub mod model;
|
||||
pub mod partial_renderer;
|
||||
pub mod platform;
|
||||
pub mod properties;
|
||||
pub mod renderer;
|
||||
#[cfg(feature = "rtti")]
|
||||
pub mod rtti;
|
||||
pub mod sharedvector;
|
||||
pub mod slice;
|
||||
pub mod string;
|
||||
pub mod styled_text;
|
||||
pub mod tests;
|
||||
pub mod textlayout;
|
||||
pub mod timers;
|
||||
pub mod translations;
|
||||
pub mod window;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use string::SharedString;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use sharedvector::SharedVector;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use graphics::{ImageInner, StaticTextures};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use properties::Property;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use callbacks::Callback;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use graphics::Color;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use graphics::Brush;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use graphics::RgbaColor;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[doc(inline)]
|
||||
pub use graphics::PathData;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use graphics::BorderRadius;
|
||||
|
||||
pub use context::{SlintContext, with_global_context};
|
||||
|
||||
#[cfg(not(slint_int_coord))]
|
||||
pub type Coord = f32;
|
||||
#[cfg(slint_int_coord)]
|
||||
pub type Coord = i32;
|
||||
|
||||
/// This type is not exported from the public API crate, so function having this
|
||||
/// parameter cannot be called from the public API without naming it
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct InternalToken;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
thread_local!(
|
||||
/// Permit testing code to force an OS type
|
||||
pub static OPERATING_SYSTEM_OVERRIDE: core::cell::Cell<Option<OperatingSystemType>> =
|
||||
Default::default();
|
||||
);
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn detect_operating_system() -> OperatingSystemType {
|
||||
#[cfg(feature = "std")]
|
||||
if let Some(os_override) = OPERATING_SYSTEM_OVERRIDE.with(|os_override| os_override.get()) {
|
||||
return os_override;
|
||||
}
|
||||
|
||||
if cfg!(target_os = "android") {
|
||||
OperatingSystemType::Android
|
||||
} else if cfg!(target_os = "ios") {
|
||||
OperatingSystemType::Ios
|
||||
} else if cfg!(target_os = "macos") {
|
||||
OperatingSystemType::Macos
|
||||
} else if cfg!(target_os = "windows") {
|
||||
OperatingSystemType::Windows
|
||||
} else if cfg!(target_os = "linux") {
|
||||
OperatingSystemType::Linux
|
||||
} else {
|
||||
OperatingSystemType::Other
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn detect_operating_system() -> OperatingSystemType {
|
||||
if let Some(os_override) = OPERATING_SYSTEM_OVERRIDE.with(|os_override| os_override.get()) {
|
||||
return os_override;
|
||||
}
|
||||
|
||||
let mut user_agent =
|
||||
web_sys::window().and_then(|w| w.navigator().user_agent().ok()).unwrap_or_default();
|
||||
user_agent.make_ascii_lowercase();
|
||||
let mut platform =
|
||||
web_sys::window().and_then(|w| w.navigator().platform().ok()).unwrap_or_default();
|
||||
platform.make_ascii_lowercase();
|
||||
|
||||
if user_agent.contains("ipad") || user_agent.contains("iphone") {
|
||||
OperatingSystemType::Ios
|
||||
} else if user_agent.contains("android") {
|
||||
OperatingSystemType::Android
|
||||
} else if platform.starts_with("mac") {
|
||||
OperatingSystemType::Macos
|
||||
} else if platform.starts_with("win") {
|
||||
OperatingSystemType::Windows
|
||||
} else if platform.starts_with("linux") {
|
||||
OperatingSystemType::Linux
|
||||
} else {
|
||||
OperatingSystemType::Other
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the current platform is an Apple platform (macOS, iOS, iPadOS)
|
||||
pub fn is_apple_platform() -> bool {
|
||||
matches!(detect_operating_system(), OperatingSystemType::Macos | OperatingSystemType::Ios)
|
||||
}
|
||||
|
||||
pub fn open_url(url: &str, window: &crate::api::Window) -> Result<(), crate::api::PlatformError> {
|
||||
crate::window::WindowInner::from_pub(window).context().platform().open_url(url)
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
// for MenuVTable_static
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use crate::graphics::Image;
|
||||
use crate::input::{InternalKeyEvent, Keys};
|
||||
use crate::item_rendering::CachedRenderingData;
|
||||
use crate::item_tree::{ItemTreeRc, ItemWeak, VisitChildrenResult};
|
||||
use crate::items::{ItemRc, ItemRef, MenuEntry, VoidArg};
|
||||
use crate::properties::PropertyTracker;
|
||||
#[cfg(feature = "rtti")]
|
||||
use crate::rtti::*;
|
||||
use crate::string::ToSharedString;
|
||||
use crate::window::WindowAdapter;
|
||||
use crate::{Callback, Property, SharedString, SharedVector};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::pin::Pin;
|
||||
use i_slint_core_macros::SlintElement;
|
||||
use vtable::{VRef, VRefMut};
|
||||
|
||||
/// Interface for native menu and menubar
|
||||
#[cfg_attr(not(feature = "ffi"), i_slint_core_macros::remove_extern)]
|
||||
#[vtable::vtable]
|
||||
#[repr(C)]
|
||||
pub struct MenuVTable {
|
||||
/// Return the list of items for the sub menu (or the main menu of parent is None)
|
||||
sub_menu: extern "C" fn(VRef<MenuVTable>, Option<&MenuEntry>, &mut SharedVector<MenuEntry>),
|
||||
/// Handler when the menu entry is activated
|
||||
activate: extern "C" fn(VRef<MenuVTable>, &MenuEntry),
|
||||
/// drop_in_place handler
|
||||
drop_in_place: extern "C" fn(VRefMut<MenuVTable>) -> Layout,
|
||||
/// dealloc handler
|
||||
dealloc: extern "C" fn(&MenuVTable, ptr: *mut u8, layout: Layout),
|
||||
}
|
||||
|
||||
struct ShadowTreeNode {
|
||||
item: ItemWeak,
|
||||
children: SharedVector<MenuEntry>,
|
||||
}
|
||||
|
||||
pub struct MenuFromItemTree {
|
||||
item_tree: ItemTreeRc,
|
||||
item_cache: RefCell<BTreeMap<SharedString, ShadowTreeNode>>,
|
||||
root: RefCell<SharedVector<MenuEntry>>,
|
||||
next_id: Cell<usize>,
|
||||
tracker: Pin<Box<PropertyTracker>>,
|
||||
condition: Option<Pin<Box<Property<bool>>>>,
|
||||
}
|
||||
|
||||
impl MenuFromItemTree {
|
||||
pub fn new(item_tree: ItemTreeRc) -> Self {
|
||||
Self {
|
||||
item_tree,
|
||||
item_cache: Default::default(),
|
||||
root: Default::default(),
|
||||
tracker: Box::pin(PropertyTracker::default()),
|
||||
next_id: 0.into(),
|
||||
condition: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_condition(
|
||||
item_tree: ItemTreeRc,
|
||||
condition: impl Fn() -> bool + 'static,
|
||||
) -> Self {
|
||||
let cond_prop = Box::pin(Property::new_named(true, "MenuFromItemTree::condition"));
|
||||
cond_prop.as_ref().set_binding(condition);
|
||||
|
||||
Self {
|
||||
item_tree,
|
||||
item_cache: Default::default(),
|
||||
root: Default::default(),
|
||||
tracker: Box::pin(PropertyTracker::default()),
|
||||
next_id: 0.into(),
|
||||
condition: Some(cond_prop),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_shadow_tree(&self) {
|
||||
self.tracker.as_ref().evaluate_if_dirty(|| {
|
||||
self.item_cache.replace(Default::default());
|
||||
if let Some(condition) = &self.condition
|
||||
&& !condition.as_ref().get()
|
||||
{
|
||||
self.root.replace(SharedVector::default());
|
||||
return;
|
||||
}
|
||||
self.root.replace(
|
||||
self.update_shadow_tree_recursive(&ItemRc::new_root(self.item_tree.clone())),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_shadow_tree_recursive(&self, parent: &ItemRc) -> SharedVector<MenuEntry> {
|
||||
let mut result = SharedVector::default();
|
||||
let mut last_is_separator = false;
|
||||
|
||||
let mut actual_visitor =
|
||||
|item_tree: &ItemTreeRc, index: u32, item_pin: Pin<ItemRef>| -> VisitChildrenResult {
|
||||
if let Some(menu_item) = ItemRef::downcast_pin::<MenuItem>(item_pin) {
|
||||
let title = menu_item.title();
|
||||
let is_separator =
|
||||
title.as_str() == i_slint_common::MENU_SEPARATOR_PLACEHOLDER_TITLE;
|
||||
if is_separator && (last_is_separator || result.is_empty()) {
|
||||
return VisitChildrenResult::CONTINUE;
|
||||
}
|
||||
last_is_separator = is_separator;
|
||||
let next_id = self.next_id.get();
|
||||
self.next_id.set(next_id + 1);
|
||||
let id = next_id.to_shared_string();
|
||||
let item = ItemRc::new(item_tree.clone(), index);
|
||||
let children = self.update_shadow_tree_recursive(&item);
|
||||
let has_sub_menu = !children.is_empty();
|
||||
let enabled = menu_item.enabled();
|
||||
let checkable = menu_item.checkable();
|
||||
let checked = menu_item.checked();
|
||||
let icon = menu_item.icon();
|
||||
let shortcut = menu_item.shortcut();
|
||||
self.item_cache.borrow_mut().insert(
|
||||
id.clone(),
|
||||
ShadowTreeNode { item: ItemRc::downgrade(&item), children },
|
||||
);
|
||||
result.push(MenuEntry {
|
||||
title,
|
||||
id,
|
||||
has_sub_menu,
|
||||
is_separator,
|
||||
enabled,
|
||||
checkable,
|
||||
checked,
|
||||
icon,
|
||||
shortcut,
|
||||
});
|
||||
}
|
||||
VisitChildrenResult::CONTINUE
|
||||
};
|
||||
vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
|
||||
|
||||
vtable::VRc::borrow_pin(parent.item_tree()).as_ref().visit_children_item(
|
||||
parent.index() as isize,
|
||||
crate::item_tree::TraversalOrder::BackToFront,
|
||||
actual_visitor,
|
||||
);
|
||||
if last_is_separator {
|
||||
result.pop();
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Menu for MenuFromItemTree {
|
||||
fn sub_menu(&self, parent: Option<&MenuEntry>, result: &mut SharedVector<MenuEntry>) {
|
||||
self.update_shadow_tree();
|
||||
match parent {
|
||||
Some(parent) => {
|
||||
if let Some(r) = self.item_cache.borrow().get(parent.id.as_str()) {
|
||||
*result = r.children.clone();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
*result = self.root.borrow().clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&self, entry: &MenuEntry) {
|
||||
if let Some(menu_item) =
|
||||
self.item_cache.borrow().get(entry.id.as_str()).and_then(|e| e.item.upgrade())
|
||||
&& let Some(menu_item) = menu_item.downcast::<MenuItem>()
|
||||
{
|
||||
if menu_item.as_pin_ref().checkable() {
|
||||
menu_item.checked.set(!menu_item.as_pin_ref().checked());
|
||||
}
|
||||
|
||||
menu_item.activated.call(&());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MenuVTable_static!(static MENU_FROM_ITEM_TREE_VT for MenuFromItemTree);
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(const_field_offset::FieldOffsets, Default, SlintElement)]
|
||||
#[pin]
|
||||
/// The implementation of an MenuItem items that does nothing
|
||||
pub struct MenuItem {
|
||||
pub cached_rendering_data: CachedRenderingData,
|
||||
pub title: Property<SharedString>,
|
||||
pub activated: Callback<VoidArg>,
|
||||
pub enabled: Property<bool>,
|
||||
pub checkable: Property<bool>,
|
||||
pub checked: Property<bool>,
|
||||
pub icon: Property<Image>,
|
||||
pub shortcut: Property<Keys>,
|
||||
}
|
||||
|
||||
impl crate::items::Item for MenuItem {
|
||||
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
||||
|
||||
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
||||
|
||||
fn layout_info(
|
||||
self: Pin<&Self>,
|
||||
_orientation: crate::items::Orientation,
|
||||
_cross_axis_constraint: crate::Coord,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> crate::layout::LayoutInfo {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn input_event_filter_before_children(
|
||||
self: Pin<&Self>,
|
||||
_: &crate::input::MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut crate::items::MouseCursor,
|
||||
) -> crate::input::InputEventFilterResult {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn input_event(
|
||||
self: Pin<&Self>,
|
||||
_: &crate::input::MouseEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
_: &mut crate::items::MouseCursor,
|
||||
) -> crate::input::InputEventResult {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn capture_key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> crate::input::KeyEventResult {
|
||||
crate::input::KeyEventResult::EventIgnored
|
||||
}
|
||||
|
||||
fn key_event(
|
||||
self: Pin<&Self>,
|
||||
_: &InternalKeyEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> crate::input::KeyEventResult {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn focus_event(
|
||||
self: Pin<&Self>,
|
||||
_: &crate::input::FocusEvent,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
) -> crate::input::FocusEventResult {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn render(
|
||||
self: Pin<&Self>,
|
||||
_backend: &mut &mut dyn crate::item_rendering::ItemRenderer,
|
||||
_self_rc: &ItemRc,
|
||||
_size: crate::lengths::LogicalSize,
|
||||
) -> crate::items::RenderingResult {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn bounding_rect(
|
||||
self: Pin<&Self>,
|
||||
_window_adapter: &Rc<dyn WindowAdapter>,
|
||||
_self_rc: &ItemRc,
|
||||
geometry: crate::lengths::LogicalRect,
|
||||
) -> crate::lengths::LogicalRect {
|
||||
geometry
|
||||
}
|
||||
|
||||
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::items::ItemConsts for MenuItem {
|
||||
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
||||
MenuItem,
|
||||
CachedRenderingData,
|
||||
> = MenuItem::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
pub mod ffi {
|
||||
use super::*;
|
||||
|
||||
/// Create a `VRc::<MenuVTable>`` that wraps the [`ItemTreeRc`]
|
||||
///
|
||||
/// Put the created VRc into the result pointer with std::ptr::write
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_menus_create_wrapper(
|
||||
menu_tree: &ItemTreeRc,
|
||||
result: *mut vtable::VRc<MenuVTable>,
|
||||
condition: Option<extern "C" fn(menu_tree: &ItemTreeRc) -> bool>,
|
||||
) {
|
||||
let menu = match condition {
|
||||
Some(condition) => {
|
||||
let menu_weak = ItemTreeRc::downgrade(menu_tree);
|
||||
MenuFromItemTree::new_with_condition(menu_tree.clone(), move || {
|
||||
let Some(menu_rc) = menu_weak.upgrade() else { return false };
|
||||
condition(&menu_rc)
|
||||
})
|
||||
}
|
||||
None => MenuFromItemTree::new(menu_tree.clone()),
|
||||
};
|
||||
|
||||
let vrc = vtable::VRc::into_dyn(vtable::VRc::new(menu));
|
||||
unsafe { core::ptr::write(result, vrc) };
|
||||
}
|
||||
}
|
||||
+1126
File diff suppressed because it is too large.
Load diff
+1561
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,179 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! This module contains the implementation of the model change tracking.
|
||||
|
||||
// Safety: we use pointer to ModelChangeListenerContainer in the DependencyList,
|
||||
// but the Drop of the ModelChangeListenerContainer will remove them from the list
|
||||
// so it will not be accessed after it is dropped
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use super::*;
|
||||
use crate::properties::dependency_tracker::DependencyNode;
|
||||
|
||||
type DependencyListHead =
|
||||
crate::properties::dependency_tracker::DependencyListHead<*const dyn ModelChangeListener>;
|
||||
|
||||
/// Represent a handle to a view that listens to changes to a model.
|
||||
///
|
||||
/// One should normally not use this class directly, it is just
|
||||
/// used internally by via [`ModelTracker::attach_peer`] and [`ModelNotify`]
|
||||
#[derive(Clone)]
|
||||
pub struct ModelPeer<'a> {
|
||||
inner: Pin<&'a DependencyNode<*const dyn ModelChangeListener>>,
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
#[derive(Default)]
|
||||
struct ModelNotifyInner {
|
||||
#[pin]
|
||||
model_row_count_dirty_property: Property<()>,
|
||||
#[pin]
|
||||
model_row_data_dirty_property: Property<()>,
|
||||
#[pin]
|
||||
peers: DependencyListHead,
|
||||
// Sorted list of rows that track_row_data_changes() was called for
|
||||
tracked_rows: RefCell<Vec<usize>>,
|
||||
}
|
||||
|
||||
/// Dispatch notifications from a [`Model`] to one or several [`ModelPeer`].
|
||||
/// Typically, you would want to put this in the implementation of the Model
|
||||
#[derive(Default)]
|
||||
pub struct ModelNotify {
|
||||
inner: OnceCell<Pin<Box<ModelNotifyInner>>>,
|
||||
}
|
||||
|
||||
impl ModelNotify {
|
||||
fn inner(&self) -> Pin<&ModelNotifyInner> {
|
||||
self.inner.get_or_init(|| Box::pin(ModelNotifyInner::default())).as_ref()
|
||||
}
|
||||
|
||||
/// Notify the peers that a specific row was changed
|
||||
pub fn row_changed(&self, row: usize) {
|
||||
if let Some(inner) = self.inner.get() {
|
||||
if inner.tracked_rows.borrow().binary_search(&row).is_ok() {
|
||||
inner.model_row_data_dirty_property.mark_dirty();
|
||||
}
|
||||
inner.as_ref().project_ref().peers.for_each(|p| {
|
||||
// Safety: The peers contain a list of pinned ModelChangedListener
|
||||
unsafe { Pin::new_unchecked(&**p) }.row_changed(row)
|
||||
})
|
||||
}
|
||||
}
|
||||
/// Notify the peers that rows were added
|
||||
pub fn row_added(&self, index: usize, count: usize) {
|
||||
if let Some(inner) = self.inner.get() {
|
||||
inner.model_row_count_dirty_property.mark_dirty();
|
||||
inner.tracked_rows.borrow_mut().clear();
|
||||
inner.model_row_data_dirty_property.mark_dirty();
|
||||
inner.as_ref().project_ref().peers.for_each(|p| {
|
||||
// Safety: The peers contain a list of pinned ModelChangedListener
|
||||
unsafe { Pin::new_unchecked(&**p) }.row_added(index, count)
|
||||
})
|
||||
}
|
||||
}
|
||||
/// Notify the peers that rows were removed
|
||||
pub fn row_removed(&self, index: usize, count: usize) {
|
||||
if let Some(inner) = self.inner.get() {
|
||||
inner.model_row_count_dirty_property.mark_dirty();
|
||||
inner.tracked_rows.borrow_mut().clear();
|
||||
inner.model_row_data_dirty_property.mark_dirty();
|
||||
inner.as_ref().project_ref().peers.for_each(|p| {
|
||||
// Safety: The peers contain a list of pinned ModelChangedListener
|
||||
unsafe { Pin::new_unchecked(&**p) }.row_removed(index, count)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify the peer that the model has been changed in some way and
|
||||
/// everything needs to be reloaded
|
||||
pub fn reset(&self) {
|
||||
if let Some(inner) = self.inner.get() {
|
||||
inner.model_row_count_dirty_property.mark_dirty();
|
||||
inner.tracked_rows.borrow_mut().clear();
|
||||
inner.model_row_data_dirty_property.mark_dirty();
|
||||
inner.as_ref().project_ref().peers.for_each(|p| {
|
||||
// Safety: The peers contain a list of pinned ModelChangedListener
|
||||
unsafe { Pin::new_unchecked(&**p) }.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelTracker for ModelNotify {
|
||||
/// Attach one peer. The peer will be notified when the model changes
|
||||
fn attach_peer(&self, peer: ModelPeer) {
|
||||
self.inner().project_ref().peers.append(peer.inner)
|
||||
}
|
||||
|
||||
fn track_row_count_changes(&self) {
|
||||
self.inner().project_ref().model_row_count_dirty_property.get();
|
||||
}
|
||||
|
||||
fn track_row_data_changes(&self, row: usize) {
|
||||
if crate::properties::is_currently_tracking() {
|
||||
let inner = self.inner().project_ref();
|
||||
|
||||
let mut tracked_rows = inner.tracked_rows.borrow_mut();
|
||||
if let Err(insertion_point) = tracked_rows.binary_search(&row) {
|
||||
tracked_rows.insert(insertion_point, row);
|
||||
}
|
||||
|
||||
inner.model_row_data_dirty_property.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ModelChangeListener {
|
||||
fn row_changed(self: Pin<&Self>, row: usize);
|
||||
fn row_added(self: Pin<&Self>, index: usize, count: usize);
|
||||
fn row_removed(self: Pin<&Self>, index: usize, count: usize);
|
||||
fn reset(self: Pin<&Self>);
|
||||
}
|
||||
|
||||
#[pin_project(PinnedDrop)]
|
||||
#[derive(Default, derive_more::Deref)]
|
||||
/// This is a structure that contains a T which implements [`ModelChangeListener`]
|
||||
/// and can provide a [`ModelPeer`] for it when pinned.
|
||||
pub struct ModelChangeListenerContainer<T: ModelChangeListener> {
|
||||
/// Will be initialized when the ModelPeer is initialized.
|
||||
/// The DependencyNode points to data.
|
||||
peer: OnceCell<DependencyNode<*const dyn ModelChangeListener>>,
|
||||
|
||||
#[pin]
|
||||
#[deref]
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[pin_project::pinned_drop]
|
||||
impl<T: ModelChangeListener> PinnedDrop for ModelChangeListenerContainer<T> {
|
||||
fn drop(self: Pin<&mut Self>) {
|
||||
if let Some(peer) = self.peer.get() {
|
||||
peer.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ModelChangeListener + 'static> ModelChangeListenerContainer<T> {
|
||||
pub fn new(data: T) -> Self {
|
||||
Self { peer: Default::default(), data }
|
||||
}
|
||||
|
||||
pub fn model_peer(self: Pin<&Self>) -> ModelPeer<'_> {
|
||||
let peer = self.get_ref().peer.get_or_init(|| {
|
||||
//Safety: self.data and self.peer have the same lifetime, so the pointer stays valid
|
||||
DependencyNode::new(
|
||||
(&self.data) as &dyn ModelChangeListener as *const dyn ModelChangeListener,
|
||||
)
|
||||
});
|
||||
|
||||
// Safety: `peer` is pinned because `self` is pinned and it is a projection, but pin_project don't go through the OnceCell
|
||||
let peer = unsafe { Pin::new_unchecked(peer) };
|
||||
|
||||
ModelPeer { inner: peer }
|
||||
}
|
||||
|
||||
pub fn get(self: Pin<&Self>) -> Pin<&T> {
|
||||
self.project_ref().data
|
||||
}
|
||||
}
|
||||
+882
@@ -0,0 +1,882 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! This module contains the [`Repeater`] and [`Conditional`] types that are
|
||||
//! used by generated code to instantiate items from a model using the `for`
|
||||
//! syntax, and [`RepeatedItemTree`] which is the trait implemented by the
|
||||
//! generated repeated components.
|
||||
//!
|
||||
//! The [`RepeaterInstanceOps`] trait abstracts over instance storage so the
|
||||
//! update algorithm can be shared between Rust and C++ (via FFI).
|
||||
|
||||
use super::model_peer::{ModelChangeListener, ModelChangeListenerContainer};
|
||||
use super::{Model, ModelRc};
|
||||
use crate::item_tree::{ItemTreeVTable, TraversalOrder};
|
||||
use crate::layout::Orientation;
|
||||
use crate::lengths::{LogicalLength, RectLengths};
|
||||
use crate::{Coord, Property};
|
||||
use alloc::vec::Vec;
|
||||
use core::cell::RefCell;
|
||||
use core::pin::Pin;
|
||||
#[allow(unused)]
|
||||
use euclid::num::Floor;
|
||||
use pin_project::pin_project;
|
||||
|
||||
type ItemTreeRc<C> = vtable::VRc<crate::item_tree::ItemTreeVTable, C>;
|
||||
|
||||
/// ItemTree that can be instantiated by a repeater.
|
||||
pub trait RepeatedItemTree:
|
||||
crate::item_tree::ItemTree + vtable::HasStaticVTable<ItemTreeVTable> + 'static
|
||||
{
|
||||
/// The data corresponding to the model
|
||||
type Data: Default + 'static;
|
||||
|
||||
/// Update this ItemTree at the given index and the given data
|
||||
fn update(&self, index: usize, data: Self::Data);
|
||||
|
||||
/// Called once after the ItemTree has been instantiated and update()
|
||||
/// was called once.
|
||||
fn init(&self) {}
|
||||
|
||||
/// Layout this item in the listview
|
||||
///
|
||||
/// offset_y is the `y` position where this item should be placed.
|
||||
/// it should be updated to be to the y position of the next item.
|
||||
///
|
||||
/// Returns the minimum item width which will be used to compute the listview's viewport width
|
||||
fn listview_layout(self: Pin<&Self>, _offset_y: &mut LogicalLength) -> LogicalLength {
|
||||
LogicalLength::default()
|
||||
}
|
||||
|
||||
/// Returns what's needed to perform the layout if this ItemTree is in a layout
|
||||
/// In case of repeated Rows, the index of a child item is set
|
||||
fn layout_item_info(
|
||||
self: Pin<&Self>,
|
||||
_orientation: Orientation,
|
||||
_child_index: Option<usize>,
|
||||
) -> crate::layout::LayoutItemInfo {
|
||||
crate::layout::LayoutItemInfo::default()
|
||||
}
|
||||
|
||||
/// Returns what's needed to perform a flexbox layout if this ItemTree is in a FlexboxLayout.
|
||||
/// Includes flex-specific properties (flex-grow, flex-shrink, flex-basis, align-self, order).
|
||||
fn flexbox_layout_item_info(
|
||||
self: Pin<&Self>,
|
||||
orientation: Orientation,
|
||||
child_index: Option<usize>,
|
||||
) -> crate::layout::FlexboxLayoutItemInfo {
|
||||
self.layout_item_info(orientation, child_index).into()
|
||||
}
|
||||
|
||||
/// Fills in the grid layout input data for this ItemTree if it is in a grid layout.
|
||||
/// This will be a single GridLayoutInputData if the repeated item is a single cell,
|
||||
/// or multiple GridLayoutInputData if the repeated item is a full Row.
|
||||
/// The slice must have the exact size required (known at compile time).
|
||||
fn grid_layout_input_data(
|
||||
self: Pin<&Self>,
|
||||
_new_row: bool,
|
||||
_result: &mut [crate::layout::GridLayoutInputData],
|
||||
) {
|
||||
crate::debug_log!(
|
||||
"Internal error in Slint: RepeatedItemTree::grid_layout_input_data() not implemented for {}",
|
||||
core::any::type_name::<Self>()
|
||||
);
|
||||
// the actual implementation is in the code generated by generate_repeated_component()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
enum RepeatedInstanceState {
|
||||
/// The item is in a clean state
|
||||
Clean,
|
||||
/// The model data is stale and needs to be refreshed
|
||||
Dirty,
|
||||
}
|
||||
struct RepeaterInner<C: RepeatedItemTree> {
|
||||
instances: Vec<(RepeatedInstanceState, Option<ItemTreeRc<C>>)>,
|
||||
/// ListView-specific layout state (offset, cached heights, scroll position).
|
||||
layout_state: RepeaterLayoutState,
|
||||
}
|
||||
|
||||
impl<C: RepeatedItemTree> Default for RepeaterInner<C> {
|
||||
fn default() -> Self {
|
||||
RepeaterInner { instances: Default::default(), layout_state: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistent layout state for a ListView repeater.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct RepeaterLayoutState {
|
||||
/// The model row index of the first instance in the collection.
|
||||
pub offset: usize,
|
||||
/// The average visible item height (cached between frames).
|
||||
pub cached_item_height: Coord,
|
||||
/// The viewport_y value from the previous layout pass.
|
||||
/// It is used to detect if we are scrolling up or down
|
||||
pub previous_viewport_y: Coord,
|
||||
/// The y position of the item at `offset`.
|
||||
pub anchor_y: Coord,
|
||||
}
|
||||
|
||||
/// Abstraction over a repeater's instance collection so the same algorithm
|
||||
/// works for both native Rust repeaters and C++ repeaters via FFI.
|
||||
trait RepeaterInstanceOps {
|
||||
/// Number of currently instantiated items.
|
||||
fn len(&self) -> usize;
|
||||
|
||||
/// Replace the range `position..position+remove` with `add` new empty/dirty slots.
|
||||
fn splice(&mut self, position: usize, remove: usize, add: usize);
|
||||
|
||||
/// If dirty, ensure the instance is created and updated for `row`.
|
||||
/// Returns `true` if freshly created (needs init later).
|
||||
fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool;
|
||||
|
||||
/// Height of the instance, or `None` if not yet created.
|
||||
fn height(&self, instance_idx: usize) -> Option<Coord>;
|
||||
|
||||
/// Call `listview_layout` on the instance.
|
||||
/// Advances `*y` to the next item position. Returns item width.
|
||||
fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord;
|
||||
}
|
||||
|
||||
/// Update all instances in the repeater, creating any that are missing.
|
||||
/// Returns indices of newly created instances (that need `init()` called).
|
||||
fn update_all_instances(
|
||||
ops: &mut impl RepeaterInstanceOps,
|
||||
offset: usize,
|
||||
count: usize,
|
||||
) -> Vec<usize> {
|
||||
let cur = ops.len();
|
||||
if count > cur {
|
||||
ops.splice(cur, 0, count - cur);
|
||||
} else if count < cur {
|
||||
ops.splice(count, cur - count, 0);
|
||||
}
|
||||
let mut indices_to_init = Vec::new();
|
||||
for i in 0..count {
|
||||
if ops.ensure_updated(i, i + offset) {
|
||||
indices_to_init.push(i);
|
||||
}
|
||||
}
|
||||
indices_to_init
|
||||
}
|
||||
|
||||
/// Update only the instances visible in the ListView viewport.
|
||||
///
|
||||
/// This is the core virtualization algorithm: it estimates which model rows
|
||||
/// are visible, instantiates/updates those, lays them out, and cleans up
|
||||
/// off-screen instances. Returns indices of newly created instances.
|
||||
fn update_visible_instances(
|
||||
ops: &mut impl RepeaterInstanceOps,
|
||||
state: &mut RepeaterLayoutState,
|
||||
row_count: usize,
|
||||
viewport_width: Pin<&Property<LogicalLength>>,
|
||||
viewport_height: Pin<&Property<LogicalLength>>,
|
||||
viewport_y: Pin<&Property<LogicalLength>>,
|
||||
listview_width: LogicalLength,
|
||||
listview_height: LogicalLength,
|
||||
) -> Vec<usize> {
|
||||
let zero = LogicalLength::default();
|
||||
let mut vp_width = listview_width.get();
|
||||
let listview_height = listview_height.get();
|
||||
|
||||
if row_count == 0 {
|
||||
ops.splice(0, ops.len(), 0);
|
||||
viewport_height.set(zero);
|
||||
viewport_y.set(zero);
|
||||
viewport_width.set(listview_width);
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut vp_y = viewport_y.get().get();
|
||||
if !viewport_y.has_binding() {
|
||||
vp_y = vp_y.min(0 as Coord);
|
||||
}
|
||||
|
||||
let mut indices_to_init = Vec::new();
|
||||
|
||||
// Estimate element height from cached value or by measuring existing instances.
|
||||
let element_height = if state.cached_item_height > 0 as Coord {
|
||||
state.cached_item_height
|
||||
} else {
|
||||
let mut total_height: Coord = 0 as Coord;
|
||||
let mut count = 0usize;
|
||||
for i in 0..ops.len() {
|
||||
if let Some(h) = ops.height(i) {
|
||||
total_height += h;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
total_height / count as Coord
|
||||
} else {
|
||||
// No items exist yet. Create one to measure.
|
||||
state.offset = state.offset.min(row_count - 1);
|
||||
ops.splice(0, ops.len(), 1);
|
||||
if ops.ensure_updated(0, state.offset) {
|
||||
indices_to_init.push(0);
|
||||
}
|
||||
ops.height(0).unwrap_or(0 as Coord)
|
||||
}
|
||||
};
|
||||
|
||||
if state.offset >= row_count {
|
||||
state.offset = row_count - 1;
|
||||
}
|
||||
|
||||
let one_and_a_half_screen = listview_height * 3 as Coord / 2 as Coord;
|
||||
let first_item_y = state.anchor_y;
|
||||
let last_item_bottom = first_item_y + element_height * ops.len() as Coord;
|
||||
|
||||
let (mut new_offset, mut new_offset_y) = if first_item_y > -vp_y + one_and_a_half_screen
|
||||
|| last_item_bottom + element_height < -vp_y
|
||||
{
|
||||
// Jumping more than 1.5 screens: random seek.
|
||||
ops.splice(0, ops.len(), 0);
|
||||
state.offset = ((-vp_y / element_height).floor() as usize).min(row_count - 1);
|
||||
(state.offset, 0 as Coord)
|
||||
} else if vp_y < state.previous_viewport_y {
|
||||
// Scrolled down: find the new offset by walking existing instances.
|
||||
let mut it_y = first_item_y + vp_y;
|
||||
let mut new_off = state.offset;
|
||||
for i in 0..ops.len() {
|
||||
if ops.ensure_updated(i, new_off) {
|
||||
indices_to_init.push(i);
|
||||
}
|
||||
let h = ops.height(i).unwrap_or(0 as Coord);
|
||||
if it_y + h > 0 as Coord || new_off + 1 >= row_count {
|
||||
break;
|
||||
}
|
||||
it_y += h;
|
||||
new_off += 1;
|
||||
}
|
||||
(new_off, it_y)
|
||||
} else {
|
||||
// Scrolled up: will instantiate items before offset in the loop below.
|
||||
(state.offset, first_item_y + vp_y)
|
||||
};
|
||||
|
||||
let mut loop_count = 0;
|
||||
loop {
|
||||
// Fill gap before new_offset using already-instantiated items.
|
||||
while new_offset > state.offset && new_offset_y > 0 as Coord {
|
||||
new_offset -= 1;
|
||||
new_offset_y -= ops.height(new_offset - state.offset).unwrap_or(0 as Coord);
|
||||
}
|
||||
// If there is still a gap, create new instances before the current ones.
|
||||
let mut prepend_count = 0;
|
||||
while new_offset > 0 && new_offset_y > 0 as Coord {
|
||||
new_offset -= 1;
|
||||
ops.splice(0, 0, 1);
|
||||
ops.ensure_updated(0, new_offset);
|
||||
new_offset_y -= ops.height(0).unwrap_or(0 as Coord);
|
||||
prepend_count += 1;
|
||||
}
|
||||
if prepend_count > 0 {
|
||||
for x in &mut indices_to_init {
|
||||
*x += prepend_count;
|
||||
}
|
||||
indices_to_init.extend(0..prepend_count);
|
||||
state.offset = new_offset;
|
||||
}
|
||||
debug_assert!(new_offset >= state.offset && new_offset <= state.offset + ops.len());
|
||||
|
||||
// Layout items until we fill the view, starting with already-instantiated ones.
|
||||
let mut y = new_offset_y;
|
||||
let mut idx = new_offset;
|
||||
let instances_begin = new_offset - state.offset;
|
||||
for i in instances_begin..ops.len() {
|
||||
if idx >= row_count {
|
||||
break;
|
||||
}
|
||||
if ops.ensure_updated(i, idx) {
|
||||
indices_to_init.push(i);
|
||||
}
|
||||
vp_width = vp_width.max(ops.listview_layout(i, &mut y));
|
||||
idx += 1;
|
||||
if y >= listview_height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create more items until there is no more room.
|
||||
while y < listview_height && idx < row_count {
|
||||
let i = ops.len();
|
||||
ops.splice(i, 0, 1);
|
||||
ops.ensure_updated(i, idx);
|
||||
indices_to_init.push(i);
|
||||
vp_width = vp_width.max(ops.listview_layout(i, &mut y));
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
if y < listview_height && vp_y < 0 as Coord && loop_count < 3 {
|
||||
debug_assert!(idx >= row_count);
|
||||
// Reached end of model with room to spare. Scroll up.
|
||||
vp_y += listview_height - y;
|
||||
loop_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clean up instances that are not shown.
|
||||
if new_offset != state.offset {
|
||||
let remove_count = new_offset - state.offset;
|
||||
ops.splice(0, remove_count, 0);
|
||||
indices_to_init.retain_mut(|i| {
|
||||
if *i < remove_count {
|
||||
false
|
||||
} else {
|
||||
*i -= remove_count;
|
||||
true
|
||||
}
|
||||
});
|
||||
state.offset = new_offset;
|
||||
}
|
||||
let keep = idx - new_offset;
|
||||
if ops.len() > keep {
|
||||
ops.splice(keep, ops.len() - keep, 0);
|
||||
indices_to_init.retain(|x| *x < keep);
|
||||
}
|
||||
|
||||
if ops.len() == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// Recompute coordinates for the scrollbar.
|
||||
state.cached_item_height = (y - new_offset_y) / ops.len() as Coord;
|
||||
state.anchor_y = state.cached_item_height * state.offset as Coord;
|
||||
viewport_height.set(LogicalLength::new(state.cached_item_height * row_count as Coord));
|
||||
viewport_width.set(LogicalLength::new(vp_width));
|
||||
// If an animation is ongoing we should not interrupt it
|
||||
if !viewport_y.has_binding() {
|
||||
let new_viewport_y = -state.anchor_y + new_offset_y;
|
||||
if new_viewport_y != viewport_y.get().get() {
|
||||
viewport_y.set(LogicalLength::new(new_viewport_y));
|
||||
}
|
||||
state.previous_viewport_y = new_viewport_y;
|
||||
} else {
|
||||
state.previous_viewport_y = viewport_y.get().0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
indices_to_init
|
||||
}
|
||||
|
||||
/// Adapter implementing [`RepeaterInstanceOps`] for the native Rust repeater.
|
||||
struct RustRepeaterOps<'a, C: RepeatedItemTree> {
|
||||
instances: &'a mut Vec<(RepeatedInstanceState, Option<ItemTreeRc<C>>)>,
|
||||
init: &'a dyn Fn() -> ItemTreeRc<C>,
|
||||
model: &'a ModelRc<C::Data>,
|
||||
}
|
||||
|
||||
impl<C: RepeatedItemTree> RepeaterInstanceOps for RustRepeaterOps<'_, C> {
|
||||
fn len(&self) -> usize {
|
||||
self.instances.len()
|
||||
}
|
||||
|
||||
fn splice(&mut self, position: usize, remove: usize, add: usize) {
|
||||
self.instances.splice(
|
||||
position..position + remove,
|
||||
core::iter::repeat_with(|| (RepeatedInstanceState::Dirty, None)).take(add),
|
||||
);
|
||||
}
|
||||
|
||||
fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool {
|
||||
let c = &mut self.instances[instance_idx];
|
||||
if c.0 == RepeatedInstanceState::Dirty {
|
||||
let created = c.1.is_none();
|
||||
if created {
|
||||
c.1 = Some((self.init)());
|
||||
}
|
||||
c.1.as_ref().unwrap().update(row, self.model.row_data(row).unwrap_or_default());
|
||||
c.0 = RepeatedInstanceState::Clean;
|
||||
created
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn height(&self, instance_idx: usize) -> Option<Coord> {
|
||||
self.instances[instance_idx]
|
||||
.1
|
||||
.as_ref()
|
||||
.map(|x| x.as_pin_ref().item_geometry(0).height_length().get())
|
||||
}
|
||||
|
||||
fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord {
|
||||
let mut y_len = LogicalLength::new(*y);
|
||||
let w = self.instances[instance_idx]
|
||||
.1
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_pin_ref()
|
||||
.listview_layout(&mut y_len);
|
||||
*y = y_len.get();
|
||||
w.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct is put in a component when using the `for` syntax
|
||||
/// It helps instantiating the ItemTree `T`
|
||||
#[pin_project]
|
||||
pub struct RepeaterTracker<T: RepeatedItemTree> {
|
||||
inner: RefCell<RepeaterInner<T>>,
|
||||
#[pin]
|
||||
model: Property<ModelRc<T::Data>>,
|
||||
#[pin]
|
||||
/// Set to true when the model becomes dirty.
|
||||
is_dirty: Property<bool>,
|
||||
/// Only used for the list view to track if the scrollbar has changed and item needs to be laid out again.
|
||||
#[pin]
|
||||
listview_geometry_tracker: crate::properties::PropertyTracker,
|
||||
}
|
||||
|
||||
impl<T: RepeatedItemTree> ModelChangeListener for RepeaterTracker<T> {
|
||||
/// Notify the peers that a specific row was changed
|
||||
fn row_changed(self: Pin<&Self>, row: usize) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
let inner = &mut *inner;
|
||||
if let Some(c) = inner.instances.get_mut(row.wrapping_sub(inner.layout_state.offset)) {
|
||||
if !self.model.is_dirty() {
|
||||
if let Some(comp) = c.1.as_ref() {
|
||||
let model = self.project_ref().model.get_untracked();
|
||||
comp.update(row, model.row_data(row).unwrap_or_default());
|
||||
c.0 = RepeatedInstanceState::Clean;
|
||||
}
|
||||
} else {
|
||||
c.0 = RepeatedInstanceState::Dirty;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Notify the peers that rows were added
|
||||
fn row_added(self: Pin<&Self>, mut index: usize, mut count: usize) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if index < inner.layout_state.offset {
|
||||
if index + count <= inner.layout_state.offset {
|
||||
// Entirely before the visible range: shift the offset.
|
||||
inner.layout_state.offset += count;
|
||||
self.is_dirty.set(true);
|
||||
for c in inner.instances.iter_mut() {
|
||||
c.0 = RepeatedInstanceState::Dirty;
|
||||
}
|
||||
return;
|
||||
}
|
||||
count -= inner.layout_state.offset - index;
|
||||
index = 0;
|
||||
} else {
|
||||
index -= inner.layout_state.offset;
|
||||
}
|
||||
if count == 0 || index > inner.instances.len() {
|
||||
return;
|
||||
}
|
||||
self.is_dirty.set(true);
|
||||
inner.instances.splice(
|
||||
index..index,
|
||||
core::iter::repeat_n((RepeatedInstanceState::Dirty, None), count),
|
||||
);
|
||||
for c in inner.instances[index + count..].iter_mut() {
|
||||
// Because all the indexes are dirty
|
||||
c.0 = RepeatedInstanceState::Dirty;
|
||||
}
|
||||
}
|
||||
/// Notify the peers that rows were removed
|
||||
fn row_removed(self: Pin<&Self>, mut index: usize, mut count: usize) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if index < inner.layout_state.offset {
|
||||
if index + count <= inner.layout_state.offset {
|
||||
// Entirely before the visible range: shift the offset.
|
||||
inner.layout_state.offset -= count;
|
||||
self.is_dirty.set(true);
|
||||
for c in inner.instances.iter_mut() {
|
||||
c.0 = RepeatedInstanceState::Dirty;
|
||||
}
|
||||
return;
|
||||
}
|
||||
count -= inner.layout_state.offset - index;
|
||||
inner.layout_state.offset = index;
|
||||
index = 0;
|
||||
} else {
|
||||
index -= inner.layout_state.offset;
|
||||
}
|
||||
if count == 0 || index >= inner.instances.len() {
|
||||
return;
|
||||
}
|
||||
if (index + count) > inner.instances.len() {
|
||||
count = inner.instances.len() - index;
|
||||
}
|
||||
self.is_dirty.set(true);
|
||||
inner.instances.drain(index..(index + count));
|
||||
for c in inner.instances[index..].iter_mut() {
|
||||
// Because all the indexes are dirty
|
||||
c.0 = RepeatedInstanceState::Dirty;
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(self: Pin<&Self>) {
|
||||
self.is_dirty.set(true);
|
||||
self.inner.borrow_mut().instances.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: RepeatedItemTree> Default for RepeaterTracker<C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: Default::default(),
|
||||
model: Property::new_named(ModelRc::default(), "i_slint_core::Repeater::model"),
|
||||
is_dirty: Property::new_named(false, "i_slint_core::Repeater::is_dirty"),
|
||||
listview_geometry_tracker: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
pub struct Repeater<C: RepeatedItemTree>(#[pin] ModelChangeListenerContainer<RepeaterTracker<C>>);
|
||||
|
||||
impl<C: RepeatedItemTree> Default for Repeater<C> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: RepeatedItemTree + 'static> Repeater<C> {
|
||||
fn data(self: Pin<&Self>) -> Pin<&RepeaterTracker<C>> {
|
||||
self.project_ref().0.get()
|
||||
}
|
||||
|
||||
fn model(self: Pin<&Self>) -> ModelRc<C::Data> {
|
||||
let model = self.data().project_ref().model;
|
||||
|
||||
if model.is_dirty() {
|
||||
let old_model = model.get_internal();
|
||||
let m = model.get();
|
||||
if old_model != m {
|
||||
*self.data().inner.borrow_mut() = RepeaterInner::default();
|
||||
self.data().is_dirty.set(true);
|
||||
let peer = self.project_ref().0.model_peer();
|
||||
m.model_tracker().attach_peer(peer);
|
||||
}
|
||||
m
|
||||
} else {
|
||||
model.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// Call this function to make sure that the model is updated.
|
||||
/// The init function is the function to create a ItemTree
|
||||
pub fn ensure_updated(self: Pin<&Self>, init: impl Fn() -> ItemTreeRc<C>) {
|
||||
let model = self.model();
|
||||
if !self.data().project_ref().is_dirty.get() {
|
||||
return;
|
||||
}
|
||||
let count = model.row_count();
|
||||
let mut inner = self.0.inner.borrow_mut();
|
||||
let offset = inner.layout_state.offset;
|
||||
let mut ops =
|
||||
RustRepeaterOps { instances: &mut inner.instances, init: &init, model: &model };
|
||||
self.data().is_dirty.set(false);
|
||||
let indices_to_init = update_all_instances(&mut ops, offset, count);
|
||||
|
||||
drop(inner);
|
||||
self.init_instances(indices_to_init);
|
||||
}
|
||||
|
||||
fn init_instances(&self, indices: Vec<usize>) {
|
||||
let inner = self.0.inner.borrow();
|
||||
for index in indices {
|
||||
if let Some((_, Some(comp))) = inner.instances.get(index) {
|
||||
comp.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as `Self::ensure_updated` but for a ListView
|
||||
pub fn ensure_updated_listview(
|
||||
self: Pin<&Self>,
|
||||
init: impl Fn() -> ItemTreeRc<C>,
|
||||
viewport_width: Pin<&Property<LogicalLength>>,
|
||||
viewport_height: Pin<&Property<LogicalLength>>,
|
||||
viewport_y: Pin<&Property<LogicalLength>>,
|
||||
listview_width: LogicalLength,
|
||||
listview_height: Pin<&Property<LogicalLength>>,
|
||||
) {
|
||||
// Query is_dirty to track model changes
|
||||
let _ = self.data().project_ref().is_dirty.get();
|
||||
self.data().project_ref().is_dirty.set(false);
|
||||
|
||||
let model = self.model();
|
||||
let row_count = model.row_count();
|
||||
|
||||
let data = self.data();
|
||||
let mut inner = data.inner.borrow_mut();
|
||||
|
||||
let RepeaterInner { ref mut instances, ref mut layout_state } = *inner;
|
||||
let mut ops = RustRepeaterOps { instances, init: &init, model: &model };
|
||||
let indices_to_init = update_visible_instances(
|
||||
&mut ops,
|
||||
layout_state,
|
||||
row_count,
|
||||
viewport_width,
|
||||
viewport_height,
|
||||
viewport_y,
|
||||
listview_width,
|
||||
listview_height.get(),
|
||||
);
|
||||
|
||||
drop(inner);
|
||||
self.init_instances(indices_to_init);
|
||||
}
|
||||
|
||||
/// Sets the data directly in the model
|
||||
pub fn model_set_row_data(self: Pin<&Self>, row: usize, data: C::Data) {
|
||||
let model = self.model();
|
||||
model.set_row_data(row, data);
|
||||
}
|
||||
|
||||
/// Set the model binding
|
||||
pub fn set_model_binding(&self, binding: impl Fn() -> ModelRc<C::Data> + 'static) {
|
||||
self.0.model.set_binding(binding);
|
||||
}
|
||||
|
||||
/// Call the visitor for the root of each instance
|
||||
pub fn visit(
|
||||
&self,
|
||||
order: TraversalOrder,
|
||||
mut visitor: crate::item_tree::ItemVisitorRefMut,
|
||||
) -> crate::item_tree::VisitChildrenResult {
|
||||
// We can't keep self.inner borrowed because the event might modify the model
|
||||
let count = self.0.inner.borrow().instances.len() as u32;
|
||||
for i in 0..count {
|
||||
let i = if order == TraversalOrder::BackToFront { i } else { count - i - 1 };
|
||||
let c = self.0.inner.borrow().instances.get(i as usize).and_then(|c| c.1.clone());
|
||||
if let Some(c) = c
|
||||
&& c.as_pin_ref().visit_children_item(-1, order, visitor.borrow_mut()).has_aborted()
|
||||
{
|
||||
return crate::item_tree::VisitChildrenResult::abort(i, 0);
|
||||
}
|
||||
}
|
||||
crate::item_tree::VisitChildrenResult::CONTINUE
|
||||
}
|
||||
|
||||
/// Return the amount of instances currently in the repeater
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.inner.borrow().instances.len()
|
||||
}
|
||||
|
||||
/// Return the range of indices used by this Repeater.
|
||||
///
|
||||
/// Two values are necessary here since the Repeater can start to insert the data from its
|
||||
/// model at an offset.
|
||||
pub fn range(&self) -> core::ops::Range<usize> {
|
||||
let inner = self.0.inner.borrow();
|
||||
core::ops::Range {
|
||||
start: inner.layout_state.offset,
|
||||
end: inner.layout_state.offset + inner.instances.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the instance for the given model index.
|
||||
/// The index should be within [`Self::range()`]
|
||||
pub fn instance_at(&self, index: usize) -> Option<ItemTreeRc<C>> {
|
||||
let inner = self.0.inner.borrow();
|
||||
inner
|
||||
.instances
|
||||
.get(index.checked_sub(inner.layout_state.offset)?)
|
||||
.map(|c| c.1.clone().expect("That was updated before!"))
|
||||
}
|
||||
|
||||
/// Return true if the Repeater as empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns a vector containing all instances
|
||||
pub fn instances_vec(&self) -> Vec<ItemTreeRc<C>> {
|
||||
self.0.inner.borrow().instances.iter().flat_map(|x| x.1.clone()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
pub struct Conditional<C: RepeatedItemTree> {
|
||||
#[pin]
|
||||
model: Property<bool>,
|
||||
instance: RefCell<Option<ItemTreeRc<C>>>,
|
||||
}
|
||||
|
||||
impl<C: RepeatedItemTree> Default for Conditional<C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: Property::new_named(false, "i_slint_core::Conditional::model"),
|
||||
instance: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: RepeatedItemTree + 'static> Conditional<C> {
|
||||
/// Call this function to make sure that the model is updated.
|
||||
/// The init function is the function to create a ItemTree
|
||||
pub fn ensure_updated(self: Pin<&Self>, init: impl Fn() -> ItemTreeRc<C>) {
|
||||
let model = self.project_ref().model.get();
|
||||
|
||||
if !model {
|
||||
drop(self.instance.replace(None));
|
||||
} else if self.instance.borrow().is_none() {
|
||||
let i = init();
|
||||
self.instance.replace(Some(i.clone()));
|
||||
i.init();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the model binding
|
||||
pub fn set_model_binding(&self, binding: impl Fn() -> bool + 'static) {
|
||||
self.model.set_binding(binding);
|
||||
}
|
||||
|
||||
/// Call the visitor for the root of each instance
|
||||
pub fn visit(
|
||||
&self,
|
||||
order: TraversalOrder,
|
||||
mut visitor: crate::item_tree::ItemVisitorRefMut,
|
||||
) -> crate::item_tree::VisitChildrenResult {
|
||||
// We can't keep self.inner borrowed because the event might modify the model
|
||||
let instance = self.instance.borrow().clone();
|
||||
if let Some(c) = instance
|
||||
&& c.as_pin_ref().visit_children_item(-1, order, visitor.borrow_mut()).has_aborted()
|
||||
{
|
||||
return crate::item_tree::VisitChildrenResult::abort(0, 0);
|
||||
}
|
||||
|
||||
crate::item_tree::VisitChildrenResult::CONTINUE
|
||||
}
|
||||
|
||||
/// Return the amount of instances (1 if the conditional is active, 0 otherwise)
|
||||
pub fn len(&self) -> usize {
|
||||
self.instance.borrow().is_some() as usize
|
||||
}
|
||||
|
||||
/// Return the range of indices used by this Conditional.
|
||||
///
|
||||
/// Similar to Repeater::range, but the range is always [0, 1] if the Conditional is active.
|
||||
pub fn range(&self) -> core::ops::Range<usize> {
|
||||
0..self.len()
|
||||
}
|
||||
|
||||
/// Return the instance for the given model index.
|
||||
/// The index should be within [`Self::range()`]
|
||||
pub fn instance_at(&self, index: usize) -> Option<ItemTreeRc<C>> {
|
||||
if index != 0 {
|
||||
return None;
|
||||
}
|
||||
self.instance.borrow().clone()
|
||||
}
|
||||
|
||||
/// Return true if the Repeater as empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns a vector containing all instances
|
||||
pub fn instances_vec(&self) -> Vec<ItemTreeRc<C>> {
|
||||
self.instance.borrow().clone().into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use super::*;
|
||||
|
||||
/// C++ callback table for [`RepeaterInstanceOps`], including the opaque
|
||||
/// user_data pointer that is passed to each callback.
|
||||
#[repr(C)]
|
||||
pub struct RepeaterInstanceOpsVTable {
|
||||
pub user_data: *mut core::ffi::c_void,
|
||||
pub len: unsafe extern "C" fn(user_data: *mut core::ffi::c_void) -> usize,
|
||||
pub splice: unsafe extern "C" fn(
|
||||
user_data: *mut core::ffi::c_void,
|
||||
position: usize,
|
||||
remove: usize,
|
||||
add: usize,
|
||||
),
|
||||
pub ensure_updated: unsafe extern "C" fn(
|
||||
user_data: *mut core::ffi::c_void,
|
||||
instance_idx: usize,
|
||||
row: usize,
|
||||
) -> bool,
|
||||
/// Height of instance, or NaN if not yet created.
|
||||
pub height:
|
||||
unsafe extern "C" fn(user_data: *mut core::ffi::c_void, instance_idx: usize) -> Coord,
|
||||
pub listview_layout: Option<
|
||||
unsafe extern "C" fn(
|
||||
user_data: *mut core::ffi::c_void,
|
||||
instance_idx: usize,
|
||||
y: &mut Coord,
|
||||
) -> Coord,
|
||||
>,
|
||||
pub init: unsafe extern "C" fn(user_data: *mut core::ffi::c_void, instance_idx: usize),
|
||||
}
|
||||
|
||||
impl RepeaterInstanceOpsVTable {
|
||||
fn init_instances(&self, indices: Vec<usize>) {
|
||||
for idx in indices {
|
||||
unsafe { (self.init)(self.user_data, idx) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RepeaterInstanceOps for RepeaterInstanceOpsVTable {
|
||||
fn len(&self) -> usize {
|
||||
unsafe { (self.len)(self.user_data) }
|
||||
}
|
||||
fn splice(&mut self, position: usize, remove: usize, add: usize) {
|
||||
unsafe { (self.splice)(self.user_data, position, remove, add) }
|
||||
}
|
||||
fn ensure_updated(&mut self, instance_idx: usize, row: usize) -> bool {
|
||||
unsafe { (self.ensure_updated)(self.user_data, instance_idx, row) }
|
||||
}
|
||||
fn height(&self, instance_idx: usize) -> Option<Coord> {
|
||||
let h = unsafe { (self.height)(self.user_data, instance_idx) };
|
||||
if h.is_nan() { None } else { Some(h) }
|
||||
}
|
||||
fn listview_layout(&self, instance_idx: usize, y: &mut Coord) -> Coord {
|
||||
self.listview_layout
|
||||
.map_or(0 as Coord, |f| unsafe { f(self.user_data, instance_idx, y) })
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_repeater_ensure_updated(
|
||||
ops: &mut RepeaterInstanceOpsVTable,
|
||||
offset: usize,
|
||||
count: usize,
|
||||
) {
|
||||
let indices_to_init = update_all_instances(ops, offset, count);
|
||||
ops.init_instances(indices_to_init);
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_repeater_ensure_updated_listview(
|
||||
ops: &mut RepeaterInstanceOpsVTable,
|
||||
state: &mut RepeaterLayoutState,
|
||||
row_count: usize,
|
||||
viewport_width: Pin<&Property<LogicalLength>>,
|
||||
viewport_height: Pin<&Property<LogicalLength>>,
|
||||
viewport_y: Pin<&Property<LogicalLength>>,
|
||||
listview_width: LogicalLength,
|
||||
listview_height: LogicalLength,
|
||||
) {
|
||||
let indices_to_init = update_visible_instances(
|
||||
ops,
|
||||
state,
|
||||
row_count,
|
||||
viewport_width,
|
||||
viewport_height,
|
||||
viewport_y,
|
||||
listview_width,
|
||||
listview_height,
|
||||
);
|
||||
ops.init_instances(indices_to_init);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,840 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! Module for a renderer proxy that tries to render only the parts of the tree that have changed.
|
||||
//!
|
||||
//! This is the way the partial renderer work:
|
||||
//!
|
||||
//! 1. [`PartialRenderer::compute_dirty_regions`] will go over the items and try to compute the region that needs to be repainted.
|
||||
//! If either the bounding box has changed, or the PropertyTracker that tracks the rendering properties is dirty, then the
|
||||
//! region is marked dirty.
|
||||
//! That pass also register dependencies on every geometry, and on the non-dirty property trackers.
|
||||
//! 2. The Renderer calls [`PartialRenderer::filter_item`] For most items.
|
||||
//! This assume that the cached geometry was requested in the previous step. So it will not register new dependencies.
|
||||
//! 3. Then the renderer calls the rendering function for each item that needs to be rendered.
|
||||
//! This register dependencies only on the rendering tracker.
|
||||
//!
|
||||
|
||||
use crate::Coord;
|
||||
use crate::item_rendering::{
|
||||
ItemRenderer, ItemRendererFeatures, RenderBorderRectangle, RenderImage, RenderRectangle,
|
||||
RenderText,
|
||||
};
|
||||
use crate::item_tree::{ItemTreeRc, ItemTreeWeak, ItemVisitorResult};
|
||||
#[cfg(feature = "path")]
|
||||
use crate::items::Path;
|
||||
use crate::items::{BoxShadow, Clip, ItemRc, ItemRef, Layer, Opacity, RenderingResult, TextInput};
|
||||
use crate::lengths::{
|
||||
ItemTransform, LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect,
|
||||
LogicalSize, LogicalVector,
|
||||
};
|
||||
use crate::properties::PropertyTracker;
|
||||
use crate::window::WindowAdapter;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::pin::Pin;
|
||||
|
||||
/// This structure must be present in items that are Rendered and contains information.
|
||||
/// Used by the backend.
|
||||
#[derive(Default, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct CachedRenderingData {
|
||||
/// Used and modified by the backend, should be initialized to 0 by the user code
|
||||
pub(crate) cache_index: Cell<usize>,
|
||||
/// Used and modified by the backend, should be initialized to 0 by the user code.
|
||||
/// The backend compares this generation against the one of the cache to verify
|
||||
/// the validity of the cache_index field.
|
||||
pub(crate) cache_generation: Cell<usize>,
|
||||
}
|
||||
|
||||
impl CachedRenderingData {
|
||||
/// This function can be used to remove an entry from the rendering cache for a given item, if it
|
||||
/// exists, i.e. if any data was ever cached. This is typically called by the graphics backend's
|
||||
/// implementation of the release_item_graphics_cache function.
|
||||
fn release(
|
||||
&self,
|
||||
cache: &mut PartialRendererCache,
|
||||
) -> Option<CachedItemBoundingBoxAndTransform> {
|
||||
if self.cache_generation.get() == cache.generation() {
|
||||
let index = self.cache_index.get();
|
||||
self.cache_generation.set(0);
|
||||
Some(cache.remove(index).data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the value if it is in the cache
|
||||
fn get_entry<'a>(
|
||||
&self,
|
||||
cache: &'a mut PartialRendererCache,
|
||||
) -> Option<&'a mut PartialRenderingCachedData> {
|
||||
let index = self.cache_index.get();
|
||||
if self.cache_generation.get() == cache.generation() { cache.get_mut(index) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
/// After rendering an item, we cache the geometry and the transform it applies to
|
||||
/// children.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum CachedItemBoundingBoxAndTransform {
|
||||
/// A regular item with a translation
|
||||
RegularItem {
|
||||
/// The item's bounding rect relative to its parent.
|
||||
bounding_rect: LogicalRect,
|
||||
/// The item's offset relative to its parent.
|
||||
offset: LogicalVector,
|
||||
},
|
||||
/// An item such as Rotate that defines an additional transformation
|
||||
ItemWithTransform {
|
||||
/// The item's bounding rect relative to its parent.
|
||||
bounding_rect: LogicalRect,
|
||||
/// The item's transform to apply to children.
|
||||
transform: Box<ItemTransform>,
|
||||
},
|
||||
/// A clip item.
|
||||
ClipItem {
|
||||
/// The item's geometry relative to its parent.
|
||||
geometry: LogicalRect,
|
||||
},
|
||||
}
|
||||
|
||||
impl CachedItemBoundingBoxAndTransform {
|
||||
fn bounding_rect(&self) -> &LogicalRect {
|
||||
match self {
|
||||
CachedItemBoundingBoxAndTransform::RegularItem { bounding_rect, .. } => bounding_rect,
|
||||
CachedItemBoundingBoxAndTransform::ItemWithTransform { bounding_rect, .. } => {
|
||||
bounding_rect
|
||||
}
|
||||
CachedItemBoundingBoxAndTransform::ClipItem { geometry } => geometry,
|
||||
}
|
||||
}
|
||||
|
||||
fn transform(&self) -> ItemTransform {
|
||||
match self {
|
||||
CachedItemBoundingBoxAndTransform::RegularItem { offset, .. } => {
|
||||
ItemTransform::translation(offset.x as f32, offset.y as f32)
|
||||
}
|
||||
CachedItemBoundingBoxAndTransform::ItemWithTransform { transform, .. } => **transform,
|
||||
CachedItemBoundingBoxAndTransform::ClipItem { geometry } => {
|
||||
ItemTransform::translation(geometry.origin.x as f32, geometry.origin.y as f32)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new<T: ItemRendererFeatures>(
|
||||
item_rc: &ItemRc,
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
) -> Self {
|
||||
let geometry = item_rc.geometry();
|
||||
|
||||
if item_rc.borrow().as_ref().clips_children() {
|
||||
return Self::ClipItem { geometry };
|
||||
}
|
||||
|
||||
// Evaluate the bounding rect untracked, as properties that affect the bounding rect are already tracked
|
||||
// at rendering time.
|
||||
let bounding_rect = crate::properties::evaluate_no_tracking(|| {
|
||||
item_rc.bounding_rect(&geometry, window_adapter)
|
||||
});
|
||||
|
||||
if let Some(complex_child_transform) = (T::SUPPORTS_TRANSFORMATIONS
|
||||
&& window_adapter.renderer().supports_transformations())
|
||||
.then(|| item_rc.children_transform())
|
||||
.flatten()
|
||||
{
|
||||
Self::ItemWithTransform {
|
||||
bounding_rect,
|
||||
transform: complex_child_transform
|
||||
.then_translate(geometry.origin.to_vector().cast())
|
||||
.into(),
|
||||
}
|
||||
} else {
|
||||
Self::RegularItem { bounding_rect, offset: geometry.origin.to_vector() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PartialRenderingCachedData {
|
||||
/// The geometry of the item as it was previously rendered.
|
||||
pub data: CachedItemBoundingBoxAndTransform,
|
||||
/// The property tracker that should be used to evaluate whether the item needs to be re-rendered
|
||||
pub tracker: Option<core::pin::Pin<Box<PropertyTracker>>>,
|
||||
}
|
||||
impl PartialRenderingCachedData {
|
||||
fn new(data: CachedItemBoundingBoxAndTransform) -> Self {
|
||||
Self { data, tracker: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// The cache that needs to be held by the Window for the partial rendering
|
||||
struct PartialRendererCache {
|
||||
slab: slab::Slab<PartialRenderingCachedData>,
|
||||
generation: usize,
|
||||
}
|
||||
|
||||
impl Default for PartialRendererCache {
|
||||
fn default() -> Self {
|
||||
Self { slab: Default::default(), generation: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialRendererCache {
|
||||
/// Returns the generation of the cache. The generation starts at 1 and is increased
|
||||
/// whenever the cache is cleared, for example when the GL context is lost.
|
||||
pub fn generation(&self) -> usize {
|
||||
self.generation
|
||||
}
|
||||
|
||||
/// Retrieves a mutable reference to the cached graphics data at index.
|
||||
pub fn get_mut(&mut self, index: usize) -> Option<&mut PartialRenderingCachedData> {
|
||||
self.slab.get_mut(index)
|
||||
}
|
||||
|
||||
/// Inserts data into the cache and returns the index for retrieval later.
|
||||
pub fn insert(&mut self, data: PartialRenderingCachedData) -> usize {
|
||||
self.slab.insert(data)
|
||||
}
|
||||
|
||||
/// Removes the cached graphics data at the given index.
|
||||
pub fn remove(&mut self, index: usize) -> PartialRenderingCachedData {
|
||||
self.slab.remove(index)
|
||||
}
|
||||
|
||||
/// Removes all entries from the cache and increases the cache's generation count, so
|
||||
/// that stale index access can be avoided.
|
||||
pub fn clear(&mut self) {
|
||||
self.slab.clear();
|
||||
self.generation += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// A region composed of a few rectangles that need to be redrawn.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct DirtyRegion {
|
||||
rectangles: [euclid::Box2D<Coord, LogicalPx>; Self::MAX_COUNT],
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for DirtyRegion {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{:?}", &self.rectangles[..self.count])
|
||||
}
|
||||
}
|
||||
|
||||
impl DirtyRegion {
|
||||
/// The maximum number of rectangles that can be stored in a DirtyRegion
|
||||
pub const MAX_COUNT: usize = 3;
|
||||
|
||||
/// An iterator over the part of the region (they can overlap)
|
||||
pub fn iter(&self) -> impl Iterator<Item = euclid::Box2D<Coord, LogicalPx>> + '_ {
|
||||
(0..self.count).map(|x| self.rectangles[x])
|
||||
}
|
||||
|
||||
/// Add a rectangle to the region.
|
||||
///
|
||||
/// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union.
|
||||
pub fn add_rect(&mut self, rect: LogicalRect) {
|
||||
self.add_box(rect.to_box2d());
|
||||
}
|
||||
|
||||
/// Add a box to the region
|
||||
///
|
||||
/// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union.
|
||||
pub fn add_box(&mut self, b: euclid::Box2D<Coord, LogicalPx>) {
|
||||
if b.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut i = 0;
|
||||
while i < self.count {
|
||||
let r = &self.rectangles[i];
|
||||
if r.contains_box(&b) {
|
||||
// the rectangle is already in the union
|
||||
return;
|
||||
} else if b.contains_box(r) {
|
||||
self.rectangles.swap(i, self.count - 1);
|
||||
self.count -= 1;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if self.count < Self::MAX_COUNT {
|
||||
self.rectangles[self.count] = b;
|
||||
self.count += 1;
|
||||
} else {
|
||||
let best_merge = (0..self.count)
|
||||
.map(|i| (i, self.rectangles[i].union(&b).area() - self.rectangles[i].area()))
|
||||
.min_by(|a, b| PartialOrd::partial_cmp(&a.1, &b.1).unwrap())
|
||||
.expect("There should always be rectangles")
|
||||
.0;
|
||||
self.rectangles[best_merge] = self.rectangles[best_merge].union(&b);
|
||||
}
|
||||
}
|
||||
|
||||
/// Make an union of two regions.
|
||||
///
|
||||
/// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union
|
||||
#[must_use]
|
||||
pub fn union(&self, other: &Self) -> Self {
|
||||
let mut s = self.clone();
|
||||
for o in other.iter() {
|
||||
s.add_box(o)
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Bounding rectangle of the region.
|
||||
#[must_use]
|
||||
pub fn bounding_rect(&self) -> LogicalRect {
|
||||
if self.count == 0 {
|
||||
return Default::default();
|
||||
}
|
||||
let mut r = self.rectangles[0];
|
||||
for i in 1..self.count {
|
||||
r = r.union(&self.rectangles[i]);
|
||||
}
|
||||
r.to_rect()
|
||||
}
|
||||
|
||||
/// Intersection of a region and a rectangle.
|
||||
#[must_use]
|
||||
pub fn intersection(&self, other: LogicalRect) -> DirtyRegion {
|
||||
let mut ret = self.clone();
|
||||
let other = other.to_box2d();
|
||||
let mut i = 0;
|
||||
while i < ret.count {
|
||||
if let Some(x) = ret.rectangles[i].intersection(&other) {
|
||||
ret.rectangles[i] = x;
|
||||
} else {
|
||||
ret.count -= 1;
|
||||
ret.rectangles.swap(i, ret.count);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn draw_intersects(&self, clipped_geom: LogicalRect) -> bool {
|
||||
let b = clipped_geom.to_box2d();
|
||||
self.iter().any(|r| r.intersects(&b))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogicalRect> for DirtyRegion {
|
||||
fn from(value: LogicalRect) -> Self {
|
||||
let mut s = Self::default();
|
||||
s.add_rect(value);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum describes which parts of the buffer passed to the `SoftwareRenderer` may be re-used to speed up painting.
|
||||
// FIXME: #[non_exhaustive] #3023
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Default, Copy)]
|
||||
pub enum RepaintBufferType {
|
||||
#[default]
|
||||
/// The full window is always redrawn. No attempt at partial rendering will be made.
|
||||
NewBuffer,
|
||||
/// Only redraw the parts that have changed since the previous call to render().
|
||||
///
|
||||
/// This variant assumes that the same buffer is passed on every call to render() and
|
||||
/// that it still contains the previously rendered frame.
|
||||
ReusedBuffer,
|
||||
|
||||
/// Redraw the part that have changed since the last two frames were drawn.
|
||||
///
|
||||
/// This is used when using double buffering and swapping of the buffers.
|
||||
SwappedBuffers,
|
||||
}
|
||||
|
||||
/// Put this structure in the renderer to help with partial rendering
|
||||
///
|
||||
/// This is constructed from a [`PartialRenderingState`]
|
||||
pub struct PartialRenderer<'a, T> {
|
||||
cache: &'a RefCell<PartialRendererCache>,
|
||||
/// The region of the screen which is considered dirty and that should be repainted
|
||||
pub dirty_region: DirtyRegion,
|
||||
/// The actual renderer which the drawing call will be forwarded to
|
||||
pub actual_renderer: T,
|
||||
/// The window adapter the renderer is rendering into.
|
||||
pub window_adapter: Rc<dyn WindowAdapter>,
|
||||
}
|
||||
|
||||
impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> {
|
||||
/// Create a new PartialRenderer
|
||||
fn new(
|
||||
cache: &'a RefCell<PartialRendererCache>,
|
||||
initial_dirty_region: DirtyRegion,
|
||||
actual_renderer: T,
|
||||
) -> Self {
|
||||
let window_adapter = actual_renderer.window().window_adapter();
|
||||
Self { cache, dirty_region: initial_dirty_region, actual_renderer, window_adapter }
|
||||
}
|
||||
|
||||
/// Visit the tree of item and compute what are the dirty regions
|
||||
pub fn compute_dirty_regions(
|
||||
&mut self,
|
||||
component: &ItemTreeRc,
|
||||
origin: LogicalPoint,
|
||||
size: LogicalSize,
|
||||
) {
|
||||
#[derive(Clone, Copy)]
|
||||
struct ComputeDirtyRegionState {
|
||||
transform_to_screen: ItemTransform,
|
||||
old_transform_to_screen: ItemTransform,
|
||||
clipped: LogicalRect,
|
||||
must_refresh_children: bool,
|
||||
}
|
||||
|
||||
impl ComputeDirtyRegionState {
|
||||
/// Adjust transform_to_screen and old_transform_to_screen to map from item coordinates
|
||||
/// to the screen when using it on a child, specified by its children transform.
|
||||
fn adjust_transforms_for_child(
|
||||
&mut self,
|
||||
children_transform: &ItemTransform,
|
||||
old_children_transform: &ItemTransform,
|
||||
) {
|
||||
self.transform_to_screen = children_transform.then(&self.transform_to_screen);
|
||||
self.old_transform_to_screen =
|
||||
old_children_transform.then(&self.old_transform_to_screen);
|
||||
}
|
||||
}
|
||||
|
||||
crate::item_tree::visit_items(
|
||||
component,
|
||||
crate::item_tree::TraversalOrder::BackToFront,
|
||||
|component, item, index, state| {
|
||||
let mut new_state = *state;
|
||||
let item_rc = ItemRc::new(component.clone(), index);
|
||||
let new_geom =
|
||||
CachedItemBoundingBoxAndTransform::new::<T>(&item_rc, &self.window_adapter);
|
||||
|
||||
let rendering_data = item.cached_rendering_data_offset();
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
match rendering_data.get_entry(&mut cache) {
|
||||
Some(PartialRenderingCachedData { data: cached_geom, tracker }) => {
|
||||
let rendering_dirty = tracker.as_ref().is_some_and(|tr| tr.is_dirty());
|
||||
let old_geom = cached_geom.clone();
|
||||
|
||||
let geometry_changed = old_geom != new_geom;
|
||||
if ItemRef::downcast_pin::<Clip>(item).is_some()
|
||||
|| ItemRef::downcast_pin::<Opacity>(item).is_some()
|
||||
{
|
||||
// When the opacity or the clip change, this will impact all the children, including
|
||||
// the ones outside the element, regardless if they are themselves dirty or not.
|
||||
new_state.must_refresh_children |= rendering_dirty || geometry_changed;
|
||||
|
||||
if rendering_dirty {
|
||||
// Destroy the tracker as we we might not re-render this clipped item but it would stay dirty
|
||||
*tracker = None;
|
||||
}
|
||||
}
|
||||
|
||||
if geometry_changed {
|
||||
self.mark_dirty_rect(
|
||||
old_geom.bounding_rect(),
|
||||
state.old_transform_to_screen,
|
||||
&state.clipped,
|
||||
);
|
||||
self.mark_dirty_rect(
|
||||
new_geom.bounding_rect(),
|
||||
state.transform_to_screen,
|
||||
&state.clipped,
|
||||
);
|
||||
|
||||
new_state.adjust_transforms_for_child(
|
||||
&new_geom.transform(),
|
||||
&old_geom.transform(),
|
||||
);
|
||||
|
||||
*cached_geom = new_geom;
|
||||
|
||||
return ItemVisitorResult::Continue(new_state);
|
||||
}
|
||||
|
||||
new_state.adjust_transforms_for_child(
|
||||
&cached_geom.transform(),
|
||||
&cached_geom.transform(),
|
||||
);
|
||||
|
||||
if rendering_dirty {
|
||||
self.mark_dirty_rect(
|
||||
cached_geom.bounding_rect(),
|
||||
state.transform_to_screen,
|
||||
&state.clipped,
|
||||
);
|
||||
|
||||
ItemVisitorResult::Continue(new_state)
|
||||
} else {
|
||||
if state.must_refresh_children
|
||||
|| new_state.transform_to_screen
|
||||
!= new_state.old_transform_to_screen
|
||||
{
|
||||
self.mark_dirty_rect(
|
||||
cached_geom.bounding_rect(),
|
||||
state.old_transform_to_screen,
|
||||
&state.clipped,
|
||||
);
|
||||
self.mark_dirty_rect(
|
||||
cached_geom.bounding_rect(),
|
||||
state.transform_to_screen,
|
||||
&state.clipped,
|
||||
);
|
||||
} else if let Some(tr) = &tracker {
|
||||
tr.as_ref().register_as_dependency_to_current_binding();
|
||||
}
|
||||
|
||||
if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } =
|
||||
&cached_geom
|
||||
{
|
||||
new_state.clipped = new_state
|
||||
.clipped
|
||||
.intersection(
|
||||
&state
|
||||
.transform_to_screen
|
||||
.outer_transformed_rect(&geometry.cast())
|
||||
.cast()
|
||||
.union(
|
||||
&state
|
||||
.old_transform_to_screen
|
||||
.outer_transformed_rect(&geometry.cast())
|
||||
.cast(),
|
||||
),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if new_state.clipped.is_empty() {
|
||||
return ItemVisitorResult::SkipChildren;
|
||||
}
|
||||
}
|
||||
ItemVisitorResult::Continue(new_state)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let cache_entry = PartialRenderingCachedData::new(new_geom.clone());
|
||||
rendering_data.cache_index.set(cache.insert(cache_entry));
|
||||
rendering_data.cache_generation.set(cache.generation());
|
||||
|
||||
new_state.adjust_transforms_for_child(
|
||||
&new_geom.transform(),
|
||||
&new_geom.transform(),
|
||||
);
|
||||
|
||||
if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = new_geom {
|
||||
new_state.clipped = new_state
|
||||
.clipped
|
||||
.intersection(
|
||||
&state
|
||||
.transform_to_screen
|
||||
.outer_transformed_rect(&geometry.cast())
|
||||
.cast(),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
self.mark_dirty_rect(
|
||||
new_geom.bounding_rect(),
|
||||
state.transform_to_screen,
|
||||
&state.clipped,
|
||||
);
|
||||
if new_state.clipped.is_empty() {
|
||||
ItemVisitorResult::SkipChildren
|
||||
} else {
|
||||
ItemVisitorResult::Continue(new_state)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
let initial_transform =
|
||||
euclid::Transform2D::translation(origin.x as f32, origin.y as f32);
|
||||
ComputeDirtyRegionState {
|
||||
transform_to_screen: initial_transform,
|
||||
old_transform_to_screen: initial_transform,
|
||||
clipped: LogicalRect::from_size(size),
|
||||
must_refresh_children: false,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn mark_dirty_rect(
|
||||
&mut self,
|
||||
rect: &LogicalRect,
|
||||
transform: ItemTransform,
|
||||
clip_rect: &LogicalRect,
|
||||
) {
|
||||
#[cfg(not(slint_int_coord))]
|
||||
if !rect.origin.is_finite() {
|
||||
// Account for NaN
|
||||
return;
|
||||
}
|
||||
|
||||
if !rect.is_empty()
|
||||
&& let Some(rect) =
|
||||
transform.outer_transformed_rect(&rect.cast()).cast().intersection(clip_rect)
|
||||
{
|
||||
self.dirty_region.add_rect(rect);
|
||||
}
|
||||
}
|
||||
|
||||
fn do_rendering(
|
||||
cache: &RefCell<PartialRendererCache>,
|
||||
rendering_data: &CachedRenderingData,
|
||||
item_rc: &ItemRc,
|
||||
render_fn: impl FnOnce(),
|
||||
) {
|
||||
let mut cache = cache.borrow_mut();
|
||||
if let Some(entry) = rendering_data.get_entry(&mut cache) {
|
||||
entry
|
||||
.tracker
|
||||
.get_or_insert_with(|| Box::pin(PropertyTracker::default()))
|
||||
.as_ref()
|
||||
.evaluate(render_fn);
|
||||
} else {
|
||||
// This item was created between the computation of the dirty region and the actual rendering.
|
||||
// Register a dependency to the geometry since this wasn't done before
|
||||
item_rc.geometry();
|
||||
render_fn();
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the actual renderer
|
||||
pub fn into_inner(self) -> T {
|
||||
self.actual_renderer
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! forward_rendering_call {
|
||||
(fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => {
|
||||
fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize) $(-> $Ret)? {
|
||||
let mut ret = None;
|
||||
Self::do_rendering(&self.cache, &obj.cached_rendering_data, item_rc, || {
|
||||
ret = Some(self.actual_renderer.$fn(obj, item_rc, size));
|
||||
});
|
||||
ret.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! forward_rendering_call2 {
|
||||
(fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => {
|
||||
fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize, cache: &CachedRenderingData) $(-> $Ret)? {
|
||||
let mut ret = None;
|
||||
Self::do_rendering(&self.cache, &cache, item_rc, || {
|
||||
ret = Some(self.actual_renderer.$fn(obj, item_rc, size, &cache));
|
||||
});
|
||||
ret.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<T: ItemRenderer + ItemRendererFeatures> ItemRenderer for PartialRenderer<'_, T> {
|
||||
fn filter_item(
|
||||
&mut self,
|
||||
item_rc: &ItemRc,
|
||||
window_adapter: &Rc<dyn WindowAdapter>,
|
||||
) -> (bool, LogicalRect) {
|
||||
let item = item_rc.borrow();
|
||||
|
||||
// Query untracked, as the bounding rect calculation already registers a dependency on the geometry.
|
||||
let item_geometry = crate::properties::evaluate_no_tracking(|| item_rc.geometry());
|
||||
|
||||
let rendering_data = item.cached_rendering_data_offset();
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
let item_bounding_rect = match rendering_data.get_entry(&mut cache) {
|
||||
Some(PartialRenderingCachedData { data, tracker: _ }) => *data.bounding_rect(),
|
||||
None => {
|
||||
// This item was created between the computation of the dirty region and the actual rendering.
|
||||
item_rc.bounding_rect(&item_geometry, window_adapter)
|
||||
}
|
||||
};
|
||||
|
||||
let clipped_geom = self.get_current_clip().intersection(&item_bounding_rect);
|
||||
let draw = clipped_geom.is_some_and(|clipped_geom| {
|
||||
let screen_geom =
|
||||
self.current_transform().outer_transformed_rect(&clipped_geom.cast()).cast();
|
||||
self.dirty_region.draw_intersects(screen_geom)
|
||||
});
|
||||
|
||||
(draw, item_geometry)
|
||||
}
|
||||
|
||||
forward_rendering_call2!(fn draw_rectangle(dyn RenderRectangle));
|
||||
forward_rendering_call2!(fn draw_border_rectangle(dyn RenderBorderRectangle));
|
||||
forward_rendering_call2!(fn draw_window_background(dyn RenderRectangle));
|
||||
forward_rendering_call2!(fn draw_image(dyn RenderImage));
|
||||
forward_rendering_call2!(fn draw_text(dyn RenderText));
|
||||
forward_rendering_call!(fn draw_text_input(TextInput));
|
||||
#[cfg(feature = "path")]
|
||||
forward_rendering_call!(fn draw_path(Path));
|
||||
forward_rendering_call!(fn draw_box_shadow(BoxShadow));
|
||||
|
||||
forward_rendering_call!(fn visit_clip(Clip) -> RenderingResult);
|
||||
forward_rendering_call!(fn visit_opacity(Opacity) -> RenderingResult);
|
||||
forward_rendering_call!(fn visit_layer(Layer) -> RenderingResult);
|
||||
|
||||
fn combine_clip(
|
||||
&mut self,
|
||||
rect: LogicalRect,
|
||||
radius: LogicalBorderRadius,
|
||||
border_width: LogicalLength,
|
||||
) -> bool {
|
||||
self.actual_renderer.combine_clip(rect, radius, border_width)
|
||||
}
|
||||
|
||||
fn get_current_clip(&self) -> LogicalRect {
|
||||
self.actual_renderer.get_current_clip()
|
||||
}
|
||||
|
||||
fn translate(&mut self, distance: LogicalVector) {
|
||||
self.actual_renderer.translate(distance)
|
||||
}
|
||||
fn current_transform(&self) -> ItemTransform {
|
||||
self.actual_renderer.current_transform()
|
||||
}
|
||||
|
||||
fn rotate(&mut self, angle_in_degrees: f32) {
|
||||
self.actual_renderer.rotate(angle_in_degrees)
|
||||
}
|
||||
|
||||
fn scale(&mut self, x_factor: f32, y_factor: f32) {
|
||||
self.actual_renderer.scale(x_factor, y_factor)
|
||||
}
|
||||
|
||||
fn apply_opacity(&mut self, opacity: f32) {
|
||||
self.actual_renderer.apply_opacity(opacity)
|
||||
}
|
||||
|
||||
fn save_state(&mut self) {
|
||||
self.actual_renderer.save_state()
|
||||
}
|
||||
|
||||
fn restore_state(&mut self) {
|
||||
self.actual_renderer.restore_state()
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.actual_renderer.scale_factor()
|
||||
}
|
||||
|
||||
fn draw_cached_pixmap(
|
||||
&mut self,
|
||||
item_rc: &ItemRc,
|
||||
update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
|
||||
) {
|
||||
self.actual_renderer.draw_cached_pixmap(item_rc, update_fn)
|
||||
}
|
||||
|
||||
fn draw_string(&mut self, string: &str, color: crate::Color) {
|
||||
self.actual_renderer.draw_string(string, color)
|
||||
}
|
||||
|
||||
fn draw_image_direct(&mut self, image: crate::graphics::image::Image) {
|
||||
self.actual_renderer.draw_image_direct(image)
|
||||
}
|
||||
|
||||
fn window(&self) -> &crate::window::WindowInner {
|
||||
self.actual_renderer.window()
|
||||
}
|
||||
|
||||
fn as_any(&mut self) -> Option<&mut dyn core::any::Any> {
|
||||
self.actual_renderer.as_any()
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct holds the state of the partial renderer between different frames, in particular the cache of the bounding rect
|
||||
/// of each item. This permits a more fine-grained computation of the region that needs to be repainted.
|
||||
#[derive(Default)]
|
||||
pub struct PartialRenderingState {
|
||||
partial_cache: RefCell<PartialRendererCache>,
|
||||
/// This is the area which we are going to redraw in the next frame, no matter if the items are dirty or not
|
||||
force_dirty: RefCell<DirtyRegion>,
|
||||
/// Force a redraw in the next frame, no matter what's dirty. Use only as a last resort.
|
||||
force_screen_refresh: Cell<bool>,
|
||||
}
|
||||
|
||||
impl PartialRenderingState {
|
||||
/// Creates a partial renderer that's initialized with the partial rendering caches maintained in this state structure.
|
||||
/// Call [`Self::apply_dirty_region`] after this function to compute the correct partial rendering region.
|
||||
pub fn create_partial_renderer<T: ItemRenderer + ItemRendererFeatures>(
|
||||
&self,
|
||||
renderer: T,
|
||||
) -> PartialRenderer<'_, T> {
|
||||
PartialRenderer::new(&self.partial_cache, self.force_dirty.take(), renderer)
|
||||
}
|
||||
|
||||
/// Compute the correct partial rendering region based on the components to be drawn, the bounding rectangles of
|
||||
/// changes items within, and the current repaint buffer type. Returns the computed dirty region just for this frame.
|
||||
/// The provided buffer_dirty_region specifies which area of the buffer is known to *additionally* require repainting,
|
||||
/// where `None` means that buffer is not known to be dirty beyond what applies to this frame (reused buffer).
|
||||
pub fn apply_dirty_region<T: ItemRenderer + ItemRendererFeatures>(
|
||||
&self,
|
||||
partial_renderer: &mut PartialRenderer<'_, T>,
|
||||
components: &[(ItemTreeWeak, LogicalPoint)],
|
||||
logical_window_size: LogicalSize,
|
||||
dirty_region_of_existing_buffer: Option<DirtyRegion>,
|
||||
) -> DirtyRegion {
|
||||
for (component, origin) in components {
|
||||
if let Some(component) = crate::item_tree::ItemTreeWeak::upgrade(component) {
|
||||
partial_renderer.compute_dirty_regions(&component, *origin, logical_window_size);
|
||||
}
|
||||
}
|
||||
|
||||
let screen_region = LogicalRect::from_size(logical_window_size);
|
||||
|
||||
if self.force_screen_refresh.take() {
|
||||
partial_renderer.dirty_region = screen_region.into();
|
||||
}
|
||||
|
||||
let region_to_repaint = partial_renderer.dirty_region.clone();
|
||||
|
||||
partial_renderer.dirty_region = match dirty_region_of_existing_buffer {
|
||||
Some(dirty_region) => partial_renderer.dirty_region.union(&dirty_region),
|
||||
None => partial_renderer.dirty_region.clone(),
|
||||
}
|
||||
.intersection(screen_region);
|
||||
|
||||
region_to_repaint
|
||||
}
|
||||
|
||||
/// Add the specified region to the list of regions to include in the next rendering.
|
||||
pub fn mark_dirty_region(&self, region: DirtyRegion) {
|
||||
self.force_dirty.replace_with(|r| r.union(®ion));
|
||||
}
|
||||
|
||||
/// Call this from your renderer's `free_graphics_resources` function to ensure that the cached item geometries
|
||||
/// are cleared for the destroyed items in the item tree.
|
||||
pub fn free_graphics_resources(&self, items: &mut dyn Iterator<Item = Pin<ItemRef<'_>>>) {
|
||||
for item in items {
|
||||
item.cached_rendering_data_offset().release(&mut self.partial_cache.borrow_mut());
|
||||
}
|
||||
|
||||
// We don't have a way to determine the screen region of the delete items, what's in the cache is relative. So
|
||||
// as a last resort, refresh everything.
|
||||
self.force_screen_refresh.set(true)
|
||||
}
|
||||
|
||||
/// Clears the partial rendering cache. Use this for example when the entire underlying window surface changes.
|
||||
pub fn clear_cache(&self) {
|
||||
self.partial_cache.borrow_mut().clear();
|
||||
}
|
||||
|
||||
/// Force re-rendering of the entire window region the next time a partial renderer is created.
|
||||
pub fn force_screen_refresh(&self) {
|
||||
self.force_screen_refresh.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_region_no_intersection() {
|
||||
let mut region = DirtyRegion::default();
|
||||
region.add_rect(LogicalRect::new(LogicalPoint::new(10., 10.), LogicalSize::new(16., 16.)));
|
||||
region.add_rect(LogicalRect::new(LogicalPoint::new(100., 100.), LogicalSize::new(16., 16.)));
|
||||
region.add_rect(LogicalRect::new(LogicalPoint::new(200., 100.), LogicalSize::new(16., 16.)));
|
||||
let i = region
|
||||
.intersection(LogicalRect::new(LogicalPoint::new(50., 50.), LogicalSize::new(10., 10.)));
|
||||
assert_eq!(i.iter().count(), 0);
|
||||
}
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
The backend is the abstraction for crates that need to do the actual drawing and event loop
|
||||
*/
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use crate::SharedString;
|
||||
pub use crate::api::PlatformError;
|
||||
use crate::api::{LogicalPosition, LogicalSize};
|
||||
pub use crate::renderer::Renderer;
|
||||
#[cfg(all(not(feature = "std"), feature = "unsafe-single-threaded"))]
|
||||
use crate::unsafe_single_threaded::OnceCell;
|
||||
pub use crate::window::{LayoutConstraints, WindowAdapter, WindowProperties};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::String;
|
||||
#[cfg(all(feature = "std", not(target_os = "android")))]
|
||||
use once_cell::sync::OnceCell;
|
||||
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
|
||||
use std::time;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use web_time as time;
|
||||
|
||||
/// This trait defines the interface between Slint and platform APIs typically provided by operating and windowing systems.
|
||||
pub trait Platform {
|
||||
/// Instantiate a window for a component.
|
||||
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError>;
|
||||
|
||||
/// Spins an event loop and renders the visible windows.
|
||||
fn run_event_loop(&self) -> Result<(), PlatformError> {
|
||||
Err(PlatformError::NoEventLoopProvider)
|
||||
}
|
||||
|
||||
/// Spins an event loop for a specified period of time.
|
||||
///
|
||||
/// This function is similar to `run_event_loop()` with two differences:
|
||||
/// * The function is expected to return after the provided timeout, but
|
||||
/// allow for subsequent invocations to resume the previous loop. The
|
||||
/// function can return earlier if the loop was terminated otherwise,
|
||||
/// for example by `quit_event_loop()` or a last-window-closed mechanism.
|
||||
/// * If the timeout is zero, the implementation should merely peek and
|
||||
/// process any pending events, but then return immediately.
|
||||
///
|
||||
/// When the function returns `ControlFlow::Continue`, it is assumed that
|
||||
/// the loop remains intact and that in the future the caller should call
|
||||
/// `process_events()` again, to permit the user to continue to interact with
|
||||
/// windows.
|
||||
/// When the function returns `ControlFlow::Break`, it is assumed that the
|
||||
/// event loop was terminated. Any subsequent calls to `process_events()`
|
||||
/// will start the event loop afresh.
|
||||
#[doc(hidden)]
|
||||
fn process_events(
|
||||
&self,
|
||||
_timeout: core::time::Duration,
|
||||
_: crate::InternalToken,
|
||||
) -> Result<core::ops::ControlFlow<()>, PlatformError> {
|
||||
Err(PlatformError::NoEventLoopProvider)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(
|
||||
note = "i-slint-core takes care of closing behavior. Application should call run_event_loop_until_quit"
|
||||
)]
|
||||
/// This is being phased out, see #1499.
|
||||
fn set_event_loop_quit_on_last_window_closed(&self, quit_on_last_window_closed: bool) {
|
||||
assert!(!quit_on_last_window_closed);
|
||||
crate::context::GLOBAL_CONTEXT
|
||||
.with(|ctx| (*ctx.get().unwrap().0.window_count.borrow_mut()) += 1);
|
||||
}
|
||||
|
||||
/// Return an [`EventLoopProxy`] that can be used to send event to the event loop
|
||||
///
|
||||
/// If this function returns `None` (the default implementation), then it will
|
||||
/// not be possible to send event to the event loop and the function
|
||||
/// [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop) and
|
||||
/// [`slint::quit_event_loop()`](crate::api::quit_event_loop) will panic. These
|
||||
/// functions are used internally by `slint::spawn_local()`
|
||||
/// and features like live_preview. Implementing this function is necessary for
|
||||
/// aforementioned functionalities to work.
|
||||
fn new_event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the current time as a monotonic duration since the start of the program
|
||||
///
|
||||
/// This is used by the animations and timer to compute the elapsed time.
|
||||
///
|
||||
/// When the `std` feature is enabled, this function is implemented in terms of
|
||||
/// [`std::time::Instant::now()`], but on `#![no_std]` platform, this function must
|
||||
/// be implemented.
|
||||
fn duration_since_start(&self) -> core::time::Duration {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now);
|
||||
let now = time::Instant::now();
|
||||
assert!(now >= the_beginning, "The platform's clock is not monotonic!");
|
||||
now - the_beginning
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
unimplemented!("The platform abstraction must implement `duration_since_start`")
|
||||
}
|
||||
|
||||
/// Returns the current interval to internal measure the duration to send a double click event.
|
||||
///
|
||||
/// A double click event is a series of two pointer clicks.
|
||||
fn click_interval(&self) -> core::time::Duration {
|
||||
// 500ms is the default delay according to https://en.wikipedia.org/wiki/Double-click#Speed_and_timing
|
||||
core::time::Duration::from_millis(500)
|
||||
}
|
||||
|
||||
/// Returns the current rate at which the text cursor should flash or blink.
|
||||
///
|
||||
/// This is the length of the entire visible-hidden-visible cycle, so for a duration of 1000ms
|
||||
/// it is visible for 500ms then hidden for 500ms, then visible again.
|
||||
///
|
||||
/// If this value is `Duration::ZERO` then the cycle is disabled.
|
||||
fn cursor_flash_cycle(&self) -> core::time::Duration {
|
||||
core::time::Duration::from_millis(1000)
|
||||
}
|
||||
|
||||
/// Sends the given text into the system clipboard.
|
||||
///
|
||||
/// If the platform doesn't support the specified clipboard, this function should do nothing
|
||||
fn set_clipboard_text(&self, _text: &str, _clipboard: Clipboard) {}
|
||||
|
||||
/// Returns a copy of text stored in the system clipboard, if any.
|
||||
///
|
||||
/// If the platform doesn't support the specified clipboard, the function should return None
|
||||
fn clipboard_text(&self, _clipboard: Clipboard) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// This function is called when debug() is used in .slint files. The implementation
|
||||
/// should direct the output to some developer visible terminal. The default implementation
|
||||
/// uses stderr if available, or `console.log` when targeting wasm.
|
||||
fn debug_log(&self, _arguments: core::fmt::Arguments) {
|
||||
crate::tests::default_debug_log(_arguments);
|
||||
}
|
||||
|
||||
/// Opens the given URL in an external browser.
|
||||
///
|
||||
/// Returns [`PlatformError::Unsupported`] if the platform doesn't support opening URLs.
|
||||
fn open_url(&self, _url: &str) -> Result<(), PlatformError> {
|
||||
Err(PlatformError::Unsupported)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[doc(hidden)]
|
||||
/// The long press interval before showing a context menu
|
||||
fn long_press_interval(&self, _: crate::InternalToken) -> core::time::Duration {
|
||||
core::time::Duration::from_millis(500)
|
||||
}
|
||||
}
|
||||
|
||||
/// The clip board, used in [`Platform::clipboard_text`] and [Platform::set_clipboard_text`]
|
||||
#[repr(u8)]
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Clone, Default)]
|
||||
pub enum Clipboard {
|
||||
/// This is the default clipboard used for text action for Ctrl+V, Ctrl+C.
|
||||
/// Corresponds to the secondary clipboard on X11.
|
||||
#[default]
|
||||
DefaultClipboard = 0,
|
||||
|
||||
/// This is the clipboard that is used when text is selected
|
||||
/// Corresponds to the primary clipboard on X11.
|
||||
/// The Platform implementation should do nothing if copy on select is not supported on that platform.
|
||||
SelectionClipboard = 1,
|
||||
}
|
||||
|
||||
/// Trait that is returned by the [`Platform::new_event_loop_proxy`]
|
||||
///
|
||||
/// This are the implementation details for the function that may need to
|
||||
/// communicate with the eventloop from different thread
|
||||
pub trait EventLoopProxy: Send + Sync {
|
||||
/// Exits the event loop.
|
||||
///
|
||||
/// This is what is called by [`slint::quit_event_loop()`](crate::api::quit_event_loop)
|
||||
fn quit_event_loop(&self) -> Result<(), crate::api::EventLoopError>;
|
||||
|
||||
/// Invoke the function from the event loop.
|
||||
///
|
||||
/// This is what is called by [`slint::invoke_from_event_loop()`](crate::api::invoke_from_event_loop)
|
||||
fn invoke_from_event_loop(
|
||||
&self,
|
||||
event: Box<dyn FnOnce() + Send>,
|
||||
) -> Result<(), crate::api::EventLoopError>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
static INITIAL_INSTANT: once_cell::sync::OnceCell<time::Instant> = once_cell::sync::OnceCell::new();
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::convert::From<crate::animations::Instant> for time::Instant {
|
||||
fn from(our_instant: crate::animations::Instant) -> Self {
|
||||
let the_beginning = *INITIAL_INSTANT.get_or_init(time::Instant::now);
|
||||
the_beginning + core::time::Duration::from_millis(our_instant.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
static EVENTLOOP_PROXY: OnceCell<Box<dyn EventLoopProxy + 'static>> = OnceCell::new();
|
||||
|
||||
// On android, we allow the platform to be reset and the global eventloop proxy to be replaced.
|
||||
#[cfg(target_os = "android")]
|
||||
static EVENTLOOP_PROXY: std::sync::Mutex<Option<Box<dyn EventLoopProxy + 'static>>> =
|
||||
std::sync::Mutex::new(None);
|
||||
|
||||
pub(crate) fn with_event_loop_proxy<R>(f: impl FnOnce(Option<&dyn EventLoopProxy>) -> R) -> R {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
return f(EVENTLOOP_PROXY.get().map(core::ops::Deref::deref));
|
||||
#[cfg(target_os = "android")]
|
||||
return f(EVENTLOOP_PROXY.lock().unwrap().as_ref().map(core::ops::Deref::deref));
|
||||
}
|
||||
|
||||
/// This enum describes the different error scenarios that may occur when [`set_platform`]
|
||||
/// fails.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[repr(C)]
|
||||
#[non_exhaustive]
|
||||
pub enum SetPlatformError {
|
||||
/// The platform has already been initialized in an earlier call to [`set_platform`].
|
||||
AlreadySet,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SetPlatformError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
SetPlatformError::AlreadySet => {
|
||||
f.write_str("The platform has already been initialized.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for SetPlatformError {}
|
||||
|
||||
/// Set the Slint platform abstraction.
|
||||
///
|
||||
/// If the platform abstraction was already set this will return `Err`.
|
||||
pub fn set_platform(platform: Box<dyn Platform + 'static>) -> Result<(), SetPlatformError> {
|
||||
crate::context::GLOBAL_CONTEXT.with(|instance| {
|
||||
if instance.get().is_some() {
|
||||
return Err(SetPlatformError::AlreadySet);
|
||||
}
|
||||
if let Some(proxy) = platform.new_event_loop_proxy() {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
EVENTLOOP_PROXY.set(proxy).map_err(|_| SetPlatformError::AlreadySet)?;
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
*EVENTLOOP_PROXY.lock().unwrap() = Some(proxy);
|
||||
}
|
||||
}
|
||||
instance
|
||||
.set(crate::SlintContext::new(platform))
|
||||
.map_err(|_| SetPlatformError::AlreadySet)
|
||||
.unwrap();
|
||||
// Ensure a sane starting point for the animation tick.
|
||||
update_timers_and_animations();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Call this function to update and potentially activate any pending timers, as well
|
||||
/// as advance the state of any active animations.
|
||||
///
|
||||
/// This function should be called before rendering or processing input event, at the
|
||||
/// beginning of each event loop iteration.
|
||||
pub fn update_timers_and_animations() {
|
||||
crate::animations::update_animations();
|
||||
crate::timers::TimerList::maybe_activate_timers(crate::animations::Instant::now());
|
||||
crate::properties::ChangeTracker::run_change_handlers();
|
||||
}
|
||||
|
||||
/// Returns the duration before the next timer is expected to be activated. This is the
|
||||
/// largest amount of time that you can wait before calling [`update_timers_and_animations()`].
|
||||
///
|
||||
/// `None` is returned if there is no active timer.
|
||||
///
|
||||
/// Call this in your own event loop implementation to know how long the current thread can
|
||||
/// go to sleep. Note that this does not take currently activate animations into account.
|
||||
/// Only go to sleep if [`Window::has_active_animations()`](crate::api::Window::has_active_animations())
|
||||
/// returns false.
|
||||
pub fn duration_until_next_timer_update() -> Option<core::time::Duration> {
|
||||
crate::timers::TimerList::next_timeout().map(|timeout| {
|
||||
let duration_since_start = crate::context::GLOBAL_CONTEXT
|
||||
.with(|p| p.get().map(|p| p.platform().duration_since_start()))
|
||||
.unwrap_or_default();
|
||||
core::time::Duration::from_millis(
|
||||
timeout.0.saturating_sub(duration_since_start.as_millis() as u64),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// reexport key enum to the public api
|
||||
pub use crate::input::PointerEventButton;
|
||||
pub use crate::input::key_codes::Key;
|
||||
|
||||
/// A event that describes user input or windowing system events.
|
||||
///
|
||||
/// Slint backends typically receive events from the windowing system, translate them to this
|
||||
/// enum and deliver them to the scene of items via [`slint::Window::try_dispatch_event()`](`crate::api::Window::try_dispatch_event()`).
|
||||
///
|
||||
/// The pointer variants describe events originating from an input device such as a mouse
|
||||
/// or a contact point on a touch-enabled surface.
|
||||
///
|
||||
/// All position fields are in logical window coordinates.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
#[repr(u32)]
|
||||
pub enum WindowEvent {
|
||||
/// A pointer was pressed.
|
||||
PointerPressed {
|
||||
position: LogicalPosition,
|
||||
/// The button that was pressed.
|
||||
button: PointerEventButton,
|
||||
},
|
||||
/// A pointer was released.
|
||||
PointerReleased {
|
||||
position: LogicalPosition,
|
||||
/// The button that was released.
|
||||
button: PointerEventButton,
|
||||
},
|
||||
/// The position of the pointer has changed.
|
||||
PointerMoved { position: LogicalPosition },
|
||||
/// The wheel button of a mouse was rotated to initiate scrolling.
|
||||
PointerScrolled {
|
||||
position: LogicalPosition,
|
||||
/// The amount of logical pixels to scroll in the horizontal direction.
|
||||
delta_x: f32,
|
||||
/// The amount of logical pixels to scroll in the vertical direction.
|
||||
delta_y: f32,
|
||||
},
|
||||
/// The pointer exited the window.
|
||||
PointerExited,
|
||||
/// A key was pressed.
|
||||
KeyPressed {
|
||||
/// The unicode representation of the key pressed.
|
||||
///
|
||||
/// # Example
|
||||
/// A specific key can be mapped to a unicode by using the [`Key`] enum
|
||||
/// ```rust
|
||||
/// let _ = slint::platform::WindowEvent::KeyPressed { text: slint::platform::Key::Shift.into() };
|
||||
/// ```
|
||||
text: SharedString,
|
||||
},
|
||||
/// A key press was auto-repeated.
|
||||
KeyPressRepeated {
|
||||
/// The unicode representation of the key pressed.
|
||||
///
|
||||
/// # Example
|
||||
/// A specific key can be mapped to a unicode by using the [`Key`] enum
|
||||
/// ```rust
|
||||
/// let _ = slint::platform::WindowEvent::KeyPressRepeated { text: slint::platform::Key::Shift.into() };
|
||||
/// ```
|
||||
text: SharedString,
|
||||
},
|
||||
/// A key was released.
|
||||
KeyReleased {
|
||||
/// The unicode representation of the key released.
|
||||
///
|
||||
/// # Example
|
||||
/// A specific key can be mapped to a unicode by using the [`Key`] enum
|
||||
/// ```rust
|
||||
/// let _ = slint::platform::WindowEvent::KeyReleased { text: slint::platform::Key::Shift.into() };
|
||||
/// ```
|
||||
text: SharedString,
|
||||
},
|
||||
/// The window's scale factor has changed. This can happen for example when the display's resolution
|
||||
/// changes, the user selects a new scale factor in the system settings, or the window is moved to a
|
||||
/// different screen.
|
||||
/// Platform implementations should dispatch this event also right after the initial window creation,
|
||||
/// to set the initial scale factor the windowing system provided for the window.
|
||||
ScaleFactorChanged {
|
||||
/// The window system provided scale factor to map logical pixels to physical pixels.
|
||||
scale_factor: f32,
|
||||
},
|
||||
/// The window was resized.
|
||||
///
|
||||
/// The backend must send this event to ensure that the `width` and `height` property of the root Window
|
||||
/// element are properly set.
|
||||
Resized {
|
||||
/// The new logical size of the window
|
||||
size: LogicalSize,
|
||||
},
|
||||
/// The user requested to close the window.
|
||||
///
|
||||
/// The backend should send this event when the user tries to close the window,for example by pressing the close button.
|
||||
///
|
||||
/// This will have the effect of invoking the callback set in [`Window::on_close_requested()`](`crate::api::Window::on_close_requested()`)
|
||||
/// and then hiding the window depending on the return value of the callback.
|
||||
CloseRequested,
|
||||
|
||||
/// The Window was activated or de-activated.
|
||||
///
|
||||
/// The backend should dispatch this event with true when the window gains focus
|
||||
/// and false when the window loses focus.
|
||||
WindowActiveChanged(bool),
|
||||
}
|
||||
|
||||
impl WindowEvent {
|
||||
/// The position of the cursor for this event, if any
|
||||
pub fn position(&self) -> Option<LogicalPosition> {
|
||||
match self {
|
||||
WindowEvent::PointerPressed { position, .. } => Some(*position),
|
||||
WindowEvent::PointerReleased { position, .. } => Some(*position),
|
||||
WindowEvent::PointerMoved { position } => Some(*position),
|
||||
WindowEvent::PointerScrolled { position, .. } => Some(*position),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the animation tick is updated when a platform is set
|
||||
```rust
|
||||
use i_slint_core::platform::*;
|
||||
struct DummyBackend;
|
||||
impl Platform for DummyBackend {
|
||||
fn create_window_adapter(
|
||||
&self,
|
||||
) -> Result<std::rc::Rc<dyn WindowAdapter>, PlatformError> {
|
||||
Err(PlatformError::Other("not implemented".into()))
|
||||
}
|
||||
fn duration_since_start(&self) -> core::time::Duration {
|
||||
core::time::Duration::from_millis(100)
|
||||
}
|
||||
}
|
||||
|
||||
let start_time = i_slint_core::tests::slint_get_mocked_time();
|
||||
i_slint_core::platform::set_platform(Box::new(DummyBackend{}));
|
||||
let time_after_platform_init = i_slint_core::tests::slint_get_mocked_time();
|
||||
assert_ne!(time_after_platform_init, start_time);
|
||||
assert_eq!(time_after_platform_init, 100);
|
||||
```
|
||||
*/
|
||||
#[cfg(doctest)]
|
||||
const _ANIM_TICK_UPDATED_ON_PLATFORM_SET: () = ();
|
||||
+1504
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,425 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use super::{
|
||||
BindingHolder, BindingResult, BindingVTable, DependencyListHead, DependencyNode,
|
||||
single_linked_list_pin::SingleLinkedListPinHead,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use core::cell::{Cell, UnsafeCell};
|
||||
use core::marker::PhantomPinned;
|
||||
use core::pin::Pin;
|
||||
use core::ptr::addr_of;
|
||||
|
||||
// TODO a pinned thread local key?
|
||||
crate::thread_local! {static CHANGED_NODES : Pin<Box<DependencyListHead>> = Box::pin(DependencyListHead::default()) }
|
||||
|
||||
struct ChangeTrackerInner<T, EvalFn, NotifyFn, Data> {
|
||||
eval_fn: EvalFn,
|
||||
notify_fn: NotifyFn,
|
||||
/// The value. Borrowed-mut when `evaluating` is true
|
||||
value: UnsafeCell<T>,
|
||||
data: Data,
|
||||
/// When true, we are currently running eval_fn or notify_fn and we shouldn't be dropped
|
||||
evaluating: Cell<bool>,
|
||||
}
|
||||
|
||||
/// A change tracker is used to run a callback when a property value changes.
|
||||
///
|
||||
/// The Change Tracker must be initialized with the [`Self::init`] method.
|
||||
///
|
||||
/// When the property changes, the ChangeTracker is added to a thread local list, and the notify
|
||||
/// callback is called when the [`Self::run_change_handlers()`] method is called
|
||||
#[derive(Debug)]
|
||||
pub struct ChangeTracker {
|
||||
/// (Actually a `BindingHolder<ChangeTrackerInner>`)
|
||||
inner: Cell<*mut BindingHolder>,
|
||||
}
|
||||
|
||||
impl Default for ChangeTracker {
|
||||
fn default() -> Self {
|
||||
Self { inner: Cell::new(core::ptr::null_mut()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ChangeTracker {
|
||||
fn drop(&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl ChangeTracker {
|
||||
/// Initialize the change tracker with the given data and callbacks.
|
||||
///
|
||||
/// The `data` is any struct that is going to be passed to the functor.
|
||||
/// The `eval_fn` is a function that queries and return the property.
|
||||
/// And the `notify_fn` is the callback run if the property is changed
|
||||
pub fn init<
|
||||
Data: 'static,
|
||||
T: Default + PartialEq,
|
||||
EF: Fn(&Data) -> T + 'static,
|
||||
NF: Fn(&Data, &T) + 'static,
|
||||
>(
|
||||
&self,
|
||||
data: Data,
|
||||
eval_fn: EF,
|
||||
notify_fn: NF,
|
||||
) {
|
||||
self.init_impl(data, eval_fn, notify_fn, false);
|
||||
}
|
||||
|
||||
/// Initialize the change tracker with the given data and callbacks.
|
||||
///
|
||||
/// Same as [`Self::init`], but the first eval function is called in a future evaluation of the event loop.
|
||||
/// This means that the change tracker will consider the value as default initialized, and the eval function will
|
||||
/// be called the firs ttime if the initial value is not equal to the default constructed value.
|
||||
pub fn init_delayed<
|
||||
Data: 'static,
|
||||
T: Default + PartialEq,
|
||||
EF: Fn(&Data) -> T + 'static,
|
||||
NF: Fn(&Data, &T) + 'static,
|
||||
>(
|
||||
&self,
|
||||
data: Data,
|
||||
eval_fn: EF,
|
||||
notify_fn: NF,
|
||||
) {
|
||||
self.init_impl(data, eval_fn, notify_fn, true);
|
||||
}
|
||||
|
||||
fn init_impl<
|
||||
Data: 'static,
|
||||
T: Default + PartialEq,
|
||||
EF: Fn(&Data) -> T + 'static,
|
||||
NF: Fn(&Data, &T) + 'static,
|
||||
>(
|
||||
&self,
|
||||
data: Data,
|
||||
eval_fn: EF,
|
||||
notify_fn: NF,
|
||||
delayed: bool,
|
||||
) {
|
||||
self.clear();
|
||||
let inner = ChangeTrackerInner {
|
||||
eval_fn,
|
||||
notify_fn,
|
||||
value: T::default().into(),
|
||||
data,
|
||||
evaluating: false.into(),
|
||||
};
|
||||
|
||||
unsafe fn evaluate<
|
||||
T: PartialEq,
|
||||
EF: Fn(&Data) -> T + 'static,
|
||||
NF: Fn(&Data, &T) + 'static,
|
||||
Data: 'static,
|
||||
>(
|
||||
_self: *const BindingHolder,
|
||||
_value: *mut (),
|
||||
) -> BindingResult {
|
||||
unsafe {
|
||||
let pinned_holder = Pin::new_unchecked(&*_self);
|
||||
let _self = _self as *const BindingHolder<ChangeTrackerInner<T, EF, NF, Data>>;
|
||||
let inner = core::ptr::addr_of!((*_self).binding).as_ref().unwrap();
|
||||
(*core::ptr::addr_of!((*_self).dep_nodes)).take();
|
||||
assert!(!inner.evaluating.get());
|
||||
inner.evaluating.set(true);
|
||||
let new_value = super::CURRENT_BINDING
|
||||
.set(Some(pinned_holder), || (inner.eval_fn)(&inner.data));
|
||||
{
|
||||
// Safety: We just set `evaluating` to true which means we can borrow
|
||||
let inner_value = &mut *inner.value.get();
|
||||
if new_value != *inner_value {
|
||||
*inner_value = new_value;
|
||||
(inner.notify_fn)(&inner.data, inner_value);
|
||||
}
|
||||
}
|
||||
|
||||
if !inner.evaluating.replace(false) {
|
||||
// `drop` from the vtable was called while evaluating. Do it now.
|
||||
core::mem::drop(Box::from_raw(
|
||||
_self as *mut BindingHolder<ChangeTrackerInner<T, EF, NF, Data>>,
|
||||
));
|
||||
}
|
||||
BindingResult::KeepBinding
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn drop<T, EF, NF, Data>(_self: *mut BindingHolder) {
|
||||
unsafe {
|
||||
let _self = _self as *mut BindingHolder<ChangeTrackerInner<T, EF, NF, Data>>;
|
||||
let evaluating = core::ptr::addr_of!((*_self).binding)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.evaluating
|
||||
.replace(false);
|
||||
if !evaluating {
|
||||
core::mem::drop(Box::from_raw(_self));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait HasBindingVTable {
|
||||
const VT: &'static BindingVTable;
|
||||
}
|
||||
impl<T: PartialEq, EF: Fn(&Data) -> T + 'static, NF: Fn(&Data, &T) + 'static, Data: 'static>
|
||||
HasBindingVTable for ChangeTrackerInner<T, EF, NF, Data>
|
||||
{
|
||||
const VT: &'static BindingVTable = &BindingVTable {
|
||||
drop: drop::<T, EF, NF, Data>,
|
||||
evaluate: evaluate::<T, EF, NF, Data>,
|
||||
mark_dirty: ChangeTracker::mark_dirty,
|
||||
intercept_set: |_, _| false,
|
||||
intercept_set_binding: |_, _| false,
|
||||
};
|
||||
}
|
||||
let holder = BindingHolder {
|
||||
dependencies: Cell::new(0),
|
||||
dep_nodes: Default::default(),
|
||||
vtable: <ChangeTrackerInner<T, EF, NF, Data> as HasBindingVTable>::VT,
|
||||
dirty: Cell::new(false),
|
||||
is_two_way_binding: false,
|
||||
pinned: PhantomPinned,
|
||||
binding: inner,
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name: "<ChangeTracker>".into(),
|
||||
};
|
||||
|
||||
let raw = Box::into_raw(Box::new(holder));
|
||||
unsafe { self.set_internal(raw as *mut BindingHolder) };
|
||||
if delayed {
|
||||
let mut dep_nodes = SingleLinkedListPinHead::default();
|
||||
let node = dep_nodes.push_front(DependencyNode::new(raw as *const BindingHolder));
|
||||
CHANGED_NODES.with(|changed_nodes| {
|
||||
changed_nodes.append(node);
|
||||
});
|
||||
unsafe { (*core::ptr::addr_of_mut!((*raw).dep_nodes)).set(dep_nodes) };
|
||||
return;
|
||||
}
|
||||
let value = unsafe {
|
||||
let pinned_holder = Pin::new_unchecked((raw as *mut BindingHolder).as_ref().unwrap());
|
||||
let inner = core::ptr::addr_of!((*raw).binding).as_ref().unwrap();
|
||||
super::CURRENT_BINDING.set(Some(pinned_holder), || (inner.eval_fn)(&inner.data))
|
||||
};
|
||||
unsafe {
|
||||
*core::ptr::addr_of_mut!((*raw).binding).as_mut().unwrap().value.get_mut() = value
|
||||
};
|
||||
}
|
||||
|
||||
/// Clear the change tracker.
|
||||
/// No notify function will be called after this.
|
||||
pub fn clear(&self) {
|
||||
let inner = self.inner.get();
|
||||
if !inner.is_null() {
|
||||
unsafe {
|
||||
let drop = (*core::ptr::addr_of!((*inner).vtable)).drop;
|
||||
drop(inner);
|
||||
}
|
||||
self.inner.set(core::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
|
||||
/// Run all the change handler that were queued.
|
||||
pub fn run_change_handlers() {
|
||||
CHANGED_NODES.with(|list| {
|
||||
let old_list = DependencyListHead::default();
|
||||
let old_list = core::pin::pin!(old_list);
|
||||
let mut count = 0;
|
||||
while !list.is_empty() {
|
||||
count += 1;
|
||||
if count > 9 {
|
||||
crate::debug_log!("Slint: long changed callback chain detected");
|
||||
return;
|
||||
}
|
||||
DependencyListHead::swap(list.as_ref(), old_list.as_ref());
|
||||
while let Some(node) = old_list.take_head() {
|
||||
unsafe {
|
||||
((*addr_of!((*node).vtable)).evaluate)(
|
||||
node as *mut BindingHolder,
|
||||
core::ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) unsafe fn mark_dirty(_self: *const BindingHolder, _was_dirty: bool) {
|
||||
let _self = unsafe { _self.as_ref().unwrap() };
|
||||
let node_head = _self.dep_nodes.take();
|
||||
if let Some(node) = node_head.iter().next() {
|
||||
node.remove();
|
||||
CHANGED_NODES.with(|changed_nodes| {
|
||||
changed_nodes.append(node);
|
||||
});
|
||||
}
|
||||
let other = _self.dep_nodes.replace(node_head);
|
||||
debug_assert!(other.iter().next().is_none());
|
||||
}
|
||||
|
||||
pub(super) unsafe fn set_internal(&self, raw: *mut BindingHolder) {
|
||||
self.inner.set(raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_tracker() {
|
||||
use super::Property;
|
||||
use std::rc::Rc;
|
||||
let prop1 = Rc::pin(Property::new(42));
|
||||
let prop2 = Rc::pin(Property::<i32>::default());
|
||||
prop2.as_ref().set_binding({
|
||||
let prop1 = prop1.clone();
|
||||
move || prop1.as_ref().get() * 2
|
||||
});
|
||||
|
||||
let change1 = ChangeTracker::default();
|
||||
let change2 = ChangeTracker::default();
|
||||
|
||||
let state = Rc::new(core::cell::RefCell::new(std::string::String::new()));
|
||||
|
||||
change1.init(
|
||||
(state.clone(), prop1.clone()),
|
||||
|(_, prop1)| prop1.as_ref().get(),
|
||||
|(state, _), val| {
|
||||
*state.borrow_mut() += &std::format!(":1({val})");
|
||||
},
|
||||
);
|
||||
change2.init(
|
||||
(state.clone(), prop2.clone()),
|
||||
|(_, prop2)| prop2.as_ref().get(),
|
||||
|(state, _), val| {
|
||||
*state.borrow_mut() += &std::format!(":2({val})");
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(state.borrow().as_str(), "");
|
||||
prop1.as_ref().set(10);
|
||||
assert_eq!(state.borrow().as_str(), "");
|
||||
prop1.as_ref().set(30);
|
||||
assert_eq!(state.borrow().as_str(), "");
|
||||
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
|
||||
prop1.as_ref().set(1);
|
||||
assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(state.borrow().as_str(), ":1(30):2(60):1(1):2(2)");
|
||||
}
|
||||
|
||||
/// test for issue #8741
|
||||
#[test]
|
||||
fn delete_from_eval_fn() {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::string::String;
|
||||
|
||||
let change = Rc::<RefCell<Option<ChangeTracker>>>::new(Some(ChangeTracker::default()).into());
|
||||
let xyz = RefCell::new(String::from("*"));
|
||||
let result = Rc::new(RefCell::new(String::new()));
|
||||
let result2 = result.clone();
|
||||
// The change event are run in reverse order as they are created, so this one shouldn't be ever called as it is being detroyed from `change`
|
||||
let another = Rc::<RefCell<Option<ChangeTracker>>>::new(Some(ChangeTracker::default()).into());
|
||||
another.borrow().as_ref().unwrap().init_delayed(
|
||||
(),
|
||||
|()| unreachable!(),
|
||||
move |(), &()| unreachable!(),
|
||||
);
|
||||
change.borrow().as_ref().unwrap().init_delayed(
|
||||
change.clone(),
|
||||
|x| {
|
||||
x.borrow_mut().take().unwrap();
|
||||
String::from("hi")
|
||||
},
|
||||
move |x, val| {
|
||||
assert!(x.borrow().is_none());
|
||||
assert_eq!(val, "hi");
|
||||
xyz.borrow_mut().push_str("+");
|
||||
assert!(xyz.borrow().as_str().starts_with("*+"));
|
||||
result2.replace(xyz.borrow().clone());
|
||||
another.borrow_mut().take().unwrap();
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(result.borrow().as_str(), "");
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(result.borrow().as_str(), "*+");
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(result.borrow().as_str(), "*+");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_mutliple_dependencies() {
|
||||
use super::Property;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::string::String;
|
||||
let prop1 = Rc::pin(Property::new(1));
|
||||
let prop2 = Rc::pin(Property::new(2));
|
||||
let prop3 = Rc::pin(Property::new(3));
|
||||
let prop4 = Rc::pin(Property::new(4));
|
||||
let prop_with_deps = Rc::pin(Property::new(5));
|
||||
let result = Rc::new(RefCell::new(String::new()));
|
||||
|
||||
let change_tracker = ChangeTracker::default();
|
||||
change_tracker.init(
|
||||
result.clone(),
|
||||
{
|
||||
let prop1 = prop1.clone();
|
||||
let prop2 = prop2.clone();
|
||||
let prop3 = prop3.clone();
|
||||
let prop4 = prop4.clone();
|
||||
let prop_with_deps = prop_with_deps.clone();
|
||||
move |_| {
|
||||
prop1.as_ref().get()
|
||||
+ prop2.as_ref().get()
|
||||
+ prop3.as_ref().get()
|
||||
+ prop4.as_ref().get()
|
||||
+ prop_with_deps.as_ref().get()
|
||||
}
|
||||
},
|
||||
move |result, val| {
|
||||
*result.borrow_mut() += &std::format!("[{val}]");
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(result.borrow().as_str(), "");
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(result.borrow().as_str(), "");
|
||||
|
||||
prop_with_deps.as_ref().set_binding({
|
||||
let prop1 = prop1.clone();
|
||||
let prop2 = prop2.clone();
|
||||
move || prop1.as_ref().get() + prop2.as_ref().get()
|
||||
});
|
||||
|
||||
assert_eq!(result.borrow().as_str(), "");
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(prop_with_deps.as_ref().get(), 3);
|
||||
assert_eq!(result.borrow().as_str(), "[13]"); // 1 + 2 + 3 + 4 + 3
|
||||
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(result.borrow().as_str(), "[13]");
|
||||
|
||||
prop1.as_ref().set(10);
|
||||
assert_eq!(result.borrow().as_str(), "[13]");
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(result.borrow().as_str(), "[13][31]"); // 10 + 2 + 3 + 4 + 12
|
||||
|
||||
prop2.as_ref().set(20);
|
||||
prop3.as_ref().set(30);
|
||||
assert_eq!(result.borrow().as_str(), "[13][31]");
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(result.borrow().as_str(), "[13][31][94]"); // 10 + 20 + 30 + 4 + 30
|
||||
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(result.borrow().as_str(), "[13][31][94]");
|
||||
|
||||
// just swap prop1 and prop2, doesn't change the outcome
|
||||
prop1.as_ref().set(20);
|
||||
prop2.as_ref().set(10);
|
||||
ChangeTracker::run_change_handlers();
|
||||
assert_eq!(result.borrow().as_str(), "[13][31][94]");
|
||||
}
|
||||
+568
@@ -0,0 +1,568 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use super::*;
|
||||
use crate::graphics::{Brush, Color};
|
||||
use crate::items::PropertyAnimation;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
type c_void = ();
|
||||
#[repr(C)]
|
||||
/// Has the same layout as PropertyHandle
|
||||
pub struct PropertyHandleOpaque(PropertyHandle);
|
||||
|
||||
/// Initialize the first pointer of the Property. Does not initialize the content.
|
||||
/// `out` is assumed to be uninitialized
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_init(out: *mut PropertyHandleOpaque) {
|
||||
unsafe { core::ptr::write(out, PropertyHandleOpaque(PropertyHandle::default())) };
|
||||
}
|
||||
|
||||
/// To be called before accessing the value
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_update(handle: &PropertyHandleOpaque, val: *mut c_void) {
|
||||
unsafe {
|
||||
let handle = Pin::new_unchecked(&handle.0);
|
||||
handle.update(val);
|
||||
handle.register_as_dependency_to_current_binding();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark the fact that the property was changed and that its binding need to be removed, and
|
||||
/// the dependencies marked dirty.
|
||||
/// To be called after the `value` has been changed
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_changed(
|
||||
handle: &PropertyHandleOpaque,
|
||||
value: *const c_void,
|
||||
) {
|
||||
unsafe {
|
||||
if !handle.0.access(|b| {
|
||||
b.is_some_and(|b| (b.vtable.intercept_set)(&*b as *const BindingHolder, value))
|
||||
}) {
|
||||
handle.0.remove_binding();
|
||||
}
|
||||
handle.0.mark_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
fn make_c_function_binding(
|
||||
binding: extern "C" fn(*mut c_void, *mut c_void),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
intercept_set: Option<
|
||||
extern "C" fn(user_data: *mut c_void, pointer_to_value: *const c_void) -> bool,
|
||||
>,
|
||||
intercept_set_binding: Option<
|
||||
extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool,
|
||||
>,
|
||||
) -> impl BindingCallable<c_void> {
|
||||
struct CFunctionBinding<T> {
|
||||
binding_function: extern "C" fn(*mut c_void, *mut T),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
intercept_set:
|
||||
Option<extern "C" fn(user_data: *mut c_void, pointer_to_value: *const T) -> bool>,
|
||||
intercept_set_binding:
|
||||
Option<extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool>,
|
||||
}
|
||||
|
||||
impl<T> Drop for CFunctionBinding<T> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(x) = self.drop_user_data {
|
||||
x(self.user_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> BindingCallable<T> for CFunctionBinding<T> {
|
||||
fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
|
||||
(self.binding_function)(self.user_data, value as *mut T);
|
||||
BindingResult::KeepBinding
|
||||
}
|
||||
fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
|
||||
match self.intercept_set {
|
||||
None => false,
|
||||
Some(intercept_set) => intercept_set(self.user_data, value as *const T),
|
||||
}
|
||||
}
|
||||
unsafe fn intercept_set_binding(self: Pin<&Self>, new_binding: *mut BindingHolder) -> bool {
|
||||
match self.intercept_set_binding {
|
||||
None => false,
|
||||
Some(intercept_set_b) => intercept_set_b(self.user_data, new_binding.cast()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CFunctionBinding {
|
||||
binding_function: binding,
|
||||
user_data,
|
||||
drop_user_data,
|
||||
intercept_set,
|
||||
intercept_set_binding,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a binding
|
||||
///
|
||||
/// The current implementation will do usually two memory allocation:
|
||||
/// 1. the allocation from the calling code to allocate user_data
|
||||
/// 2. the box allocation within this binding
|
||||
///
|
||||
/// It might be possible to reduce that by passing something with a
|
||||
/// vtable, so there is the need for less memory allocation.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_binding(
|
||||
handle: &PropertyHandleOpaque,
|
||||
binding: extern "C" fn(user_data: *mut c_void, pointer_to_value: *mut c_void),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
intercept_set: Option<
|
||||
extern "C" fn(user_data: *mut c_void, pointer_to_value: *const c_void) -> bool,
|
||||
>,
|
||||
intercept_set_binding: Option<
|
||||
extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool,
|
||||
>,
|
||||
) {
|
||||
let binding = make_c_function_binding(
|
||||
binding,
|
||||
user_data,
|
||||
drop_user_data,
|
||||
intercept_set,
|
||||
intercept_set_binding,
|
||||
);
|
||||
unsafe { handle.0.set_binding(binding) };
|
||||
}
|
||||
|
||||
/// Set a binding using an already allocated building holder
|
||||
///
|
||||
/// (take ownership of the binding)
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_binding_internal(
|
||||
handle: &PropertyHandleOpaque,
|
||||
binding: *mut c_void,
|
||||
) {
|
||||
handle.0.set_binding_impl(binding.cast());
|
||||
}
|
||||
|
||||
/// Delete a binding. The pointer must be a pointer to a binding (so a BindingHolder)
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_delete_binding(binding: *mut c_void) {
|
||||
let b = binding as *mut BindingHolder;
|
||||
unsafe { ((*b).vtable.drop)(b) };
|
||||
}
|
||||
|
||||
/// Evaluate a raw binding
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_evaluate_binding(binding: *mut c_void, value: *mut c_void) {
|
||||
let b = binding as *mut BindingHolder;
|
||||
unsafe { ((*b).vtable.evaluate)(b, value) };
|
||||
}
|
||||
|
||||
/// Returns whether the property behind this handle is marked as dirty
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_property_is_dirty(handle: &PropertyHandleOpaque) -> bool {
|
||||
handle.0.access(|binding| binding.is_some_and(|b| b.dirty.get()))
|
||||
}
|
||||
|
||||
/// Marks the property as dirty and notifies dependencies.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_property_mark_dirty(handle: &PropertyHandleOpaque) {
|
||||
handle.0.mark_dirty()
|
||||
}
|
||||
|
||||
/// Marks the property as dirty and notifies dependencies.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_property_set_constant(handle: &PropertyHandleOpaque) {
|
||||
handle.0.set_constant()
|
||||
}
|
||||
|
||||
/// Destroy handle
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_drop(handle: *mut PropertyHandleOpaque) {
|
||||
unsafe {
|
||||
core::ptr::drop_in_place(handle);
|
||||
}
|
||||
}
|
||||
|
||||
fn c_set_animated_value<T: InterpolatedPropertyValue + Clone>(
|
||||
handle: &PropertyHandleOpaque,
|
||||
from: T,
|
||||
to: T,
|
||||
animation_data: &PropertyAnimation,
|
||||
) {
|
||||
let d = RefCell::new(properties_animations::PropertyValueAnimationData::new(
|
||||
from,
|
||||
to,
|
||||
animation_data.clone(),
|
||||
));
|
||||
// Safety: The BindingCallable is for type T
|
||||
unsafe {
|
||||
handle.0.set_binding(move |val: &mut T| {
|
||||
let (value, finished) = d.borrow_mut().compute_interpolated_value();
|
||||
*val = value;
|
||||
if finished {
|
||||
BindingResult::RemoveBinding
|
||||
} else {
|
||||
crate::animations::CURRENT_ANIMATION_DRIVER
|
||||
.with(|driver| driver.set_has_active_animations());
|
||||
BindingResult::KeepBinding
|
||||
}
|
||||
})
|
||||
};
|
||||
handle.0.mark_dirty();
|
||||
}
|
||||
|
||||
/// Internal function to set up a property animation to the specified target value for an integer property.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_animated_value_int(
|
||||
handle: &PropertyHandleOpaque,
|
||||
from: i32,
|
||||
to: i32,
|
||||
animation_data: &PropertyAnimation,
|
||||
) {
|
||||
c_set_animated_value(handle, from, to, animation_data)
|
||||
}
|
||||
|
||||
/// Internal function to set up a property animation to the specified target value for a float property.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_animated_value_float(
|
||||
handle: &PropertyHandleOpaque,
|
||||
from: f32,
|
||||
to: f32,
|
||||
animation_data: &PropertyAnimation,
|
||||
) {
|
||||
c_set_animated_value(handle, from, to, animation_data)
|
||||
}
|
||||
|
||||
/// Internal function to set up a property animation to the specified target value for a color property.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_animated_value_color(
|
||||
handle: &PropertyHandleOpaque,
|
||||
from: Color,
|
||||
to: Color,
|
||||
animation_data: &PropertyAnimation,
|
||||
) {
|
||||
c_set_animated_value(handle, from, to, animation_data);
|
||||
}
|
||||
|
||||
/// Internal function to set up a property animation to the specified target value for a brush property.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_animated_value_brush(
|
||||
handle: &PropertyHandleOpaque,
|
||||
from: &Brush,
|
||||
to: &Brush,
|
||||
animation_data: &PropertyAnimation,
|
||||
) {
|
||||
c_set_animated_value(handle, from.clone(), to.clone(), animation_data);
|
||||
}
|
||||
|
||||
unsafe fn c_set_animated_binding<T: InterpolatedPropertyValue + Clone>(
|
||||
handle: &PropertyHandleOpaque,
|
||||
binding: extern "C" fn(*mut c_void, *mut T),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
transition_data: extern "C" fn(
|
||||
user_data: *mut c_void,
|
||||
start_instant: &mut *mut u64,
|
||||
) -> PropertyAnimation,
|
||||
) {
|
||||
unsafe {
|
||||
let binding = core::mem::transmute::<
|
||||
extern "C" fn(*mut c_void, *mut T),
|
||||
extern "C" fn(*mut c_void, *mut ()),
|
||||
>(binding);
|
||||
let original_binding = PropertyHandle {
|
||||
handle: Cell::new(
|
||||
(alloc_binding_holder(make_c_function_binding(
|
||||
binding,
|
||||
user_data,
|
||||
drop_user_data,
|
||||
None,
|
||||
None,
|
||||
)) as usize)
|
||||
| 0b10,
|
||||
),
|
||||
};
|
||||
let animation_data = RefCell::new(properties_animations::PropertyValueAnimationData::new(
|
||||
T::default(),
|
||||
T::default(),
|
||||
PropertyAnimation::default(),
|
||||
));
|
||||
|
||||
handle.0.set_binding(properties_animations::AnimatedBindingCallable::<T, _> {
|
||||
original_binding,
|
||||
state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating),
|
||||
animation_data,
|
||||
compute_animation_details: move || -> properties_animations::AnimationDetail {
|
||||
// The transition_data function receives a *mut *mut u64 pointer for the
|
||||
// timestamp.
|
||||
// If the function sets the pointer to nullptr, it doesn't provide a start_time.
|
||||
// Otherwise, we assume it has written a value to the start_instant.
|
||||
// This basically models a `&mut Option<u64>`, which is then converted to an
|
||||
// `Option<Instant>`
|
||||
let mut start_instant = 0u64;
|
||||
let mut start_instant_ref = &mut start_instant as *mut u64;
|
||||
let anim = transition_data(user_data, &mut start_instant_ref);
|
||||
let start_instant = if start_instant_ref.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(crate::animations::Instant(start_instant))
|
||||
};
|
||||
(anim, start_instant)
|
||||
},
|
||||
});
|
||||
handle.0.mark_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal function to set up a property animation between values produced by the specified binding for an integer property.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_animated_binding_int(
|
||||
handle: &PropertyHandleOpaque,
|
||||
binding: extern "C" fn(*mut c_void, *mut core::ffi::c_int),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
transition_data: extern "C" fn(
|
||||
user_data: *mut c_void,
|
||||
start_instant: &mut *mut u64,
|
||||
) -> PropertyAnimation,
|
||||
) {
|
||||
unsafe {
|
||||
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal function to set up a property animation between values produced by the specified binding for a float property.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_animated_binding_float(
|
||||
handle: &PropertyHandleOpaque,
|
||||
binding: extern "C" fn(*mut c_void, *mut f32),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
transition_data: extern "C" fn(
|
||||
user_data: *mut c_void,
|
||||
start_instant: &mut *mut u64,
|
||||
) -> PropertyAnimation,
|
||||
) {
|
||||
unsafe {
|
||||
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal function to set up a property animation between values produced by the specified binding for a color property.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_animated_binding_color(
|
||||
handle: &PropertyHandleOpaque,
|
||||
binding: extern "C" fn(*mut c_void, *mut Color),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
transition_data: extern "C" fn(
|
||||
user_data: *mut c_void,
|
||||
start_instant: &mut *mut u64,
|
||||
) -> PropertyAnimation,
|
||||
) {
|
||||
unsafe {
|
||||
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal function to set up a property animation between values produced by the specified binding for a brush property.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_animated_binding_brush(
|
||||
handle: &PropertyHandleOpaque,
|
||||
binding: extern "C" fn(*mut c_void, *mut Brush),
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
transition_data: extern "C" fn(
|
||||
user_data: *mut c_void,
|
||||
start_instant: &mut *mut u64,
|
||||
) -> PropertyAnimation,
|
||||
) {
|
||||
unsafe {
|
||||
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal function to set up a state binding on a Property<StateInfo>.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_set_state_binding(
|
||||
handle: &PropertyHandleOpaque,
|
||||
binding: extern "C" fn(*mut c_void) -> i32,
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
) {
|
||||
struct CStateBinding {
|
||||
binding: extern "C" fn(*mut c_void) -> i32,
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
||||
}
|
||||
|
||||
impl Drop for CStateBinding {
|
||||
fn drop(&mut self) {
|
||||
if let Some(x) = self.drop_user_data {
|
||||
x(self.user_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CStateBinding {
|
||||
fn call(&self) -> i32 {
|
||||
(self.binding)(self.user_data)
|
||||
}
|
||||
}
|
||||
|
||||
let c_state_binding = CStateBinding { binding, user_data, drop_user_data };
|
||||
let bind_callable =
|
||||
StateInfoBinding { dirty_time: Cell::new(None), binding: move || c_state_binding.call() };
|
||||
unsafe { handle.0.set_binding(bind_callable) }
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
/// Opaque type representing the PropertyTracker
|
||||
pub struct PropertyTrackerOpaque {
|
||||
dependencies: usize,
|
||||
dep_nodes: usize,
|
||||
vtable: usize,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
static_assertions::assert_eq_align!(PropertyTrackerOpaque, PropertyTracker);
|
||||
static_assertions::assert_eq_size!(PropertyTrackerOpaque, PropertyTracker);
|
||||
|
||||
/// Initialize the first pointer of the PropertyTracker.
|
||||
/// `out` is assumed to be uninitialized
|
||||
/// slint_property_tracker_drop need to be called after that
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_tracker_init(out: *mut PropertyTrackerOpaque) {
|
||||
unsafe {
|
||||
core::ptr::write(out as *mut PropertyTracker, PropertyTracker::default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the callback with the user data. Any properties access within the callback will be registered.
|
||||
/// Any currently evaluated bindings or property trackers will be notified if accessed properties are changed.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_tracker_evaluate(
|
||||
handle: *const PropertyTrackerOpaque,
|
||||
callback: extern "C" fn(user_data: *mut c_void),
|
||||
user_data: *mut c_void,
|
||||
) {
|
||||
unsafe { Pin::new_unchecked(&*(handle as *const PropertyTracker)) }
|
||||
.evaluate(|| callback(user_data))
|
||||
}
|
||||
|
||||
/// Call the callback with the user data. Any properties access within the callback will be registered.
|
||||
/// Any currently evaluated bindings or property trackers will be not notified if accessed properties are changed.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_tracker_evaluate_as_dependency_root(
|
||||
handle: *const PropertyTrackerOpaque,
|
||||
callback: extern "C" fn(user_data: *mut c_void),
|
||||
user_data: *mut c_void,
|
||||
) {
|
||||
unsafe { Pin::new_unchecked(&*(handle as *const PropertyTracker)) }
|
||||
.evaluate_as_dependency_root(|| callback(user_data))
|
||||
}
|
||||
/// Query if the property tracker is dirty
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_tracker_is_dirty(
|
||||
handle: *const PropertyTrackerOpaque,
|
||||
) -> bool {
|
||||
unsafe { (*(handle as *const PropertyTracker)).is_dirty() }
|
||||
}
|
||||
|
||||
/// Destroy handle
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_property_tracker_drop(handle: *mut PropertyTrackerOpaque) {
|
||||
unsafe { core::ptr::drop_in_place(handle as *mut PropertyTracker) };
|
||||
}
|
||||
|
||||
/// Construct a ChangeTracker
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_change_tracker_construct(ct: *mut ChangeTracker) {
|
||||
unsafe { core::ptr::write(ct, ChangeTracker::default()) };
|
||||
}
|
||||
|
||||
/// Drop a ChangeTracker
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_change_tracker_drop(ct: *mut ChangeTracker) {
|
||||
unsafe { core::ptr::drop_in_place(ct) };
|
||||
}
|
||||
|
||||
/// initialize the change tracker
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_change_tracker_init(
|
||||
ct: &ChangeTracker,
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: extern "C" fn(user_data: *mut c_void),
|
||||
eval_fn: extern "C" fn(user_data: *mut c_void) -> bool,
|
||||
notify_fn: extern "C" fn(user_data: *mut c_void),
|
||||
) {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct C_ChangeTrackerInner {
|
||||
user_data: *mut c_void,
|
||||
drop_user_data: extern "C" fn(user_data: *mut c_void),
|
||||
eval_fn: extern "C" fn(user_data: *mut c_void) -> bool,
|
||||
notify_fn: extern "C" fn(user_data: *mut c_void),
|
||||
}
|
||||
impl Drop for C_ChangeTrackerInner {
|
||||
fn drop(&mut self) {
|
||||
(self.drop_user_data)(self.user_data);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn drop(_self: *mut BindingHolder) {
|
||||
core::mem::drop(unsafe {
|
||||
Box::from_raw(_self as *mut BindingHolder<C_ChangeTrackerInner>)
|
||||
});
|
||||
}
|
||||
|
||||
unsafe fn evaluate(_self: *const BindingHolder, _value: *mut ()) -> BindingResult {
|
||||
let pinned_holder = unsafe { Pin::new_unchecked(&*_self) };
|
||||
let _self = _self as *mut BindingHolder<C_ChangeTrackerInner>;
|
||||
let inner = unsafe { core::ptr::addr_of_mut!((*_self).binding).as_mut().unwrap() };
|
||||
let notify =
|
||||
super::CURRENT_BINDING.set(Some(pinned_holder), || (inner.eval_fn)(inner.user_data));
|
||||
if notify {
|
||||
(inner.notify_fn)(inner.user_data);
|
||||
}
|
||||
BindingResult::KeepBinding
|
||||
}
|
||||
|
||||
const VT: &BindingVTable = &BindingVTable {
|
||||
drop,
|
||||
evaluate,
|
||||
mark_dirty: ChangeTracker::mark_dirty,
|
||||
intercept_set: |_, _| false,
|
||||
intercept_set_binding: |_, _| false,
|
||||
};
|
||||
|
||||
ct.clear();
|
||||
|
||||
let inner = C_ChangeTrackerInner { user_data, drop_user_data, eval_fn, notify_fn };
|
||||
|
||||
let holder = BindingHolder {
|
||||
dependencies: Cell::new(0),
|
||||
dep_nodes: Default::default(),
|
||||
vtable: VT,
|
||||
dirty: Cell::new(false),
|
||||
is_two_way_binding: false,
|
||||
pinned: PhantomPinned,
|
||||
binding: inner,
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name: "<ChangeTracker>".into(),
|
||||
};
|
||||
|
||||
let raw = Box::into_raw(Box::new(holder));
|
||||
unsafe { ct.set_internal(raw as *mut BindingHolder) };
|
||||
|
||||
let pinned_holder = unsafe { Pin::new_unchecked(&*(raw as *mut BindingHolder)) };
|
||||
let inner = unsafe { core::ptr::addr_of_mut!((*raw).binding).as_mut().unwrap() };
|
||||
super::CURRENT_BINDING.set(Some(pinned_holder), || (inner.eval_fn)(inner.user_data));
|
||||
}
|
||||
|
||||
/// return the current animation tick for the `animation-tick` function
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_animation_tick() -> u64 {
|
||||
crate::animations::animation_tick()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,614 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use super::*;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::Cell;
|
||||
use core::cell::UnsafeCell;
|
||||
use core::marker::PhantomData;
|
||||
use core::marker::PhantomPinned;
|
||||
use core::pin::Pin;
|
||||
|
||||
struct TwoWayBinding<T> {
|
||||
common_property: Pin<Rc<Property<T>>>,
|
||||
}
|
||||
unsafe impl<T: PartialEq + Clone + 'static> BindingCallable<T> for TwoWayBinding<T> {
|
||||
fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
|
||||
*value = self.common_property.as_ref().get();
|
||||
BindingResult::KeepBinding
|
||||
}
|
||||
|
||||
fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
|
||||
self.common_property.as_ref().set(value.clone());
|
||||
true
|
||||
}
|
||||
|
||||
unsafe fn intercept_set_binding(self: Pin<&Self>, new_binding: *mut BindingHolder) -> bool {
|
||||
self.common_property.handle.set_binding_impl(new_binding);
|
||||
true
|
||||
}
|
||||
|
||||
const IS_TWO_WAY_BINDING: bool = true;
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Clone + 'static> Property<T> {
|
||||
/// If the property is a two way binding, return the common property
|
||||
pub(crate) fn check_common_property(self: Pin<&Self>) -> Option<Pin<Rc<Property<T>>>> {
|
||||
let handle_val = self.handle.handle.get();
|
||||
if let Some(holder) = PropertyHandle::pointer_to_binding(handle_val) {
|
||||
// Safety: the handle is a pointer to a binding
|
||||
if unsafe { (*holder).is_two_way_binding } {
|
||||
// Safety: the handle is a pointer to a binding whose B is a TwoWayBinding<T>
|
||||
return Some(unsafe {
|
||||
(*(holder as *const BindingHolder<TwoWayBinding<T>>))
|
||||
.binding
|
||||
.common_property
|
||||
.clone()
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Link two property such that any change to one property is affecting the other property as if they
|
||||
/// where, in fact, a single property.
|
||||
/// The value or binding of prop2 is kept.
|
||||
pub fn link_two_way(prop1: Pin<&Self>, prop2: Pin<&Self>) {
|
||||
#[cfg(slint_debug_property)]
|
||||
let debug_name =
|
||||
alloc::format!("<{}<=>{}>", prop1.debug_name.borrow(), prop2.debug_name.borrow());
|
||||
|
||||
let value = prop2.get_untracked();
|
||||
|
||||
if let Some(common_property) = prop1.check_common_property() {
|
||||
// Safety: TwoWayBinding is a BindingCallable for type T
|
||||
unsafe {
|
||||
prop2.handle.set_binding(
|
||||
TwoWayBinding::<T> { common_property },
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name.as_str(),
|
||||
);
|
||||
}
|
||||
prop2.set(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(common_property) = prop2.check_common_property() {
|
||||
// Safety: TwoWayBinding is a BindingCallable for type T
|
||||
unsafe {
|
||||
prop1.handle.set_binding(
|
||||
TwoWayBinding::<T> { common_property },
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name.as_str(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let prop2_handle_val = prop2.handle.handle.get();
|
||||
let handle = if PropertyHandle::is_pointer_to_binding(prop2_handle_val) {
|
||||
// If prop2 is a binding, just "steal it"
|
||||
prop2.handle.handle.set(0);
|
||||
PropertyHandle { handle: Cell::new(prop2_handle_val) }
|
||||
} else {
|
||||
PropertyHandle::default()
|
||||
};
|
||||
|
||||
let common_property = Rc::pin(Property {
|
||||
handle,
|
||||
value: UnsafeCell::new(value),
|
||||
pinned: PhantomPinned,
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name: debug_name.clone().into(),
|
||||
});
|
||||
// Safety: TwoWayBinding's T is the same as the type for both properties
|
||||
unsafe {
|
||||
prop1.handle.set_binding(
|
||||
TwoWayBinding { common_property: common_property.clone() },
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name.as_str(),
|
||||
);
|
||||
prop2.handle.set_binding(
|
||||
TwoWayBinding { common_property },
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name.as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a property to another property of a different type, with mapping function to go between them.
|
||||
///
|
||||
/// the value of the `prop1` (of type `T`) is kept. (This is the opposite of [`Self::link_two_way`])
|
||||
/// `T2` must be able to be derived from `T` using the `map_to` function.
|
||||
/// `T` may contain more information than `T2` and the value of prop1 will be updated with the `map_from` function when `prop2` changes
|
||||
pub fn link_two_way_with_map<T2: PartialEq + Clone + 'static>(
|
||||
prop1: Pin<&Self>,
|
||||
prop2: Pin<&Property<T2>>,
|
||||
map_to: impl Fn(&T) -> T2 + Clone + 'static, // Rename map_to_t2
|
||||
map_from: impl Fn(&mut T, &T2) + Clone + 'static,
|
||||
) {
|
||||
let common_property = if let Some(common_property) = prop1.check_common_property() {
|
||||
common_property
|
||||
} else {
|
||||
let prop1_handle_val = prop1.handle.handle.get();
|
||||
let handle = if PropertyHandle::is_pointer_to_binding(prop1_handle_val) {
|
||||
// If prop1 is a binding, just "steal it"
|
||||
prop1.handle.handle.set(0);
|
||||
PropertyHandle { handle: Cell::new(prop1_handle_val) }
|
||||
} else {
|
||||
PropertyHandle::default()
|
||||
};
|
||||
|
||||
#[cfg(slint_debug_property)]
|
||||
let debug_name = alloc::format!("{}*", prop1.debug_name.borrow());
|
||||
|
||||
let common_property = Rc::pin(Property {
|
||||
handle,
|
||||
value: UnsafeCell::new(prop1.get_internal()),
|
||||
pinned: PhantomPinned,
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name: debug_name.clone().into(),
|
||||
});
|
||||
// Safety: TwoWayBinding's T is the same as the type for both properties
|
||||
unsafe {
|
||||
prop1.handle.set_binding(
|
||||
TwoWayBinding::<T> { common_property: common_property.clone() },
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name.as_str(),
|
||||
);
|
||||
}
|
||||
common_property
|
||||
};
|
||||
Self::link_two_way_with_map_to_common_property(common_property, prop2, map_to, map_from);
|
||||
}
|
||||
|
||||
/// Make a two way binding between the common property and the binding prop2.
|
||||
/// if prop2 has a binding, it will be preserved
|
||||
pub(crate) fn link_two_way_with_map_to_common_property<T2: PartialEq + Clone + 'static>(
|
||||
common_property: Pin<Rc<Self>>,
|
||||
prop2: Pin<&Property<T2>>,
|
||||
map_to: impl Fn(&T) -> T2 + Clone + 'static,
|
||||
map_from: impl Fn(&mut T, &T2) + Clone + 'static,
|
||||
) {
|
||||
struct TwoWayBindingWithMap<T, T2, M1, M2> {
|
||||
common_property: Pin<Rc<Property<T>>>,
|
||||
map_to: M1,
|
||||
map_from: M2,
|
||||
marker: PhantomData<(T, T2)>,
|
||||
}
|
||||
unsafe impl<
|
||||
T: PartialEq + Clone + 'static,
|
||||
T2: PartialEq + Clone + 'static,
|
||||
M1: Fn(&T) -> T2 + Clone + 'static,
|
||||
M2: Fn(&mut T, &T2) + Clone + 'static,
|
||||
> BindingCallable<T2> for TwoWayBindingWithMap<T, T2, M1, M2>
|
||||
{
|
||||
fn evaluate(self: Pin<&Self>, value: &mut T2) -> BindingResult {
|
||||
*value = (self.map_to)(&self.common_property.as_ref().get());
|
||||
BindingResult::KeepBinding
|
||||
}
|
||||
|
||||
fn intercept_set(self: Pin<&Self>, value: &T2) -> bool {
|
||||
let mut old = self.common_property.as_ref().get();
|
||||
(self.map_from)(&mut old, value);
|
||||
self.common_property.as_ref().set(old);
|
||||
true
|
||||
}
|
||||
|
||||
unsafe fn intercept_set_binding(
|
||||
self: Pin<&Self>,
|
||||
new_binding: *mut BindingHolder,
|
||||
) -> bool {
|
||||
let new_new_binding = alloc_binding_holder(BindingMapper::<T, T2, M1, M2> {
|
||||
b: new_binding,
|
||||
map_to: self.map_to.clone(),
|
||||
map_from: self.map_from.clone(),
|
||||
marker: PhantomData,
|
||||
});
|
||||
self.common_property.handle.set_binding_impl(new_new_binding);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a binding for T2, maps to a binding for T
|
||||
struct BindingMapper<T, T2, M1, M2> {
|
||||
/// Binding that returns a `T2`
|
||||
b: *mut BindingHolder,
|
||||
map_to: M1,
|
||||
map_from: M2,
|
||||
marker: PhantomData<(T, T2)>,
|
||||
}
|
||||
unsafe impl<
|
||||
T: PartialEq + Clone + 'static,
|
||||
T2: PartialEq + Clone + 'static,
|
||||
M1: Fn(&T) -> T2 + 'static,
|
||||
M2: Fn(&mut T, &T2) + 'static,
|
||||
> BindingCallable<T> for BindingMapper<T, T2, M1, M2>
|
||||
{
|
||||
fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
|
||||
let mut sub_value = (self.map_to)(value);
|
||||
// Safety: `self.b` is a BindingHolder that expects a `T2`
|
||||
unsafe {
|
||||
((*self.b).vtable.evaluate)(self.b, &mut sub_value as *mut T2 as *mut ());
|
||||
}
|
||||
(self.map_from)(value, &sub_value);
|
||||
BindingResult::KeepBinding
|
||||
}
|
||||
|
||||
fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
|
||||
let sub_value = (self.map_to)(value);
|
||||
// Safety: `self.b` is a BindingHolder that expects a `T2`
|
||||
unsafe {
|
||||
((*self.b).vtable.intercept_set)(self.b, &sub_value as *const T2 as *const ())
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T, T2, M1, M2> Drop for BindingMapper<T, T2, M1, M2> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
((*self.b).vtable.drop)(self.b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(slint_debug_property)]
|
||||
let debug_name = alloc::format!(
|
||||
"<{}<=>{}>",
|
||||
common_property.debug_name.borrow(),
|
||||
prop2.debug_name.borrow()
|
||||
);
|
||||
|
||||
// Detach the old binding (if any) from prop2, transferring its
|
||||
// dependency list back to the handle so that set_binding() will
|
||||
// properly move it into the new TwoWayBindingWithMap binding.
|
||||
// Without this, the old binding's dependency list would be orphaned
|
||||
// when it is later freed via BindingMapper::drop, leaving
|
||||
// DependencyNodes with dangling prev pointers.
|
||||
let old_binding = prop2.handle.detach_binding();
|
||||
|
||||
unsafe {
|
||||
prop2.handle.set_binding(
|
||||
TwoWayBindingWithMap { common_property, map_to, map_from, marker: PhantomData },
|
||||
#[cfg(slint_debug_property)]
|
||||
debug_name.as_str(),
|
||||
);
|
||||
|
||||
if let Some(binding) = old_binding {
|
||||
prop2.handle.set_binding_impl(binding);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_two_ways_test() {
|
||||
let p1 = Rc::pin(Property::new(42));
|
||||
let p2 = Rc::pin(Property::new(88));
|
||||
|
||||
let depends = Box::pin(Property::new(0));
|
||||
depends.as_ref().set_binding({
|
||||
let p1 = p1.clone();
|
||||
move || p1.as_ref().get() + 8
|
||||
});
|
||||
assert_eq!(depends.as_ref().get(), 42 + 8);
|
||||
Property::link_two_way(p1.as_ref(), p2.as_ref());
|
||||
assert_eq!(p1.as_ref().get(), 88);
|
||||
assert_eq!(p2.as_ref().get(), 88);
|
||||
assert_eq!(depends.as_ref().get(), 88 + 8);
|
||||
p2.as_ref().set(5);
|
||||
assert_eq!(p1.as_ref().get(), 5);
|
||||
assert_eq!(p2.as_ref().get(), 5);
|
||||
assert_eq!(depends.as_ref().get(), 5 + 8);
|
||||
p1.as_ref().set(22);
|
||||
assert_eq!(p1.as_ref().get(), 22);
|
||||
assert_eq!(p2.as_ref().get(), 22);
|
||||
assert_eq!(depends.as_ref().get(), 22 + 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_two_ways_test_binding() {
|
||||
let p1 = Rc::pin(Property::new(42));
|
||||
let p2 = Rc::pin(Property::new(88));
|
||||
let global = Rc::pin(Property::new(23));
|
||||
p2.as_ref().set_binding({
|
||||
let global = global.clone();
|
||||
move || global.as_ref().get() + 9
|
||||
});
|
||||
|
||||
let depends = Box::pin(Property::new(0));
|
||||
depends.as_ref().set_binding({
|
||||
let p1 = p1.clone();
|
||||
move || p1.as_ref().get() + 8
|
||||
});
|
||||
|
||||
Property::link_two_way(p1.as_ref(), p2.as_ref());
|
||||
assert_eq!(p1.as_ref().get(), 23 + 9);
|
||||
assert_eq!(p2.as_ref().get(), 23 + 9);
|
||||
assert_eq!(depends.as_ref().get(), 23 + 9 + 8);
|
||||
global.as_ref().set(55);
|
||||
assert_eq!(p1.as_ref().get(), 55 + 9);
|
||||
assert_eq!(p2.as_ref().get(), 55 + 9);
|
||||
assert_eq!(depends.as_ref().get(), 55 + 9 + 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_two_ways_recurse_from_binding() {
|
||||
let xx = Rc::pin(Property::new(0));
|
||||
|
||||
let p1 = Rc::pin(Property::new(42));
|
||||
let p2 = Rc::pin(Property::new(88));
|
||||
let global = Rc::pin(Property::new(23));
|
||||
|
||||
let done = Rc::new(Cell::new(false));
|
||||
xx.set_binding({
|
||||
let p1 = p1.clone();
|
||||
let p2 = p2.clone();
|
||||
let global = global.clone();
|
||||
let xx_weak = pin_weak::rc::PinWeak::downgrade(xx.clone());
|
||||
move || {
|
||||
if !done.get() {
|
||||
done.set(true);
|
||||
Property::link_two_way(p1.as_ref(), p2.as_ref());
|
||||
let xx_weak = xx_weak.clone();
|
||||
p1.as_ref().set_binding(move || xx_weak.upgrade().unwrap().as_ref().get() + 9);
|
||||
}
|
||||
global.as_ref().get() + 2
|
||||
}
|
||||
});
|
||||
assert_eq!(xx.as_ref().get(), 23 + 2);
|
||||
assert_eq!(p1.as_ref().get(), 23 + 2 + 9);
|
||||
assert_eq!(p2.as_ref().get(), 23 + 2 + 9);
|
||||
|
||||
global.as_ref().set(55);
|
||||
assert_eq!(p1.as_ref().get(), 55 + 2 + 9);
|
||||
assert_eq!(p2.as_ref().get(), 55 + 2 + 9);
|
||||
assert_eq!(xx.as_ref().get(), 55 + 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_two_ways_binding_of_two_way_binding_first() {
|
||||
let p1_1 = Rc::pin(Property::new(2));
|
||||
let p1_2 = Rc::pin(Property::new(4));
|
||||
Property::link_two_way(p1_1.as_ref(), p1_2.as_ref());
|
||||
|
||||
assert_eq!(p1_1.as_ref().get(), 4);
|
||||
assert_eq!(p1_2.as_ref().get(), 4);
|
||||
|
||||
let p2 = Rc::pin(Property::new(3));
|
||||
Property::link_two_way(p1_1.as_ref(), p2.as_ref());
|
||||
|
||||
assert_eq!(p1_1.as_ref().get(), 3);
|
||||
assert_eq!(p1_2.as_ref().get(), 3);
|
||||
assert_eq!(p2.as_ref().get(), 3);
|
||||
|
||||
p1_1.set(6);
|
||||
|
||||
assert_eq!(p1_1.as_ref().get(), 6);
|
||||
assert_eq!(p1_2.as_ref().get(), 6);
|
||||
assert_eq!(p2.as_ref().get(), 6);
|
||||
|
||||
p1_2.set(8);
|
||||
|
||||
assert_eq!(p1_1.as_ref().get(), 8);
|
||||
assert_eq!(p1_2.as_ref().get(), 8);
|
||||
assert_eq!(p2.as_ref().get(), 8);
|
||||
|
||||
p2.set(7);
|
||||
|
||||
assert_eq!(p1_1.as_ref().get(), 7);
|
||||
assert_eq!(p1_2.as_ref().get(), 7);
|
||||
assert_eq!(p2.as_ref().get(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_two_ways_binding_of_two_way_binding_second() {
|
||||
let p1 = Rc::pin(Property::new(2));
|
||||
let p2_1 = Rc::pin(Property::new(3));
|
||||
let p2_2 = Rc::pin(Property::new(5));
|
||||
Property::link_two_way(p2_1.as_ref(), p2_2.as_ref());
|
||||
|
||||
assert_eq!(p2_1.as_ref().get(), 5);
|
||||
assert_eq!(p2_2.as_ref().get(), 5);
|
||||
|
||||
Property::link_two_way(p1.as_ref(), p2_2.as_ref());
|
||||
|
||||
assert_eq!(p1.as_ref().get(), 5);
|
||||
assert_eq!(p2_1.as_ref().get(), 5);
|
||||
assert_eq!(p2_2.as_ref().get(), 5);
|
||||
|
||||
p1.set(6);
|
||||
|
||||
assert_eq!(p1.as_ref().get(), 6);
|
||||
assert_eq!(p2_1.as_ref().get(), 6);
|
||||
assert_eq!(p2_2.as_ref().get(), 6);
|
||||
|
||||
p2_1.set(7);
|
||||
|
||||
assert_eq!(p1.as_ref().get(), 7);
|
||||
assert_eq!(p2_1.as_ref().get(), 7);
|
||||
assert_eq!(p2_2.as_ref().get(), 7);
|
||||
|
||||
p2_2.set(9);
|
||||
|
||||
assert_eq!(p1.as_ref().get(), 9);
|
||||
assert_eq!(p2_1.as_ref().get(), 9);
|
||||
assert_eq!(p2_2.as_ref().get(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_two_ways_binding_of_two_two_way_bindings() {
|
||||
let p1_1 = Rc::pin(Property::new(2));
|
||||
let p1_2 = Rc::pin(Property::new(4));
|
||||
Property::link_two_way(p1_1.as_ref(), p1_2.as_ref());
|
||||
assert_eq!(p1_1.as_ref().get(), 4);
|
||||
assert_eq!(p1_2.as_ref().get(), 4);
|
||||
|
||||
let p2_1 = Rc::pin(Property::new(3));
|
||||
let p2_2 = Rc::pin(Property::new(5));
|
||||
Property::link_two_way(p2_1.as_ref(), p2_2.as_ref());
|
||||
|
||||
assert_eq!(p2_1.as_ref().get(), 5);
|
||||
assert_eq!(p2_2.as_ref().get(), 5);
|
||||
|
||||
Property::link_two_way(p1_1.as_ref(), p2_2.as_ref());
|
||||
|
||||
assert_eq!(p1_1.as_ref().get(), 5);
|
||||
assert_eq!(p1_2.as_ref().get(), 5);
|
||||
assert_eq!(p2_1.as_ref().get(), 5);
|
||||
assert_eq!(p2_2.as_ref().get(), 5);
|
||||
|
||||
p1_1.set(6);
|
||||
assert_eq!(p1_1.as_ref().get(), 6);
|
||||
assert_eq!(p1_2.as_ref().get(), 6);
|
||||
assert_eq!(p2_1.as_ref().get(), 6);
|
||||
assert_eq!(p2_2.as_ref().get(), 6);
|
||||
|
||||
p1_2.set(8);
|
||||
assert_eq!(p1_1.as_ref().get(), 8);
|
||||
assert_eq!(p1_2.as_ref().get(), 8);
|
||||
assert_eq!(p2_1.as_ref().get(), 8);
|
||||
assert_eq!(p2_2.as_ref().get(), 8);
|
||||
|
||||
p2_1.set(7);
|
||||
assert_eq!(p1_1.as_ref().get(), 7);
|
||||
assert_eq!(p1_2.as_ref().get(), 7);
|
||||
assert_eq!(p2_1.as_ref().get(), 7);
|
||||
assert_eq!(p2_2.as_ref().get(), 7);
|
||||
|
||||
p2_2.set(9);
|
||||
assert_eq!(p1_1.as_ref().get(), 9);
|
||||
assert_eq!(p1_2.as_ref().get(), 9);
|
||||
assert_eq!(p2_1.as_ref().get(), 9);
|
||||
assert_eq!(p2_2.as_ref().get(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_way_with_map() {
|
||||
#[derive(PartialEq, Clone, Default, Debug)]
|
||||
struct Struct {
|
||||
foo: i32,
|
||||
bar: alloc::string::String,
|
||||
}
|
||||
let p1 = Rc::pin(Property::new(Struct { foo: 42, bar: "hello".into() }));
|
||||
let p2 = Rc::pin(Property::new(88));
|
||||
let p3 = Rc::pin(Property::new(alloc::string::String::from("xyz")));
|
||||
Property::link_two_way_with_map(p1.as_ref(), p2.as_ref(), |s| s.foo, |s, foo| s.foo = *foo);
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 42, bar: "hello".into() });
|
||||
assert_eq!(p2.as_ref().get(), 42);
|
||||
|
||||
p2.as_ref().set(81);
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 81, bar: "hello".into() });
|
||||
assert_eq!(p2.as_ref().get(), 81);
|
||||
|
||||
p1.as_ref().set(Struct { foo: 78, bar: "world".into() });
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "world".into() });
|
||||
assert_eq!(p2.as_ref().get(), 78);
|
||||
|
||||
Property::link_two_way_with_map(
|
||||
p1.as_ref(),
|
||||
p3.as_ref(),
|
||||
|s| s.bar.clone(),
|
||||
|s, bar| s.bar = bar.clone(),
|
||||
);
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "world".into() });
|
||||
assert_eq!(p2.as_ref().get(), 78);
|
||||
assert_eq!(p3.as_ref().get(), "world");
|
||||
|
||||
p3.as_ref().set("abc".into());
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 78, bar: "abc".into() });
|
||||
assert_eq!(p2.as_ref().get(), 78);
|
||||
assert_eq!(p3.as_ref().get(), "abc");
|
||||
|
||||
let p4 = Rc::pin(Property::new(123));
|
||||
p2.set_binding({
|
||||
let p4 = p4.clone();
|
||||
move || p4.as_ref().get() + 1
|
||||
});
|
||||
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 124, bar: "abc".into() });
|
||||
assert_eq!(p2.as_ref().get(), 124);
|
||||
assert_eq!(p3.as_ref().get(), "abc");
|
||||
|
||||
p4.as_ref().set(456);
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "abc".into() });
|
||||
assert_eq!(p2.as_ref().get(), 457);
|
||||
assert_eq!(p3.as_ref().get(), "abc");
|
||||
|
||||
p3.as_ref().set("def".into());
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "def".into() });
|
||||
assert_eq!(p2.as_ref().get(), 457);
|
||||
assert_eq!(p3.as_ref().get(), "def");
|
||||
|
||||
p4.as_ref().set(789);
|
||||
// Note that the binding with `p2 : p4+1` is broken
|
||||
assert_eq!(p1.as_ref().get(), Struct { foo: 457, bar: "def".into() });
|
||||
assert_eq!(p2.as_ref().get(), 457);
|
||||
assert_eq!(p3.as_ref().get(), "def");
|
||||
}
|
||||
|
||||
/// Regression test for use-after-free in `link_two_way_with_map_to_common_property`.
|
||||
///
|
||||
/// When a property already has a binding with dependants and is then linked
|
||||
/// via `link_two_way_with_map`, the old binding's dependency list must be
|
||||
/// transferred to the new `TwoWayBindingWithMap` binding. Without this
|
||||
/// transfer, the old binding is later freed (via `BindingMapper::drop`) while
|
||||
/// dependency nodes still point into its `dependencies` field, causing
|
||||
/// panics in `DependencyNode::debug_assert_valid` on drop.
|
||||
///
|
||||
/// The drop order is arranged so that the tracker (which owns the
|
||||
/// dependency node) outlives the properties, forcing the node to be
|
||||
/// removed after the old binding would have been freed.
|
||||
#[test]
|
||||
fn test_two_way_with_map_dependency_list_transfer() {
|
||||
#[derive(PartialEq, Clone, Default, Debug)]
|
||||
struct Wrapper {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
let source = Rc::pin(Property::new(10));
|
||||
|
||||
// Declare the tracker before the properties so it is dropped *after*
|
||||
// them (Rust drops locals in reverse declaration order). This ensures
|
||||
// the dependency node outlives the old binding.
|
||||
let tracker = Box::pin(<PropertyTracker>::default());
|
||||
|
||||
let p_field = Rc::pin(Property::new(0i32));
|
||||
p_field.as_ref().set_binding({
|
||||
let source = source.clone();
|
||||
move || source.as_ref().get() * 2
|
||||
});
|
||||
assert_eq!(p_field.as_ref().get(), 20);
|
||||
|
||||
// Evaluate the tracker, which reads p_field and registers a dependency
|
||||
// node on p_field's binding's dependency list.
|
||||
let val = tracker.as_ref().evaluate({
|
||||
let p_field = p_field.clone();
|
||||
move || p_field.as_ref().get()
|
||||
});
|
||||
assert_eq!(val, 20);
|
||||
assert!(!tracker.as_ref().is_dirty());
|
||||
|
||||
// link_two_way_with_map replaces p_field's binding with a
|
||||
// TwoWayBindingWithMap and wraps the old binding in a BindingMapper
|
||||
// on the common property. The dependency list must be transferred.
|
||||
let p_struct = Rc::pin(Property::new(Wrapper { value: 0 }));
|
||||
Property::link_two_way_with_map(
|
||||
p_struct.as_ref(),
|
||||
p_field.as_ref(),
|
||||
|s| s.value,
|
||||
|s, v| s.value = *v,
|
||||
);
|
||||
|
||||
assert_eq!(p_field.as_ref().get(), 20);
|
||||
assert_eq!(p_struct.as_ref().get(), Wrapper { value: 20 });
|
||||
|
||||
// The tracker's dependency should still fire when the source changes.
|
||||
source.as_ref().set(5);
|
||||
assert!(tracker.as_ref().is_dirty());
|
||||
|
||||
// Implicit drop order: p_struct, p_field, tracker, source.
|
||||
// p_field's drop frees the common property chain (including the old
|
||||
// binding via BindingMapper::drop). tracker is dropped afterwards —
|
||||
// its DependencyNode::remove would panic in debug_assert_valid if
|
||||
// the dependency list was not properly transferred.
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use core::pin::Pin;
|
||||
|
||||
use crate::api::PlatformError;
|
||||
use crate::graphics::{Rgba8Pixel, SharedPixelBuffer};
|
||||
use crate::item_tree::ItemTreeRef;
|
||||
use crate::items::{ItemRc, TextWrap};
|
||||
use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor};
|
||||
use crate::window::WindowAdapter;
|
||||
|
||||
/// This trait represents a Renderer that can render a slint scene.
|
||||
///
|
||||
/// This trait is [sealed](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed),
|
||||
/// meaning that you are not expected to implement this trait
|
||||
/// yourself, but you should use the provided ones from Slint.
|
||||
pub trait Renderer: RendererSealed {}
|
||||
impl<T: RendererSealed> Renderer for T {}
|
||||
|
||||
/// Implementation details behind [`Renderer`], but since this
|
||||
/// trait is not exported in the public API, it is not possible for the
|
||||
/// users to re-implement these functions.
|
||||
pub trait RendererSealed {
|
||||
/// Returns the size of the given text in logical pixels.
|
||||
/// When set, `max_width` means that one need to wrap the text, so it does not go further than that,
|
||||
/// using the wrapping type passed by `text_wrap`.
|
||||
fn text_size(
|
||||
&self,
|
||||
text_item: Pin<&dyn crate::item_rendering::RenderString>,
|
||||
item_rc: &crate::item_tree::ItemRc,
|
||||
max_width: Option<LogicalLength>,
|
||||
text_wrap: TextWrap,
|
||||
) -> LogicalSize;
|
||||
|
||||
/// Returns the size of the individual character in logical pixels.
|
||||
fn char_size(
|
||||
&self,
|
||||
text_item: Pin<&dyn crate::item_rendering::HasFont>,
|
||||
item_rc: &crate::item_tree::ItemRc,
|
||||
ch: char,
|
||||
) -> LogicalSize;
|
||||
|
||||
/// Returns the metrics of the given font.
|
||||
fn font_metrics(&self, font_request: crate::graphics::FontRequest)
|
||||
-> crate::items::FontMetrics;
|
||||
|
||||
/// Returns the (UTF-8) byte offset in the text property that refers to the character that contributed to
|
||||
/// the glyph cluster that's visually nearest to the given coordinate. This is used for hit-testing,
|
||||
/// for example when receiving a mouse click into a text field. Then this function returns the "cursor"
|
||||
/// position.
|
||||
fn text_input_byte_offset_for_position(
|
||||
&self,
|
||||
text_input: Pin<&crate::items::TextInput>,
|
||||
item_rc: &ItemRc,
|
||||
pos: LogicalPoint,
|
||||
) -> usize;
|
||||
|
||||
/// That's the opposite of [`Self::text_input_byte_offset_for_position`]
|
||||
/// It takes a (UTF-8) byte offset in the text property, and returns a Rectangle
|
||||
/// left to the char. It is one logical pixel wide and ends at the baseline.
|
||||
fn text_input_cursor_rect_for_byte_offset(
|
||||
&self,
|
||||
text_input: Pin<&crate::items::TextInput>,
|
||||
item_rc: &ItemRc,
|
||||
byte_offset: usize,
|
||||
) -> LogicalRect;
|
||||
|
||||
/// Clear the caches for the items that are being removed
|
||||
fn free_graphics_resources(
|
||||
&self,
|
||||
_component: ItemTreeRef,
|
||||
_items: &mut dyn Iterator<Item = Pin<crate::items::ItemRef<'_>>>,
|
||||
) -> Result<(), crate::platform::PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a given region as dirty regardless whether the items actually are dirty.
|
||||
///
|
||||
/// Example: when a PopupWindow disappears, the region under the popup needs to be redrawn
|
||||
fn mark_dirty_region(&self, _region: crate::partial_renderer::DirtyRegion) {}
|
||||
|
||||
#[cfg(feature = "std")] // FIXME: just because of the Error
|
||||
/// This function can be used to register a custom TrueType font with Slint,
|
||||
/// for use with the `font-family` property. The provided slice must be a valid TrueType
|
||||
/// font.
|
||||
fn register_font_from_memory(
|
||||
&self,
|
||||
_data: &'static [u8],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This renderer does not support registering custom fonts.".into())
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// This function can be used to register a custom TrueType font with Slint,
|
||||
/// for use with the `font-family` property. The provided path must refer to a valid TrueType
|
||||
/// font.
|
||||
fn register_font_from_path(
|
||||
&self,
|
||||
_path: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This renderer does not support registering custom fonts.".into())
|
||||
}
|
||||
|
||||
fn register_bitmap_font(&self, _font_data: &'static crate::graphics::BitmapFont) {
|
||||
crate::debug_log!(
|
||||
"Internal error: The current renderer cannot load fonts build with the `EmbedForSoftwareRenderer` option. Please use the software Renderer, or disable that option when building your slint files"
|
||||
);
|
||||
}
|
||||
|
||||
/// This function is called through the public API to register a callback that the backend needs to invoke during
|
||||
/// different phases of rendering.
|
||||
fn set_rendering_notifier(
|
||||
&self,
|
||||
_callback: Box<dyn crate::api::RenderingNotifier>,
|
||||
) -> Result<(), crate::api::SetRenderingNotifierError> {
|
||||
Err(crate::api::SetRenderingNotifierError::Unsupported)
|
||||
}
|
||||
|
||||
fn set_window_adapter(&self, _window_adapter: &Rc<dyn WindowAdapter>);
|
||||
|
||||
fn window_adapter(&self) -> Option<Rc<dyn WindowAdapter>>;
|
||||
|
||||
fn scale_factor(&self) -> Option<ScaleFactor> {
|
||||
self.window_adapter()
|
||||
.map(|window_adapter| ScaleFactor::new(window_adapter.window().scale_factor()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "shared-parley")]
|
||||
fn slint_context(&self) -> Option<crate::SlintContext> {
|
||||
self.window_adapter()
|
||||
.map(|wa| crate::window::WindowInner::from_pub(wa.window()).context().clone())
|
||||
}
|
||||
|
||||
fn default_font_size(&self) -> LogicalLength;
|
||||
|
||||
fn resize(&self, _size: crate::api::PhysicalSize) -> Result<(), PlatformError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-implement this function to support Window::take_snapshot(), i.e. return
|
||||
/// the contents of the window in an image buffer.
|
||||
fn take_snapshot(&self) -> Result<SharedPixelBuffer<Rgba8Pixel>, PlatformError> {
|
||||
Err("WindowAdapter::take_snapshot is not implemented by the platform".into())
|
||||
}
|
||||
|
||||
/// Whether the renderer supports transformations such as rotations and scaling or not.
|
||||
fn supports_transformations(&self) -> bool;
|
||||
}
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/*!
|
||||
This module enable runtime type information for the builtin items and
|
||||
property so that the viewer can handle them
|
||||
*/
|
||||
|
||||
#![allow(clippy::result_unit_err)] // We have nothing better to report
|
||||
|
||||
pub type FieldOffset<T, U> = const_field_offset::FieldOffset<T, U, const_field_offset::AllowPin>;
|
||||
use crate::Property;
|
||||
use crate::items::PropertyAnimation;
|
||||
use crate::properties::InterpolatedPropertyValue;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::{TryFrom, TryInto};
|
||||
use core::pin::Pin;
|
||||
|
||||
macro_rules! declare_ValueType {
|
||||
($($ty:ty,)*) => {
|
||||
pub trait ValueType: 'static + PartialEq + Default + Clone $(+ TryInto<$ty> + TryFrom<$ty>)* {}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! declare_ValueType_2 {
|
||||
($( $(#[$enum_doc:meta])* enum $Name:ident { $($body:tt)* })*) => {
|
||||
declare_ValueType![
|
||||
(),
|
||||
bool,
|
||||
u32,
|
||||
u64,
|
||||
i32,
|
||||
i64,
|
||||
f32,
|
||||
f64,
|
||||
crate::SharedString,
|
||||
crate::graphics::Image,
|
||||
crate::Color,
|
||||
crate::PathData,
|
||||
crate::animations::EasingCurve,
|
||||
crate::items::StandardListViewItem,
|
||||
crate::items::TableColumn,
|
||||
crate::input::KeyEvent,
|
||||
crate::Brush,
|
||||
crate::graphics::Point,
|
||||
crate::items::PointerEvent,
|
||||
crate::items::PointerScrollEvent,
|
||||
crate::lengths::LogicalLength,
|
||||
crate::lengths::LogicalPoint,
|
||||
crate::lengths::LogicalSize,
|
||||
crate::lengths::LogicalEdges,
|
||||
crate::component_factory::ComponentFactory,
|
||||
crate::api::LogicalPosition,
|
||||
crate::items::FontMetrics,
|
||||
crate::items::MenuEntry,
|
||||
crate::items::DropEvent,
|
||||
crate::model::ModelRc<crate::items::MenuEntry>,
|
||||
crate::styled_text::StyledText,
|
||||
crate::input::Keys,
|
||||
$(crate::items::$Name,)*
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
i_slint_common::for_each_enums!(declare_ValueType_2);
|
||||
|
||||
/// What kind of animation is on a binding
|
||||
pub enum AnimatedBindingKind {
|
||||
/// No animation is on the binding
|
||||
NotAnimated,
|
||||
/// Single animation
|
||||
Animation(Box<dyn Fn() -> PropertyAnimation>),
|
||||
/// Transition
|
||||
Transition(Box<dyn Fn() -> (PropertyAnimation, crate::animations::Instant)>),
|
||||
}
|
||||
|
||||
impl AnimatedBindingKind {
|
||||
/// return a PropertyAnimation if self contains AnimatedBindingKind::Animation
|
||||
pub fn as_animation(self) -> Option<PropertyAnimation> {
|
||||
match self {
|
||||
AnimatedBindingKind::NotAnimated => None,
|
||||
AnimatedBindingKind::Animation(a) => Some(a()),
|
||||
AnimatedBindingKind::Transition(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TwoWayBindingMapping<Value> {
|
||||
fn map_to(&self, value: &Value) -> Value;
|
||||
fn map_from(&self, value: &mut Value, from: &Value);
|
||||
}
|
||||
|
||||
impl<Value, F1: Fn(&Value) -> Value, F2: Fn(&mut Value, &Value)> TwoWayBindingMapping<Value>
|
||||
for (F1, F2)
|
||||
{
|
||||
fn map_to(&self, value: &Value) -> Value {
|
||||
(self.0)(value)
|
||||
}
|
||||
|
||||
fn map_from(&self, value: &mut Value, from: &Value) {
|
||||
(self.1)(value, from)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PropertyInfo<Item, Value> {
|
||||
fn get(&self, item: Pin<&Item>) -> Result<Value, ()>;
|
||||
fn set(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
value: Value,
|
||||
animation: Option<PropertyAnimation>,
|
||||
) -> Result<(), ()>;
|
||||
fn set_binding(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
binding: Box<dyn Fn() -> Value>,
|
||||
animation: AnimatedBindingKind,
|
||||
) -> Result<(), ()>;
|
||||
|
||||
/// The offset of the property in the item.
|
||||
/// The use of this is unsafe
|
||||
fn offset(&self) -> usize;
|
||||
|
||||
/// Returns self. This is just a trick to get auto-deref specialization of
|
||||
/// MaybeAnimatedPropertyInfoWrapper working.
|
||||
fn as_property_info(&'static self) -> &'static dyn PropertyInfo<Item, Value>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
/// Calls Property::link_two_ways with the property represented here and the property pointer
|
||||
///
|
||||
/// # Safety
|
||||
/// the property2 must be a pinned pointer to a Property of the same type
|
||||
#[allow(unsafe_code)]
|
||||
unsafe fn link_two_ways(&self, item: Pin<&Item>, property2: *const ());
|
||||
|
||||
/// Set the debug name of this property (only effective with `cfg(slint_debug_property)`)
|
||||
#[cfg(slint_debug_property)]
|
||||
fn set_debug_name(&self, _item: Pin<&Item>, _name: alloc::string::String) {}
|
||||
|
||||
/// Prepare the property for two way binding and return the "common" shared property in the TwoWayBinding
|
||||
fn prepare_for_two_way_binding(&self, item: Pin<&Item>) -> Pin<Rc<Property<Value>>>;
|
||||
|
||||
/// Link another property to this property with a mapping function
|
||||
///
|
||||
/// if the mapper is None, it uses the identity mapping
|
||||
fn link_two_way_with_map(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
property2: Pin<Rc<Property<Value>>>,
|
||||
mapper: Option<Rc<dyn TwoWayBindingMapping<Value>>>,
|
||||
);
|
||||
}
|
||||
|
||||
impl<Item: 'static, T, Value> PropertyInfo<Item, Value> for FieldOffset<Item, Property<T>>
|
||||
where
|
||||
Value: TryInto<T> + Clone + PartialEq + Default + 'static,
|
||||
T: TryInto<Value> + Clone + PartialEq + Default + 'static,
|
||||
{
|
||||
fn get(&self, item: Pin<&Item>) -> Result<Value, ()> {
|
||||
self.apply_pin(item).get().try_into().map_err(|_| ())
|
||||
}
|
||||
fn set(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
value: Value,
|
||||
animation: Option<PropertyAnimation>,
|
||||
) -> Result<(), ()> {
|
||||
if animation.is_some() {
|
||||
Err(())
|
||||
} else {
|
||||
self.apply_pin(item).set(value.try_into().map_err(|_| ())?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn set_binding(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
binding: Box<dyn Fn() -> Value>,
|
||||
animation: AnimatedBindingKind,
|
||||
) -> Result<(), ()> {
|
||||
if !matches!(animation, AnimatedBindingKind::NotAnimated) {
|
||||
Err(())
|
||||
} else {
|
||||
self.apply_pin(item).set_binding(move || {
|
||||
binding().try_into().map_err(|_| ()).expect("binding was of the wrong type")
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn offset(&self) -> usize {
|
||||
self.get_byte_offset()
|
||||
}
|
||||
|
||||
#[cfg(slint_debug_property)]
|
||||
fn set_debug_name(&self, item: Pin<&Item>, name: alloc::string::String) {
|
||||
self.apply_pin(item).debug_name.replace(name);
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
unsafe fn link_two_ways(&self, item: Pin<&Item>, property2: *const ()) {
|
||||
let p1 = self.apply_pin(item);
|
||||
// Safety: that's the invariant of this function
|
||||
let p2 = unsafe { Pin::new_unchecked((property2 as *const Property<T>).as_ref().unwrap()) };
|
||||
Property::link_two_way(p1, p2);
|
||||
}
|
||||
|
||||
fn prepare_for_two_way_binding(&self, item: Pin<&Item>) -> Pin<Rc<Property<Value>>> {
|
||||
if let Some(self_) =
|
||||
(self as &dyn core::any::Any).downcast_ref::<FieldOffset<Item, Property<Value>>>()
|
||||
&& let Some(p) = Property::check_common_property(self_.apply_pin(item))
|
||||
{
|
||||
return p;
|
||||
}
|
||||
|
||||
let p1 = self.apply_pin(item);
|
||||
let value: Value = p1.get_internal().try_into().unwrap_or_default();
|
||||
let shared_property = Rc::pin(Property::new(value));
|
||||
Property::link_two_way_with_map_to_common_property(
|
||||
shared_property.clone(),
|
||||
p1,
|
||||
|v| v.clone().try_into().unwrap_or_default(),
|
||||
|v, v2| *v = v2.clone().try_into().unwrap_or_default(),
|
||||
);
|
||||
shared_property
|
||||
}
|
||||
|
||||
fn link_two_way_with_map(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
prop2: Pin<Rc<Property<Value>>>,
|
||||
mapper: Option<Rc<dyn TwoWayBindingMapping<Value>>>,
|
||||
) {
|
||||
let prop1 = self.prepare_for_two_way_binding(item);
|
||||
|
||||
match mapper {
|
||||
Some(m1) => {
|
||||
let m2 = m1.clone();
|
||||
Property::link_two_way_with_map_to_common_property(
|
||||
prop2,
|
||||
prop1.as_ref(),
|
||||
move |value| m1.map_to(value),
|
||||
move |value, value2| m2.map_from(value, value2),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
Property::link_two_way_with_map_to_common_property(
|
||||
prop2,
|
||||
prop1.as_ref(),
|
||||
|value| value.clone(),
|
||||
|value, value2| *value = value2.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for a field offset that optionally implement PropertyInfo and uses
|
||||
/// the auto deref specialization trick
|
||||
#[derive(derive_more::Deref)]
|
||||
pub struct MaybeAnimatedPropertyInfoWrapper<T, U>(pub FieldOffset<T, U>);
|
||||
|
||||
impl<Item: 'static, T, Value> PropertyInfo<Item, Value>
|
||||
for MaybeAnimatedPropertyInfoWrapper<Item, Property<T>>
|
||||
where
|
||||
Value: TryInto<T> + Clone + PartialEq + Default + 'static,
|
||||
T: TryInto<Value> + Clone + PartialEq + Default + 'static,
|
||||
T: InterpolatedPropertyValue,
|
||||
{
|
||||
fn get(&self, item: Pin<&Item>) -> Result<Value, ()> {
|
||||
self.0.get(item)
|
||||
}
|
||||
fn set(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
value: Value,
|
||||
animation: Option<PropertyAnimation>,
|
||||
) -> Result<(), ()> {
|
||||
if let Some(animation) = animation {
|
||||
self.apply_pin(item).set_animated_value(value.try_into().map_err(|_| ())?, animation);
|
||||
Ok(())
|
||||
} else {
|
||||
self.0.set(item, value, None)
|
||||
}
|
||||
}
|
||||
fn set_binding(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
binding: Box<dyn Fn() -> Value>,
|
||||
animation: AnimatedBindingKind,
|
||||
) -> Result<(), ()> {
|
||||
// Put in a function that does not depends on Item to avoid code bloat
|
||||
fn set_binding_impl<T, Value>(
|
||||
p: Pin<&Property<T>>,
|
||||
binding: Box<dyn Fn() -> Value>,
|
||||
animation: AnimatedBindingKind,
|
||||
) -> Result<(), ()>
|
||||
where
|
||||
T: Clone + TryInto<Value> + InterpolatedPropertyValue + 'static,
|
||||
Value: TryInto<T> + 'static,
|
||||
{
|
||||
match animation {
|
||||
AnimatedBindingKind::NotAnimated => {
|
||||
p.set_binding(move || {
|
||||
binding().try_into().map_err(|_| ()).expect("binding was of the wrong type")
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
AnimatedBindingKind::Animation(animation) => {
|
||||
p.set_animated_binding(
|
||||
move || {
|
||||
binding()
|
||||
.try_into()
|
||||
.map_err(|_| ())
|
||||
.expect("binding was of the wrong type")
|
||||
},
|
||||
move || (animation(), None),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
AnimatedBindingKind::Transition(tr) => {
|
||||
p.set_animated_binding(
|
||||
move || {
|
||||
binding()
|
||||
.try_into()
|
||||
.map_err(|_| ())
|
||||
.expect("binding was of the wrong type")
|
||||
},
|
||||
move || {
|
||||
let (animation, start_time) = tr();
|
||||
(animation, Some(start_time))
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
set_binding_impl(self.apply_pin(item), binding, animation)
|
||||
}
|
||||
fn offset(&self) -> usize {
|
||||
self.get_byte_offset()
|
||||
}
|
||||
|
||||
#[cfg(slint_debug_property)]
|
||||
fn set_debug_name(&self, item: Pin<&Item>, name: alloc::string::String) {
|
||||
self.0.set_debug_name(item, name);
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
unsafe fn link_two_ways(&self, item: Pin<&Item>, property2: *const ()) {
|
||||
let p1 = self.apply_pin(item);
|
||||
// Safety: that's the invariant of this function
|
||||
let p2 = unsafe { Pin::new_unchecked((property2 as *const Property<T>).as_ref().unwrap()) };
|
||||
Property::link_two_way(p1, p2);
|
||||
}
|
||||
|
||||
fn prepare_for_two_way_binding(&self, item: Pin<&Item>) -> Pin<Rc<Property<Value>>> {
|
||||
self.0.prepare_for_two_way_binding(item)
|
||||
}
|
||||
|
||||
fn link_two_way_with_map(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
property2: Pin<Rc<Property<Value>>>,
|
||||
mapper: Option<Rc<dyn TwoWayBindingMapping<Value>>>,
|
||||
) {
|
||||
self.0.link_two_way_with_map(item, property2, mapper)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CallbackInfo<Item, Value> {
|
||||
fn call(&self, item: Pin<&Item>, args: &[Value]) -> Result<Value, ()>;
|
||||
fn set_handler(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
handler: Box<dyn Fn(&[Value]) -> Value>,
|
||||
) -> Result<(), ()>;
|
||||
}
|
||||
|
||||
impl<Item, Value: Default + 'static, Ret: Default> CallbackInfo<Item, Value>
|
||||
for FieldOffset<Item, crate::Callback<(), Ret>>
|
||||
where
|
||||
Value: TryInto<Ret>,
|
||||
Ret: TryInto<Value>,
|
||||
{
|
||||
fn call(&self, item: Pin<&Item>, _args: &[Value]) -> Result<Value, ()> {
|
||||
self.apply_pin(item).call(&()).try_into().map_err(|_| ())
|
||||
}
|
||||
|
||||
fn set_handler(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
handler: Box<dyn Fn(&[Value]) -> Value>,
|
||||
) -> Result<(), ()> {
|
||||
self.apply_pin(item).set_handler(move |()| handler(&[]).try_into().ok().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Value: Clone + Default + 'static, T: Clone, Ret: Default> CallbackInfo<Item, Value>
|
||||
for FieldOffset<Item, crate::Callback<(T,), Ret>>
|
||||
where
|
||||
Value: TryInto<T>,
|
||||
T: TryInto<Value>,
|
||||
Value: TryInto<Ret>,
|
||||
Ret: TryInto<Value>,
|
||||
{
|
||||
fn call(&self, item: Pin<&Item>, args: &[Value]) -> Result<Value, ()> {
|
||||
let value = args.first().ok_or(())?;
|
||||
let value = value.clone().try_into().map_err(|_| ())?;
|
||||
self.apply_pin(item).call(&(value,)).try_into().map_err(|_| ())
|
||||
}
|
||||
|
||||
fn set_handler(
|
||||
&self,
|
||||
item: Pin<&Item>,
|
||||
handler: Box<dyn Fn(&[Value]) -> Value>,
|
||||
) -> Result<(), ()> {
|
||||
self.apply_pin(item).set_handler(move |(val,)| {
|
||||
let val: Value = val.clone().try_into().ok().unwrap();
|
||||
handler(&[val]).try_into().ok().unwrap()
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FieldInfo<Item, Value> {
|
||||
fn set_field(&self, item: &mut Item, value: Value) -> Result<(), ()>;
|
||||
}
|
||||
|
||||
impl<Item, T, Value: 'static> FieldInfo<Item, Value> for FieldOffset<Item, T>
|
||||
where
|
||||
Value: TryInto<T>,
|
||||
T: TryInto<Value>,
|
||||
{
|
||||
fn set_field(&self, item: &mut Item, value: Value) -> Result<(), ()> {
|
||||
*self.apply_mut(item) = value.try_into().map_err(|_| ())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BuiltinItem: Sized {
|
||||
fn name() -> &'static str;
|
||||
fn properties<Value: ValueType>() -> Vec<(&'static str, &'static dyn PropertyInfo<Self, Value>)>;
|
||||
fn fields<Value: ValueType>() -> Vec<(&'static str, &'static dyn FieldInfo<Self, Value>)>;
|
||||
fn callbacks<Value: ValueType>() -> Vec<(&'static str, &'static dyn CallbackInfo<Self, Value>)>;
|
||||
}
|
||||
|
||||
/// Trait implemented by builtin globals
|
||||
pub trait BuiltinGlobal: BuiltinItem {
|
||||
fn new() -> Pin<Rc<Self>>;
|
||||
}
|
||||
+702
@@ -0,0 +1,702 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! module for the SharedVector and related things
|
||||
#![allow(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
use core::fmt::Debug;
|
||||
use core::mem::MaybeUninit;
|
||||
use core::ops::Deref;
|
||||
use core::ptr::NonNull;
|
||||
|
||||
use portable_atomic as atomic;
|
||||
|
||||
#[repr(C)]
|
||||
struct SharedVectorHeader {
|
||||
refcount: atomic::AtomicIsize,
|
||||
size: usize,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct SharedVectorInner<T> {
|
||||
header: SharedVectorHeader,
|
||||
data: MaybeUninit<T>,
|
||||
}
|
||||
|
||||
fn compute_inner_layout<T>(capacity: usize) -> core::alloc::Layout {
|
||||
core::alloc::Layout::new::<SharedVectorHeader>()
|
||||
.extend(core::alloc::Layout::array::<T>(capacity).unwrap())
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
unsafe fn drop_inner<T>(mut inner: NonNull<SharedVectorInner<T>>) {
|
||||
unsafe {
|
||||
debug_assert_eq!(inner.as_ref().header.refcount.load(atomic::Ordering::Relaxed), 0);
|
||||
let data_ptr = inner.as_mut().data.as_mut_ptr();
|
||||
for x in 0..inner.as_ref().header.size {
|
||||
core::ptr::drop_in_place(data_ptr.add(x));
|
||||
}
|
||||
alloc::alloc::dealloc(
|
||||
inner.as_ptr() as *mut u8,
|
||||
compute_inner_layout::<T>(inner.as_ref().header.capacity),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate the memory for the SharedVector with the given capacity. Return the inner with size and refcount set to 1
|
||||
fn alloc_with_capacity<T>(capacity: usize) -> NonNull<SharedVectorInner<T>> {
|
||||
let ptr = unsafe { ::alloc::alloc::alloc(compute_inner_layout::<T>(capacity)) };
|
||||
assert!(!ptr.is_null(), "allocation of {capacity:?} bytes failed");
|
||||
unsafe {
|
||||
core::ptr::write(
|
||||
ptr as *mut SharedVectorHeader,
|
||||
SharedVectorHeader { refcount: 1.into(), size: 0, capacity },
|
||||
);
|
||||
}
|
||||
NonNull::new(ptr).unwrap().cast()
|
||||
}
|
||||
|
||||
/// Return a new capacity suitable for this vector
|
||||
/// Loosely based on alloc::raw_vec::RawVec::grow_amortized.
|
||||
fn capacity_for_grow(current_cap: usize, required_cap: usize, elem_size: usize) -> usize {
|
||||
if current_cap >= required_cap {
|
||||
return current_cap;
|
||||
}
|
||||
let cap = core::cmp::max(current_cap * 2, required_cap);
|
||||
let min_non_zero_cap = if elem_size == 1 {
|
||||
8
|
||||
} else if elem_size <= 1024 {
|
||||
4
|
||||
} else {
|
||||
1
|
||||
};
|
||||
core::cmp::max(min_non_zero_cap, cap)
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
/// SharedVector holds a reference-counted read-only copy of `[T]`.
|
||||
pub struct SharedVector<T> {
|
||||
inner: NonNull<SharedVectorInner<T>>,
|
||||
}
|
||||
|
||||
// Safety: We use atomic reference counting, and if T is Send and Sync, we can send the vector to another thread
|
||||
unsafe impl<T: Send + Sync> Send for SharedVector<T> {}
|
||||
// Safety: We use atomic reference counting, and if T is Send and Sync, we can access the vector from multiple threads
|
||||
unsafe impl<T: Send + Sync> Sync for SharedVector<T> {}
|
||||
|
||||
impl<T> Drop for SharedVector<T> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if self
|
||||
.inner
|
||||
.cast::<SharedVectorHeader>()
|
||||
.as_ref()
|
||||
.refcount
|
||||
.load(atomic::Ordering::Relaxed)
|
||||
< 0
|
||||
{
|
||||
return;
|
||||
}
|
||||
if self.inner.as_ref().header.refcount.fetch_sub(1, atomic::Ordering::SeqCst) == 1 {
|
||||
drop_inner(self.inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for SharedVector<T> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
if self
|
||||
.inner
|
||||
.cast::<SharedVectorHeader>()
|
||||
.as_ref()
|
||||
.refcount
|
||||
.load(atomic::Ordering::Relaxed)
|
||||
> 0
|
||||
{
|
||||
self.inner.as_ref().header.refcount.fetch_add(1, atomic::Ordering::SeqCst);
|
||||
}
|
||||
SharedVector { inner: self.inner }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SharedVector<T> {
|
||||
/// Create a new empty array with a pre-allocated capacity in number of items
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self { inner: alloc_with_capacity(capacity) }
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *const T {
|
||||
unsafe { self.inner.as_ref().data.as_ptr() }
|
||||
}
|
||||
|
||||
/// Number of elements in the array
|
||||
pub fn len(&self) -> usize {
|
||||
unsafe { self.inner.cast::<SharedVectorHeader>().as_ref().size }
|
||||
}
|
||||
|
||||
/// Return true if the SharedVector is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Return a slice to the array
|
||||
pub fn as_slice(&self) -> &[T] {
|
||||
if self.is_empty() {
|
||||
&[]
|
||||
} else {
|
||||
// Safety: When len > 0, we know that the pointer holds an array of the size of len
|
||||
unsafe { core::slice::from_raw_parts(self.as_ptr(), self.len()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of elements the vector can hold without reallocating, when not shared
|
||||
fn capacity(&self) -> usize {
|
||||
unsafe { self.inner.cast::<SharedVectorHeader>().as_ref().capacity }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SharedVector<T> {
|
||||
/// Create a SharedVector from a slice
|
||||
pub fn from_slice(slice: &[T]) -> SharedVector<T> {
|
||||
Self::from(slice)
|
||||
}
|
||||
|
||||
/// Ensure that the reference count is 1 so the array can be changed.
|
||||
/// If that's not the case, the array will be cloned
|
||||
fn detach(&mut self, new_capacity: usize) {
|
||||
let is_shared =
|
||||
unsafe { self.inner.as_ref().header.refcount.load(atomic::Ordering::Relaxed) } != 1;
|
||||
if !is_shared && new_capacity <= self.capacity() {
|
||||
return;
|
||||
}
|
||||
let mut new_array = SharedVector::with_capacity(new_capacity);
|
||||
core::mem::swap(&mut self.inner, &mut new_array.inner);
|
||||
let mut size = 0;
|
||||
for x in new_array.into_iter() {
|
||||
assert_ne!(size, new_capacity);
|
||||
unsafe {
|
||||
core::ptr::write(self.inner.as_mut().data.as_mut_ptr().add(size), x);
|
||||
size += 1;
|
||||
self.inner.as_mut().header.size = size;
|
||||
}
|
||||
if size == new_capacity {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mutable slice to the array. If the array was shared, this will make a copy of the array.
|
||||
pub fn make_mut_slice(&mut self) -> &mut [T] {
|
||||
self.detach(self.len());
|
||||
unsafe { core::slice::from_raw_parts_mut(self.as_ptr() as *mut T, self.len()) }
|
||||
}
|
||||
|
||||
/// Add an element to the array. If the array was shared, this will make a copy of the array.
|
||||
pub fn push(&mut self, value: T) {
|
||||
self.detach(capacity_for_grow(self.capacity(), self.len() + 1, core::mem::size_of::<T>()));
|
||||
unsafe {
|
||||
core::ptr::write(
|
||||
self.inner.as_mut().data.as_mut_ptr().add(self.inner.as_mut().header.size),
|
||||
value,
|
||||
);
|
||||
self.inner.as_mut().header.size += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes last element from the array and returns it.
|
||||
/// If the array was shared, this will make a copy of the array.
|
||||
pub fn pop(&mut self) -> Option<T> {
|
||||
if self.is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.detach(self.len());
|
||||
unsafe {
|
||||
self.inner.as_mut().header.size -= 1;
|
||||
Some(core::ptr::read(self.inner.as_mut().data.as_mut_ptr().add(self.len())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the array to the given size.
|
||||
/// If the array was smaller new elements will be initialized with the value.
|
||||
/// If the array was bigger, extra elements will be discarded
|
||||
///
|
||||
/// ```
|
||||
/// use i_slint_core::SharedVector;
|
||||
/// let mut shared_vector = SharedVector::<u32>::from_slice(&[1, 2, 3]);
|
||||
/// shared_vector.resize(5, 8);
|
||||
/// assert_eq!(shared_vector.as_slice(), &[1, 2, 3, 8, 8]);
|
||||
/// shared_vector.resize(2, 0);
|
||||
/// assert_eq!(shared_vector.as_slice(), &[1, 2]);
|
||||
/// ```
|
||||
pub fn resize(&mut self, new_len: usize, value: T) {
|
||||
if self.len() == new_len {
|
||||
return;
|
||||
}
|
||||
self.detach(new_len);
|
||||
// Safety: detach ensured that the array is not shared.
|
||||
let inner = unsafe { self.inner.as_mut() };
|
||||
|
||||
if inner.header.size >= new_len {
|
||||
self.shrink(new_len);
|
||||
} else {
|
||||
while inner.header.size < new_len {
|
||||
// Safety: The array must have a capacity of at least new_len because of the detach call earlier
|
||||
unsafe {
|
||||
core::ptr::write(inner.data.as_mut_ptr().add(inner.header.size), value.clone());
|
||||
}
|
||||
inner.header.size += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shrink(&mut self, new_len: usize) {
|
||||
if self.len() == new_len {
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
unsafe { self.inner.as_ref().header.refcount.load(atomic::Ordering::Relaxed) } == 1
|
||||
);
|
||||
// Safety: caller (and above debug_assert) must ensure that the array is not shared.
|
||||
let inner = unsafe { self.inner.as_mut() };
|
||||
|
||||
while inner.header.size > new_len {
|
||||
inner.header.size -= 1;
|
||||
// Safety: The array was of size inner.header.size, so there should be an element there
|
||||
unsafe {
|
||||
core::ptr::drop_in_place(inner.data.as_mut_ptr().add(inner.header.size));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the vector and removes all elements.
|
||||
pub fn clear(&mut self) {
|
||||
let is_shared =
|
||||
unsafe { self.inner.as_ref().header.refcount.load(atomic::Ordering::Relaxed) } != 1;
|
||||
if is_shared {
|
||||
*self = SharedVector::default();
|
||||
} else {
|
||||
self.shrink(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserves capacity for at least `additional` bytes more than the current vector's length.
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.detach((self.len() + additional).max(self.capacity()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for SharedVector<T> {
|
||||
type Target = [T];
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
/* FIXME: is this a good idea to implement DerefMut knowing what it might detach?
|
||||
impl<T> DerefMut for SharedVector<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.as_mut_slice()
|
||||
}
|
||||
}*/
|
||||
|
||||
impl<T: Clone> From<&[T]> for SharedVector<T> {
|
||||
fn from(slice: &[T]) -> Self {
|
||||
let capacity = slice.len();
|
||||
let mut result = Self::with_capacity(capacity);
|
||||
for x in slice {
|
||||
unsafe {
|
||||
core::ptr::write(
|
||||
result.inner.as_mut().data.as_mut_ptr().add(result.inner.as_mut().header.size),
|
||||
x.clone(),
|
||||
);
|
||||
result.inner.as_mut().header.size += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> From<[T; N]> for SharedVector<T> {
|
||||
fn from(array: [T; N]) -> Self {
|
||||
array.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromIterator<T> for SharedVector<T> {
|
||||
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
|
||||
let mut iter = iter.into_iter();
|
||||
let mut capacity = iter.size_hint().0;
|
||||
let mut result = Self::with_capacity(capacity);
|
||||
let mut size = 0;
|
||||
while let Some(x) = iter.next() {
|
||||
if size >= capacity {
|
||||
capacity = capacity_for_grow(
|
||||
capacity,
|
||||
size + 1 + iter.size_hint().0,
|
||||
core::mem::size_of::<T>(),
|
||||
);
|
||||
unsafe {
|
||||
result.inner.as_ref().header.refcount.store(0, atomic::Ordering::Relaxed)
|
||||
};
|
||||
let mut iter = IntoIter(IntoIterInner::UnShared(result.inner, 0));
|
||||
result.inner = alloc_with_capacity::<T>(capacity);
|
||||
match &mut iter.0 {
|
||||
IntoIterInner::UnShared(old_inner, begin) => {
|
||||
while *begin < size {
|
||||
unsafe {
|
||||
core::ptr::write(
|
||||
result.inner.as_mut().data.as_mut_ptr().add(*begin),
|
||||
core::ptr::read(old_inner.as_ref().data.as_ptr().add(*begin)),
|
||||
);
|
||||
*begin += 1;
|
||||
result.inner.as_mut().header.size = *begin;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(result.len(), size);
|
||||
debug_assert!(result.capacity() > size);
|
||||
unsafe {
|
||||
core::ptr::write(result.inner.as_mut().data.as_mut_ptr().add(size), x);
|
||||
size += 1;
|
||||
result.inner.as_mut().header.size = size;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Extend<T> for SharedVector<T> {
|
||||
fn extend<X: IntoIterator<Item = T>>(&mut self, iter: X) {
|
||||
let iter = iter.into_iter();
|
||||
let hint = iter.size_hint().0;
|
||||
if hint > 0 {
|
||||
self.detach(capacity_for_grow(
|
||||
self.capacity(),
|
||||
self.len() + hint,
|
||||
core::mem::size_of::<T>(),
|
||||
));
|
||||
}
|
||||
for item in iter {
|
||||
self.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static SHARED_NULL: SharedVectorHeader =
|
||||
SharedVectorHeader { refcount: atomic::AtomicIsize::new(-1), size: 0, capacity: 0 };
|
||||
|
||||
impl<T> Default for SharedVector<T> {
|
||||
fn default() -> Self {
|
||||
SharedVector { inner: NonNull::from(&SHARED_NULL).cast() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for SharedVector<T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.as_slice().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<[T]> for SharedVector<T> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[T] {
|
||||
self.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> PartialEq<U> for SharedVector<T>
|
||||
where
|
||||
U: ?Sized + AsRef<[T]>,
|
||||
T: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &U) -> bool {
|
||||
self.as_slice() == other.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq> Eq for SharedVector<T> {}
|
||||
|
||||
impl<T: Clone> IntoIterator for SharedVector<T> {
|
||||
type Item = T;
|
||||
type IntoIter = IntoIter<T>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
IntoIter(unsafe {
|
||||
if self.inner.as_ref().header.refcount.load(atomic::Ordering::Relaxed) == 1 {
|
||||
let inner = self.inner;
|
||||
core::mem::forget(self);
|
||||
inner.as_ref().header.refcount.store(0, atomic::Ordering::Relaxed);
|
||||
IntoIterInner::UnShared(inner, 0)
|
||||
} else {
|
||||
IntoIterInner::Shared(self, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::ser::SerializeSeq;
|
||||
#[cfg(feature = "serde")]
|
||||
impl<T> serde::Serialize for SharedVector<T>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.len()))?;
|
||||
for item in self.iter() {
|
||||
seq.serialize_element(item)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de, T> serde::Deserialize<'de> for SharedVector<T>
|
||||
where
|
||||
T: Clone + serde::Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let mut elements: alloc::vec::Vec<T> = serde::Deserialize::deserialize(deserializer)?;
|
||||
let mut shared_vec = SharedVector::with_capacity(elements.len());
|
||||
for elem in elements.drain(..) {
|
||||
shared_vec.push(elem);
|
||||
}
|
||||
Ok(shared_vec)
|
||||
}
|
||||
}
|
||||
|
||||
enum IntoIterInner<T> {
|
||||
Shared(SharedVector<T>, usize),
|
||||
// Elements up to the usize member are already moved out
|
||||
UnShared(NonNull<SharedVectorInner<T>>, usize),
|
||||
}
|
||||
|
||||
impl<T> Drop for IntoIterInner<T> {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
IntoIterInner::Shared(..) => { /* drop of SharedVector takes care of it */ }
|
||||
IntoIterInner::UnShared(inner, begin) => unsafe {
|
||||
debug_assert_eq!(inner.as_ref().header.refcount.load(atomic::Ordering::Relaxed), 0);
|
||||
let data_ptr = inner.as_mut().data.as_mut_ptr();
|
||||
for x in (*begin)..inner.as_ref().header.size {
|
||||
core::ptr::drop_in_place(data_ptr.add(x));
|
||||
}
|
||||
::alloc::alloc::dealloc(
|
||||
inner.as_ptr() as *mut u8,
|
||||
compute_inner_layout::<T>(inner.as_ref().header.capacity),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator that moves out of a SharedVector.
|
||||
///
|
||||
/// This `struct` is created by the `into_iter` method on [`SharedVector`] (provided
|
||||
/// by the [`IntoIterator`] trait).
|
||||
pub struct IntoIter<T>(IntoIterInner<T>);
|
||||
|
||||
impl<T: Clone> Iterator for IntoIter<T> {
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match &mut self.0 {
|
||||
IntoIterInner::Shared(array, moved) => {
|
||||
let result = array.as_slice().get(*moved).cloned();
|
||||
*moved += 1;
|
||||
result
|
||||
}
|
||||
IntoIterInner::UnShared(inner, begin) => unsafe {
|
||||
if *begin < inner.as_ref().header.size {
|
||||
let r = core::ptr::read(inner.as_ref().data.as_ptr().add(*begin));
|
||||
*begin += 1;
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
let x: SharedVector<i32> = SharedVector::from([1, 2, 3]);
|
||||
let y: SharedVector<i32> = SharedVector::from([3, 2, 1]);
|
||||
assert_eq!(x, x.clone());
|
||||
assert_ne!(x, y);
|
||||
let z: [i32; 3] = [1, 2, 3];
|
||||
assert_eq!(z, x.as_slice());
|
||||
let vec: std::vec::Vec<i32> = std::vec![1, 2, 3];
|
||||
assert_eq!(x, vec);
|
||||
let def: SharedVector<i32> = Default::default();
|
||||
assert_eq!(def, SharedVector::<i32>::default());
|
||||
assert_ne!(def, x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_test() {
|
||||
let mut x: SharedVector<i32> = SharedVector::from([1, 2, 3]);
|
||||
let y = x.clone();
|
||||
x.push(4);
|
||||
x.push(5);
|
||||
x.push(6);
|
||||
assert_eq!(x.as_slice(), &[1, 2, 3, 4, 5, 6]);
|
||||
assert_eq!(y.as_slice(), &[1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn invalid_capacity_test() {
|
||||
let _: SharedVector<u8> = SharedVector::with_capacity(usize::MAX / 2 - 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_from_iter_with_no_size_hint() {
|
||||
use std::string::{String, ToString};
|
||||
struct NoSizeHintIter<'a> {
|
||||
data: &'a [&'a str],
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl Iterator for NoSizeHintIter<'_> {
|
||||
type Item = String;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.i >= self.data.len() {
|
||||
return None;
|
||||
}
|
||||
let item = self.data[self.i];
|
||||
self.i += 1;
|
||||
Some(item.to_string())
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, None)
|
||||
}
|
||||
}
|
||||
|
||||
// 5 elements to be above the initial "grow"-capacity of 4 and thus require one realloc.
|
||||
let input = NoSizeHintIter { data: &["Hello", "sweet", "world", "of", "iterators"], i: 0 };
|
||||
|
||||
let shared_vec: SharedVector<String> = input.collect();
|
||||
assert_eq!(shared_vec.as_slice(), &["Hello", "sweet", "world", "of", "iterators"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capacity_grows_only_when_needed() {
|
||||
let mut vec: SharedVector<u8> = SharedVector::with_capacity(2);
|
||||
vec.push(0);
|
||||
assert_eq!(vec.capacity(), 2);
|
||||
vec.push(0);
|
||||
assert_eq!(vec.capacity(), 2);
|
||||
vec.push(0);
|
||||
assert_eq!(vec.len(), 3);
|
||||
assert!(vec.capacity() > 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vector_clear() {
|
||||
let mut vec: SharedVector<std::string::String> = Default::default();
|
||||
vec.clear();
|
||||
vec.push("Hello".into());
|
||||
vec.push("World".into());
|
||||
vec.push("of".into());
|
||||
vec.push("Vectors".into());
|
||||
|
||||
let mut copy = vec.clone();
|
||||
|
||||
assert_eq!(vec.len(), 4);
|
||||
let orig_cap = vec.capacity();
|
||||
assert!(orig_cap >= vec.len());
|
||||
vec.clear();
|
||||
assert_eq!(vec.len(), 0);
|
||||
assert_eq!(vec.capacity(), 0); // vec was shared, so start with new empty vector.
|
||||
vec.push("Welcome back".into());
|
||||
assert_eq!(vec.len(), 1);
|
||||
assert!(vec.capacity() >= vec.len());
|
||||
|
||||
assert_eq!(copy.len(), 4);
|
||||
assert_eq!(copy.capacity(), orig_cap);
|
||||
copy.clear(); // copy is not shared (anymore), retain capacity.
|
||||
assert_eq!(copy.capacity(), orig_cap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pop_test() {
|
||||
let mut x: SharedVector<i32> = SharedVector::from([1, 2, 3]);
|
||||
let y = x.clone();
|
||||
assert_eq!(x.pop(), Some(3));
|
||||
assert_eq!(x.pop(), Some(2));
|
||||
assert_eq!(x.pop(), Some(1));
|
||||
assert_eq!(x.pop(), None);
|
||||
assert!(x.is_empty());
|
||||
assert_eq!(y.as_slice(), &[1, 2, 3]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
pub(crate) mod ffi {
|
||||
use super::*;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// This function is used for the low-level C++ interface to allocate the backing vector of a SharedVector.
|
||||
pub unsafe extern "C" fn slint_shared_vector_allocate(size: usize, align: usize) -> *mut u8 {
|
||||
unsafe { alloc::alloc::alloc(alloc::alloc::Layout::from_size_align(size, align).unwrap()) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// This function is used for the low-level C++ interface to deallocate the backing vector of a SharedVector
|
||||
pub unsafe extern "C" fn slint_shared_vector_free(ptr: *mut u8, size: usize, align: usize) {
|
||||
unsafe {
|
||||
alloc::alloc::dealloc(ptr, alloc::alloc::Layout::from_size_align(size, align).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// This function is used for the low-level C++ interface to initialize the empty SharedVector.
|
||||
pub unsafe extern "C" fn slint_shared_vector_empty() -> *const u8 {
|
||||
&SHARED_NULL as *const _ as *const u8
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn test_serialize_deserialize_sharedvector() {
|
||||
let v = SharedVector::from([1, 2, 3]);
|
||||
let serialized = serde_json::to_string(&v).unwrap();
|
||||
let deserialized: SharedVector<i32> = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(v, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reserve() {
|
||||
let mut v = SharedVector::from([1, 2, 3]);
|
||||
assert_eq!(v.capacity(), 3);
|
||||
v.reserve(1);
|
||||
assert_eq!(v.capacity(), 4);
|
||||
assert_eq!(v.len(), 3);
|
||||
v.push(4);
|
||||
v.push(5);
|
||||
assert_eq!(v.len(), 5);
|
||||
assert_eq!(v.capacity(), 8);
|
||||
v.reserve(1);
|
||||
assert_eq!(v.capacity(), 8);
|
||||
v.reserve(8);
|
||||
assert_eq!(v.len(), 5);
|
||||
assert_eq!(v.capacity(), 13);
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! FFI-friendly slice
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use core::{cmp::PartialEq, fmt::Debug, marker::PhantomData, ptr::NonNull};
|
||||
|
||||
/// That's basically the same as `&'a [T]` but `repr(C)`
|
||||
///
|
||||
/// Can be constructed from a slice using the from trait.
|
||||
///
|
||||
/// ```
|
||||
/// use i_slint_core::slice::Slice;
|
||||
/// let x = Slice::from_slice(&[1, 2, 3]);
|
||||
/// assert_eq!(x.len(), 3);
|
||||
/// assert_eq!(x[1], 2);
|
||||
/// let slice : &'static [u32] = x.as_slice();
|
||||
/// ```
|
||||
///
|
||||
/// Comparing two Slice compare their pointer, not the content.
|
||||
/// ```
|
||||
/// use i_slint_core::slice::Slice;
|
||||
/// let a = Slice::from_slice(&[1, 2, 3]);
|
||||
/// let slice = [1, 2, 3, 4];
|
||||
/// let b = Slice::from(&slice[..3]);
|
||||
/// // two slice coming from the same pointer are equal.
|
||||
/// assert_eq!(b, Slice::from(&slice[..3]));
|
||||
/// // these are different because the pointers are different
|
||||
/// assert_ne!(a, b);
|
||||
/// // use as_slice to compare the contents
|
||||
/// assert_eq!(a.as_slice(), b.as_slice());
|
||||
/// ```
|
||||
#[repr(C)]
|
||||
#[derive(PartialEq)]
|
||||
pub struct Slice<'a, T> {
|
||||
/// Invariant, this is a valid slice of len `len`
|
||||
ptr: NonNull<T>,
|
||||
len: usize,
|
||||
phantom: PhantomData<&'a [T]>,
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for Slice<'_, T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.as_slice().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
// Need to implement manually otherwise it is not implemented if T do not implement Copy / Clone
|
||||
impl<T> Copy for Slice<'_, T> {}
|
||||
|
||||
impl<T> Clone for Slice<'_, T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Slice<'a, T> {
|
||||
/// Return a slice
|
||||
pub fn as_slice(self) -> &'a [T] {
|
||||
// Safety: ptr is supposed to be a valid slice of given length
|
||||
unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
|
||||
}
|
||||
|
||||
/// Create from a native slice
|
||||
pub const fn from_slice(slice: &'a [T]) -> Self {
|
||||
Slice {
|
||||
// Safety: a slice is never null
|
||||
ptr: unsafe { NonNull::new_unchecked(slice.as_ptr() as *mut T) },
|
||||
len: slice.len(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<&'a [T]> for Slice<'a, T> {
|
||||
fn from(slice: &'a [T]) -> Self {
|
||||
Self::from_slice(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> core::ops::Deref for Slice<'_, T> {
|
||||
type Target = [T];
|
||||
fn deref(&self) -> &[T] {
|
||||
self.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Slice<'_, T> {
|
||||
fn default() -> Self {
|
||||
Self::from_slice(&[])
|
||||
}
|
||||
}
|
||||
|
||||
/// Safety: Slice is the same as a rust slice, and a slice of Sync T is Sync
|
||||
unsafe impl<T: Sync> Sync for Slice<'_, T> {}
|
||||
/// Safety: Slice is the same as a rust slice, and a slice of Send T is Sync
|
||||
unsafe impl<T: Sync> Send for Slice<'_, T> {}
|
||||
+708
@@ -0,0 +1,708 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! module for the SharedString and related things
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use crate::SharedVector;
|
||||
use alloc::string::String;
|
||||
use core::fmt::{Debug, Display, Write};
|
||||
use core::ops::Deref;
|
||||
#[cfg(not(feature = "std"))]
|
||||
#[allow(unused)]
|
||||
use num_traits::Float;
|
||||
|
||||
/// This macro is the same as [`std::format!`], but it returns a [`SharedString`] instead.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```rust
|
||||
/// let s : slint::SharedString = slint::format!("Hello {}", "world");
|
||||
/// assert_eq!(s, slint::SharedString::from("Hello world"));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! format {
|
||||
($($arg:tt)*) => {{
|
||||
$crate::string::format(core::format_args!($($arg)*))
|
||||
}}
|
||||
}
|
||||
|
||||
/// A string type used by the Slint run-time.
|
||||
///
|
||||
/// SharedString uses implicit data sharing to make it efficient to pass around copies. When
|
||||
/// cloning, a reference to the data is cloned, not the data itself. The data itself is only copied
|
||||
/// when modifying it, for example using [push_str](SharedString::push_str). This is also called copy-on-write.
|
||||
///
|
||||
/// Under the hood the string data is UTF-8 encoded and it is always terminated with a null character.
|
||||
///
|
||||
/// `SharedString` implements [`Deref<Target=str>`] so it can be easily passed to any function taking a `&str`.
|
||||
/// It also implement `From` such that it an easily be converted to and from the typical rust String type with `.into()`
|
||||
#[derive(Clone, Default)]
|
||||
#[repr(C)]
|
||||
pub struct SharedString {
|
||||
// Invariant: valid utf-8, `\0` terminated
|
||||
inner: SharedVector<u8>,
|
||||
}
|
||||
|
||||
impl SharedString {
|
||||
/// Creates a new empty string
|
||||
///
|
||||
/// Same as `SharedString::default()`
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *const u8 {
|
||||
self.inner.as_ptr()
|
||||
}
|
||||
|
||||
/// Size of the string, in bytes. This excludes the terminating null character.
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len().saturating_sub(1)
|
||||
}
|
||||
|
||||
/// Return true if the String is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Return a slice to the string
|
||||
pub fn as_str(&self) -> &str {
|
||||
// Safety: self.as_ptr is a pointer from the inner which has utf-8
|
||||
unsafe {
|
||||
core::str::from_utf8_unchecked(core::slice::from_raw_parts(self.as_ptr(), self.len()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a string to this string
|
||||
///
|
||||
/// ```
|
||||
/// # use i_slint_core::SharedString;
|
||||
/// let mut hello = SharedString::from("Hello");
|
||||
/// hello.push_str(", ");
|
||||
/// hello.push_str("World");
|
||||
/// hello.push_str("!");
|
||||
/// assert_eq!(hello, "Hello, World!");
|
||||
/// ```
|
||||
pub fn push_str(&mut self, x: &str) {
|
||||
let mut iter = x.as_bytes().iter().copied();
|
||||
if self.inner.is_empty() {
|
||||
self.inner.extend(iter.chain(core::iter::once(0)));
|
||||
} else if let Some(first) = iter.next() {
|
||||
// We skip the `first` from `iter` because we will write it at the
|
||||
// location of the previous `\0`, after extend did the re-alloc of the
|
||||
// right size
|
||||
let prev_len = self.len();
|
||||
self.inner.extend(iter.chain(core::iter::once(0)));
|
||||
self.inner.make_mut_slice()[prev_len] = first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SharedString {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SharedString {
|
||||
fn from(value: &str) -> Self {
|
||||
SharedString {
|
||||
inner: SharedVector::from_iter(
|
||||
value.as_bytes().iter().cloned().chain(core::iter::once(0)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SharedString {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
Debug::fmt(self.as_str(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SharedString {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
Display::fmt(self.as_str(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for SharedString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for SharedString {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let string = self.as_str();
|
||||
serializer.serialize_str(string)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for SharedString {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let string: &str = serde::Deserialize::deserialize(deserializer)?;
|
||||
Ok(SharedString::from(string))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl AsRef<std::ffi::CStr> for SharedString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &std::ffi::CStr {
|
||||
if self.inner.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
// Safety: we ensure that there is always a terminated \0
|
||||
debug_assert_eq!(self.inner.as_slice()[self.inner.len() - 1], 0);
|
||||
unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(self.inner.as_slice()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl AsRef<std::path::Path> for SharedString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &std::path::Path {
|
||||
self.as_str().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl AsRef<std::ffi::OsStr> for SharedString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &std::ffi::OsStr {
|
||||
self.as_str().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for SharedString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.as_str().as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<T> for SharedString
|
||||
where
|
||||
T: ?Sized + AsRef<str>,
|
||||
{
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.as_str() == other.as_ref()
|
||||
}
|
||||
}
|
||||
impl Eq for SharedString {}
|
||||
|
||||
impl<T> PartialOrd<T> for SharedString
|
||||
where
|
||||
T: ?Sized + AsRef<str>,
|
||||
{
|
||||
fn partial_cmp(&self, other: &T) -> Option<core::cmp::Ordering> {
|
||||
PartialOrd::partial_cmp(self.as_str(), other.as_ref())
|
||||
}
|
||||
}
|
||||
impl Ord for SharedString {
|
||||
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
||||
Ord::cmp(self.as_str(), other.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SharedString {
|
||||
fn from(s: String) -> Self {
|
||||
s.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for SharedString {
|
||||
fn from(s: &String) -> Self {
|
||||
s.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for SharedString {
|
||||
fn from(c: char) -> Self {
|
||||
SharedString::from(c.encode_utf8(&mut [0; 6]) as &str)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for String {
|
||||
fn from(s: SharedString) -> String {
|
||||
s.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SharedString> for String {
|
||||
fn from(s: &SharedString) -> String {
|
||||
s.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::AddAssign<&str> for SharedString {
|
||||
fn add_assign(&mut self, other: &str) {
|
||||
self.push_str(other);
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Add<&str> for SharedString {
|
||||
type Output = SharedString;
|
||||
fn add(mut self, other: &str) -> SharedString {
|
||||
self.push_str(other);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for SharedString {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.as_str().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for SharedString {
|
||||
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||
self.push_str(s);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::borrow::Borrow<str> for SharedString {
|
||||
fn borrow(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<char> for SharedString {
|
||||
fn extend<X: IntoIterator<Item = char>>(&mut self, iter: X) {
|
||||
let iter = iter.into_iter();
|
||||
self.inner.reserve(iter.size_hint().0);
|
||||
let mut buf = [0; 4];
|
||||
for ch in iter {
|
||||
let utf8 = ch.encode_utf8(&mut buf);
|
||||
self.push_str(utf8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<char> for SharedString {
|
||||
fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
|
||||
let mut str = Self::new();
|
||||
str.extend(iter);
|
||||
str
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [`std::fmt::format()`], but return a [`SharedString`] instead
|
||||
pub fn format(args: core::fmt::Arguments<'_>) -> SharedString {
|
||||
// unfortunately, the estimated_capacity is unstable
|
||||
//let capacity = args.estimated_capacity();
|
||||
let mut output = SharedString::default();
|
||||
output.write_fmt(args).unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
/// A trait for converting a value to a [`SharedString`].
|
||||
///
|
||||
/// This trait is automatically implemented for any type which implements the [`Display`] trait as long as the trait is in scope.
|
||||
/// As such, `ToSharedString` shouldn’t be implemented directly: [`Display`] should be implemented instead, and you get the `ToSharedString` implementation for free.
|
||||
pub trait ToSharedString {
|
||||
/// Converts the given value to a [`SharedString`].
|
||||
fn to_shared_string(&self) -> SharedString;
|
||||
}
|
||||
|
||||
impl<T> ToSharedString for T
|
||||
where
|
||||
T: Display + ?Sized,
|
||||
{
|
||||
fn to_shared_string(&self) -> SharedString {
|
||||
format!("{}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a f62 to a SharedString
|
||||
pub fn shared_string_from_number(n: f64) -> SharedString {
|
||||
// Number from which the increment of f32 is 1, so that we print enough precision to be able to represent all integers
|
||||
if n < 16777216. { crate::format!("{}", n as f32) } else { crate::format!("{}", n) }
|
||||
}
|
||||
|
||||
/// Convert a f64 to a SharedString with a fixed number of digits after the decimal point
|
||||
pub fn shared_string_from_number_fixed(n: f64, digits: usize) -> SharedString {
|
||||
crate::format!("{number:.digits$}", number = n, digits = digits)
|
||||
}
|
||||
|
||||
/// Convert a f64 to a SharedString following a similar logic as JavaScript's Number.toPrecision()
|
||||
pub fn shared_string_from_number_precision(n: f64, precision: usize) -> SharedString {
|
||||
let exponent = f64::log10(n.abs()).floor() as isize;
|
||||
if precision == 0 {
|
||||
shared_string_from_number(n)
|
||||
} else if exponent < -6 || (exponent >= 0 && exponent as usize >= precision) {
|
||||
crate::format!(
|
||||
"{number:.digits$e}",
|
||||
number = n,
|
||||
digits = precision.saturating_add_signed(-1)
|
||||
)
|
||||
} else {
|
||||
shared_string_from_number_fixed(n, precision.saturating_add_signed(-(exponent + 1)))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
use std::string::ToString;
|
||||
let x = SharedString::from("hello world!");
|
||||
assert_eq!(x, "hello world!");
|
||||
assert_ne!(x, "hello world?");
|
||||
assert_eq!(x, x.clone());
|
||||
assert_eq!("hello world!", x.as_str());
|
||||
let string = String::from("hello world!");
|
||||
assert_eq!(x, string);
|
||||
assert_eq!(x.to_string(), string);
|
||||
let def = SharedString::default();
|
||||
assert_eq!(def, SharedString::default());
|
||||
assert_eq!(def, SharedString::new());
|
||||
assert_ne!(def, x);
|
||||
assert_eq!(
|
||||
(&x as &dyn AsRef<std::ffi::CStr>).as_ref(),
|
||||
&*std::ffi::CString::new("hello world!").unwrap()
|
||||
);
|
||||
assert_eq!(SharedString::from('h'), "h");
|
||||
assert_eq!(SharedString::from('😎'), "😎");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn threading() {
|
||||
let shared_cst = SharedString::from("Hello there!");
|
||||
let shared_mtx = std::sync::Arc::new(std::sync::Mutex::new(SharedString::from("Shared:")));
|
||||
let mut handles = std::vec::Vec::new();
|
||||
for _ in 0..20 {
|
||||
let cst = shared_cst.clone();
|
||||
let mtx = shared_mtx.clone();
|
||||
handles.push(std::thread::spawn(move || {
|
||||
assert_eq!(cst, "Hello there!");
|
||||
let mut cst2 = cst.clone();
|
||||
cst2.push_str(" ... or not?");
|
||||
assert_eq!(cst2, "Hello there! ... or not?");
|
||||
assert_eq!(cst.clone(), "Hello there!");
|
||||
|
||||
let shared = {
|
||||
let mut lock = mtx.lock().unwrap();
|
||||
assert!(lock.starts_with("Shared:"));
|
||||
lock.push_str("!");
|
||||
lock.clone()
|
||||
};
|
||||
assert!(shared.clone().starts_with("Shared:"));
|
||||
}));
|
||||
}
|
||||
for j in handles {
|
||||
j.join().unwrap();
|
||||
}
|
||||
assert_eq!(shared_cst.clone(), "Hello there!");
|
||||
assert_eq!(shared_mtx.lock().unwrap().as_str(), "Shared:!!!!!!!!!!!!!!!!!!!!");
|
||||
// 20x"!"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_shared_string() {
|
||||
let i = 5.1;
|
||||
let five = SharedString::from("5.1");
|
||||
|
||||
assert_eq!(five, i.to_shared_string());
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
pub(crate) mod ffi {
|
||||
use super::*;
|
||||
|
||||
/// for cbindgen.
|
||||
#[allow(non_camel_case_types)]
|
||||
type c_char = u8;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// Returns a nul-terminated pointer for this string.
|
||||
/// The returned value is owned by the string, and should not be used after any
|
||||
/// mutable function have been called on the string, and must not be freed.
|
||||
pub extern "C" fn slint_shared_string_bytes(ss: &SharedString) -> *const c_char {
|
||||
if ss.is_empty() { c"".as_ptr().cast() } else { ss.as_ptr() }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// Destroy the shared string
|
||||
pub unsafe extern "C" fn slint_shared_string_drop(ss: *const SharedString) {
|
||||
unsafe {
|
||||
core::ptr::read(ss);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// Increment the reference count of the string.
|
||||
/// The resulting structure must be passed to slint_shared_string_drop
|
||||
pub unsafe extern "C" fn slint_shared_string_clone(out: *mut SharedString, ss: &SharedString) {
|
||||
unsafe { core::ptr::write(out, ss.clone()) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// Safety: bytes must be a valid utf-8 string of size len without null inside.
|
||||
/// The resulting structure must be passed to slint_shared_string_drop
|
||||
pub unsafe extern "C" fn slint_shared_string_from_bytes(
|
||||
out: *mut SharedString,
|
||||
bytes: *const c_char,
|
||||
len: usize,
|
||||
) {
|
||||
unsafe {
|
||||
let str = core::str::from_utf8(core::slice::from_raw_parts(bytes, len)).unwrap();
|
||||
core::ptr::write(out, SharedString::from(str));
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a string from a number.
|
||||
/// The resulting structure must be passed to slint_shared_string_drop
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_shared_string_from_number(out: *mut SharedString, n: f64) {
|
||||
let str = shared_string_from_number(n);
|
||||
unsafe { core::ptr::write(out, str) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slint_shared_string_from_number() {
|
||||
unsafe {
|
||||
let mut s = core::mem::MaybeUninit::uninit();
|
||||
slint_shared_string_from_number(s.as_mut_ptr(), 45.);
|
||||
assert_eq!(s.assume_init(), "45");
|
||||
|
||||
let mut s = core::mem::MaybeUninit::uninit();
|
||||
slint_shared_string_from_number(s.as_mut_ptr(), 45.12);
|
||||
assert_eq!(s.assume_init(), "45.12");
|
||||
|
||||
let mut s = core::mem::MaybeUninit::uninit();
|
||||
slint_shared_string_from_number(s.as_mut_ptr(), -1325466.);
|
||||
assert_eq!(s.assume_init(), "-1325466");
|
||||
|
||||
let mut s = core::mem::MaybeUninit::uninit();
|
||||
slint_shared_string_from_number(s.as_mut_ptr(), 0.);
|
||||
assert_eq!(s.assume_init(), "0");
|
||||
|
||||
let mut s = core::mem::MaybeUninit::uninit();
|
||||
slint_shared_string_from_number(
|
||||
s.as_mut_ptr(),
|
||||
((1235.82756f32 * 1000f32).round() / 1000f32) as _,
|
||||
);
|
||||
assert_eq!(s.assume_init(), "1235.828");
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_shared_string_from_number_fixed(
|
||||
out: &mut SharedString,
|
||||
n: f64,
|
||||
digits: usize,
|
||||
) {
|
||||
*out = shared_string_from_number_fixed(n, digits);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slint_shared_string_from_number_fixed() {
|
||||
let mut s = SharedString::default();
|
||||
|
||||
let num = 12345.6789;
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, num, 0);
|
||||
assert_eq!(s.as_str(), "12346");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, num, 1);
|
||||
assert_eq!(s.as_str(), "12345.7");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, num, 6);
|
||||
assert_eq!(s.as_str(), "12345.678900");
|
||||
|
||||
let num = -12345.6789;
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, num, 0);
|
||||
assert_eq!(s.as_str(), "-12346");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, num, 1);
|
||||
assert_eq!(s.as_str(), "-12345.7");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, num, 6);
|
||||
assert_eq!(s.as_str(), "-12345.678900");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, 1.23E+20_f64, 2);
|
||||
assert_eq!(s.as_str(), "123000000000000000000.00");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, 1.23E-10_f64, 2);
|
||||
assert_eq!(s.as_str(), "0.00");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, 2.34, 1);
|
||||
assert_eq!(s.as_str(), "2.3");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, 2.35, 1);
|
||||
assert_eq!(s.as_str(), "2.4");
|
||||
|
||||
slint_shared_string_from_number_fixed(&mut s, 2.55, 1);
|
||||
assert_eq!(s.as_str(), "2.5");
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_shared_string_from_number_precision(
|
||||
out: &mut SharedString,
|
||||
n: f64,
|
||||
precision: usize,
|
||||
) {
|
||||
*out = shared_string_from_number_precision(n, precision);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slint_shared_string_from_number_precision() {
|
||||
let mut s = SharedString::default();
|
||||
|
||||
let num = 5.123456;
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 0);
|
||||
assert_eq!(s.as_str(), "5.123456");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 5);
|
||||
assert_eq!(s.as_str(), "5.1235");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 2);
|
||||
assert_eq!(s.as_str(), "5.1");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 1);
|
||||
assert_eq!(s.as_str(), "5");
|
||||
|
||||
let num = 0.000123;
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 0);
|
||||
assert_eq!(s.as_str(), "0.000123");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 5);
|
||||
assert_eq!(s.as_str(), "0.00012300");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 2);
|
||||
assert_eq!(s.as_str(), "0.00012");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 1);
|
||||
assert_eq!(s.as_str(), "0.0001");
|
||||
|
||||
let num = 1234.5;
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 1);
|
||||
assert_eq!(s.as_str(), "1e3");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 2);
|
||||
assert_eq!(s.as_str(), "1.2e3");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 6);
|
||||
assert_eq!(s.as_str(), "1234.50");
|
||||
|
||||
let num = -1234.5;
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 1);
|
||||
assert_eq!(s.as_str(), "-1e3");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 2);
|
||||
assert_eq!(s.as_str(), "-1.2e3");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 6);
|
||||
assert_eq!(s.as_str(), "-1234.50");
|
||||
|
||||
let num = 0.00000012345;
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 1);
|
||||
assert_eq!(s.as_str(), "1e-7");
|
||||
|
||||
slint_shared_string_from_number_precision(&mut s, num, 10);
|
||||
assert_eq!(s.as_str(), "1.234500000e-7");
|
||||
}
|
||||
|
||||
/// Append some bytes to an existing shared string
|
||||
///
|
||||
/// bytes must be a valid utf8 array of size `len`, without null bytes inside
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_shared_string_append(
|
||||
self_: &mut SharedString,
|
||||
bytes: *const c_char,
|
||||
len: usize,
|
||||
) {
|
||||
let str = core::str::from_utf8(unsafe { core::slice::from_raw_parts(bytes, len) }).unwrap();
|
||||
self_.push_str(str);
|
||||
}
|
||||
#[test]
|
||||
fn test_slint_shared_string_append() {
|
||||
let mut s = SharedString::default();
|
||||
let mut append = |x: &str| unsafe {
|
||||
slint_shared_string_append(&mut s, x.as_bytes().as_ptr(), x.len());
|
||||
};
|
||||
append("Hello");
|
||||
append(", ");
|
||||
append("world");
|
||||
append("");
|
||||
append("!");
|
||||
assert_eq!(s.as_str(), "Hello, world!");
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_shared_string_to_lowercase(
|
||||
out: &mut SharedString,
|
||||
ss: &SharedString,
|
||||
) {
|
||||
*out = SharedString::from(ss.to_lowercase());
|
||||
}
|
||||
#[test]
|
||||
fn test_slint_shared_string_to_lowercase() {
|
||||
let s = SharedString::from("Hello");
|
||||
let mut out = SharedString::default();
|
||||
|
||||
unsafe {
|
||||
slint_shared_string_to_lowercase(&mut out, &s);
|
||||
}
|
||||
assert_eq!(out.as_str(), "hello");
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_shared_string_to_uppercase(
|
||||
out: &mut SharedString,
|
||||
ss: &SharedString,
|
||||
) {
|
||||
*out = SharedString::from(ss.to_uppercase());
|
||||
}
|
||||
#[test]
|
||||
fn test_slint_shared_string_to_uppercase() {
|
||||
let s = SharedString::from("Hello");
|
||||
let mut out = SharedString::default();
|
||||
|
||||
unsafe {
|
||||
slint_shared_string_to_uppercase(&mut out, &s);
|
||||
}
|
||||
assert_eq!(out.as_str(), "HELLO");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn test_serialize_deserialize_sharedstring() {
|
||||
let v = SharedString::from("data");
|
||||
let serialized = serde_json::to_string(&v).unwrap();
|
||||
let deserialized: SharedString = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(v, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend_from_chars() {
|
||||
let mut s = SharedString::from("x");
|
||||
s.extend(core::iter::repeat_n('a', 4).chain(core::iter::once('🍌')));
|
||||
assert_eq!(s.as_str(), "xaaaa🍌");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collect_from_chars() {
|
||||
let s: SharedString = core::iter::repeat_n('a', 4).chain(core::iter::once('🍌')).collect();
|
||||
assert_eq!(s.as_str(), "aaaa🍌");
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
/// Styled text that has been parsed and seperated into paragraphs
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct StyledText {
|
||||
/// Paragraphs of styled text
|
||||
pub(crate) paragraphs: crate::SharedVector<i_slint_common::styled_text::StyledTextParagraph>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl StyledText {
|
||||
pub fn parse_interpolated<S: AsRef<[i_slint_common::styled_text::StyledTextParagraph]>>(
|
||||
format_string: &str,
|
||||
args: &[S],
|
||||
) -> Result<Self, i_slint_common::styled_text::StyledTextError<'static>> {
|
||||
Ok(Self {
|
||||
paragraphs: i_slint_common::styled_text::parse_interpolated(format_string, args)
|
||||
.collect::<Result<crate::SharedVector<_>, _>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[i_slint_common::styled_text::StyledTextParagraph]> for StyledText {
|
||||
fn as_ref(&self) -> &[i_slint_common::styled_text::StyledTextParagraph] {
|
||||
&self.paragraphs
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_raw_text(styled_text: &StyledText) -> alloc::borrow::Cow<'_, str> {
|
||||
match styled_text.paragraphs.as_slice() {
|
||||
[] => "".into(),
|
||||
[paragraph] => paragraph.text.as_str().into(),
|
||||
_ => {
|
||||
let mut result = alloc::string::String::new();
|
||||
for paragraph in styled_text.paragraphs.iter() {
|
||||
if !result.is_empty() {
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str(paragraph.text.as_str());
|
||||
}
|
||||
result.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings for cbindgen
|
||||
#[cfg(feature = "ffi")]
|
||||
pub mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// Create a new default styled text
|
||||
pub unsafe extern "C" fn slint_styled_text_new(out: *mut StyledText) {
|
||||
unsafe {
|
||||
core::ptr::write(out, Default::default());
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// Destroy the shared string
|
||||
pub unsafe extern "C" fn slint_styled_text_drop(text: *const StyledText) {
|
||||
unsafe {
|
||||
core::ptr::read(text);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// Returns true if \a a is equal to \a b; otherwise returns false.
|
||||
pub extern "C" fn slint_styled_text_eq(a: &StyledText, b: &StyledText) -> bool {
|
||||
a == b
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
/// Clone the styled text
|
||||
pub unsafe extern "C" fn slint_styled_text_clone(out: *mut StyledText, ss: &StyledText) {
|
||||
unsafe { core::ptr::write(out, ss.clone()) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_markdown<S: AsRef<[i_slint_common::styled_text::StyledTextParagraph]>>(
|
||||
_format_string: &str,
|
||||
_args: &[S],
|
||||
) -> StyledText {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
StyledText::parse_interpolated(_format_string, _args).unwrap()
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn string_to_styled_text(_string: alloc::string::String) -> StyledText {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
if _string.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
StyledText {
|
||||
paragraphs: [i_slint_common::styled_text::paragraph_from_plain_text(_string)].into(),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
Default::default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn string_to_styled_text() {
|
||||
assert_eq!(super::string_to_styled_text(Default::default()), StyledText::default());
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! Functions useful for testing
|
||||
#![warn(missing_docs)]
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use crate::api::LogicalPosition;
|
||||
use crate::input::key_codes::Key;
|
||||
use crate::platform::WindowEvent;
|
||||
|
||||
/// Slint animations do not use real time, but use a mocked time.
|
||||
/// Normally, the event loop update the time of the animation using
|
||||
/// real time, but in tests, it is more convenient to use the fake time.
|
||||
/// This function will add some milliseconds to the fake time
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_mock_elapsed_time(time_in_ms: u64) {
|
||||
let tick = crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| {
|
||||
let mut tick = driver.current_tick();
|
||||
tick += core::time::Duration::from_millis(time_in_ms);
|
||||
driver.update_animations(tick);
|
||||
tick
|
||||
});
|
||||
crate::timers::TimerList::maybe_activate_timers(tick);
|
||||
crate::properties::ChangeTracker::run_change_handlers();
|
||||
}
|
||||
|
||||
/// Return the current mocked time.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_get_mocked_time() -> u64 {
|
||||
crate::animations::CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick()).as_millis()
|
||||
}
|
||||
|
||||
/// Simulate a click on a position within the component and releasing after some time.
|
||||
/// The time until the release is hardcoded to 50ms
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_send_mouse_click(
|
||||
x: f32,
|
||||
y: f32,
|
||||
window_adapter: &crate::window::WindowAdapterRc,
|
||||
) {
|
||||
let position = LogicalPosition::new(x, y);
|
||||
let button = crate::items::PointerEventButton::Left;
|
||||
|
||||
window_adapter.window().dispatch_event(WindowEvent::PointerMoved { position });
|
||||
window_adapter.window().dispatch_event(WindowEvent::PointerPressed { position, button });
|
||||
slint_mock_elapsed_time(50);
|
||||
window_adapter.window().dispatch_event(WindowEvent::PointerReleased { position, button });
|
||||
}
|
||||
|
||||
/// Simulate a single key event with the given text (pressed or released).
|
||||
///
|
||||
/// Unlike [`slint_send_keyboard_char`], this dispatches a single [`WindowEvent`]
|
||||
/// with the complete text. This is important for multi-codepoint grapheme clusters
|
||||
/// (e.g. NFD-encoded `é` = `e` + `\u{0301}`).
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_send_keyboard_key_text(
|
||||
text: &crate::SharedString,
|
||||
pressed: bool,
|
||||
window_adapter: &crate::window::WindowAdapterRc,
|
||||
) {
|
||||
window_adapter.window().dispatch_event(if pressed {
|
||||
WindowEvent::KeyPressed { text: text.clone() }
|
||||
} else {
|
||||
WindowEvent::KeyReleased { text: text.clone() }
|
||||
})
|
||||
}
|
||||
|
||||
/// Simulate a character input event (pressed or released).
|
||||
///
|
||||
/// Each character in the string is dispatched as a separate [`WindowEvent`].
|
||||
/// This is useful for modifier keys where each special character code
|
||||
/// represents an independent key press.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_send_keyboard_char(
|
||||
string: &crate::SharedString,
|
||||
pressed: bool,
|
||||
window_adapter: &crate::window::WindowAdapterRc,
|
||||
) {
|
||||
for ch in string.chars() {
|
||||
slint_send_keyboard_key_text(&ch.into(), pressed, window_adapter);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulate a character input event.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn send_keyboard_string_sequence(
|
||||
sequence: &crate::SharedString,
|
||||
window_adapter: &crate::window::WindowAdapterRc,
|
||||
) {
|
||||
for ch in sequence.chars() {
|
||||
if ch.is_ascii_uppercase() {
|
||||
window_adapter
|
||||
.window()
|
||||
.dispatch_event(WindowEvent::KeyPressed { text: Key::Shift.into() });
|
||||
}
|
||||
|
||||
let text: crate::SharedString = ch.into();
|
||||
window_adapter.window().dispatch_event(WindowEvent::KeyPressed { text: text.clone() });
|
||||
window_adapter.window().dispatch_event(WindowEvent::KeyReleased { text });
|
||||
|
||||
if ch.is_ascii_uppercase() {
|
||||
window_adapter
|
||||
.window()
|
||||
.dispatch_event(WindowEvent::KeyReleased { text: Key::Shift.into() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// implementation details for debug_log()
|
||||
#[doc(hidden)]
|
||||
pub fn debug_log_impl(args: core::fmt::Arguments) {
|
||||
crate::context::GLOBAL_CONTEXT.with(|p| match p.get() {
|
||||
Some(ctx) => ctx.platform().debug_log(args),
|
||||
None => default_debug_log(args),
|
||||
});
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn default_debug_log(_arguments: core::fmt::Arguments) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
use wasm_bindgen::prelude::*;
|
||||
use std::string::ToString;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
pub fn log(s: &str);
|
||||
}
|
||||
|
||||
log(&_arguments.to_string());
|
||||
} else if #[cfg(feature = "std")] {
|
||||
use std::io::Write;
|
||||
// We were seeing intermittent, albeit very rare, crashes due to `eprintln` panicking
|
||||
// if the write to stderr fails. Since this is just for debug printing, it's safe
|
||||
// to silently drop this if we can't write (since it wouldn't be written to stderr
|
||||
// anyway)
|
||||
let _ = writeln!(std::io::stderr(), "{_arguments}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
/// This macro allows producing debug output that will appear on stderr in regular builds
|
||||
/// and in the console log for wasm builds.
|
||||
macro_rules! debug_log {
|
||||
($($t:tt)*) => ($crate::tests::debug_log_impl(format_args!($($t)*)))
|
||||
}
|
||||
+709
@@ -0,0 +1,709 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! module for basic text layout
|
||||
//!
|
||||
//! The basic algorithm for breaking text into multiple lines:
|
||||
//! 1. First we determine the boundaries for text shaping. As shaping happens based on a single font and we know that different fonts cater different
|
||||
//! writing systems, we split up the text into chunks that maximize our chances of finding a font that covers all glyphs in the chunk. This way for
|
||||
//! example arabic text can be covered by a font that has excellent arabic coverage while latin text is rendered using a different font.
|
||||
//! Shaping boundaries are always also grapheme boundaries.
|
||||
//! 2. Then we shape the text at shaping boundaries, to determine the metrics of glyphs and glyph clusters
|
||||
//! 3. Loop over all glyph clusters as well as the line break opportunities produced by the unicode line break algorithm:
|
||||
//! Sum up the width of all glyph clusters until the next line break opportunity (encapsulated in FragmentIterator), record separately the width of
|
||||
//! trailing space within the fragment.
|
||||
//! ```text
|
||||
//! If the width of the current line (including trailing whitespace) and the new fragment of glyph clusters (without trailing whitespace) is less or
|
||||
//! equal to the available width:
|
||||
//! Add fragment of glyph clusters to the current line
|
||||
//! Else:
|
||||
//! Emit current line as new line
|
||||
//! If encountering a mandatory line break opportunity:
|
||||
//! Emit current line as new line
|
||||
//! ```
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use euclid::num::{One, Zero};
|
||||
|
||||
use crate::items::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap};
|
||||
|
||||
#[cfg(feature = "unicode-linebreak")]
|
||||
mod linebreak_unicode;
|
||||
#[cfg(feature = "unicode-linebreak")]
|
||||
use linebreak_unicode::{BreakOpportunity, LineBreakIterator};
|
||||
|
||||
#[cfg(not(feature = "unicode-linebreak"))]
|
||||
mod linebreak_simple;
|
||||
#[cfg(not(feature = "unicode-linebreak"))]
|
||||
use linebreak_simple::{BreakOpportunity, LineBreakIterator};
|
||||
|
||||
mod fragments;
|
||||
mod glyphclusters;
|
||||
mod shaping;
|
||||
#[cfg(feature = "shared-parley")]
|
||||
/// cbindgen:ignore
|
||||
pub mod sharedparley;
|
||||
use shaping::ShapeBuffer;
|
||||
pub use shaping::{AbstractFont, FontMetrics, Glyph, TextShaper};
|
||||
|
||||
mod linebreaker;
|
||||
pub use linebreaker::TextLine;
|
||||
|
||||
pub use linebreaker::TextLineBreaker;
|
||||
|
||||
pub struct TextLayout<'a, Font: AbstractFont> {
|
||||
pub font: &'a Font,
|
||||
pub letter_spacing: Option<<Font as TextShaper>::Length>,
|
||||
}
|
||||
|
||||
impl<Font: AbstractFont> TextLayout<'_, Font> {
|
||||
// Measures the size of the given text when rendered with the specified font and optionally constrained
|
||||
// by the provided `max_width`.
|
||||
// Returns a tuple of the width of the longest line as well as height of all lines.
|
||||
pub fn text_size(
|
||||
&self,
|
||||
text: &str,
|
||||
max_width: Option<Font::Length>,
|
||||
text_wrap: TextWrap,
|
||||
) -> (Font::Length, Font::Length)
|
||||
where
|
||||
Font::Length: core::fmt::Debug,
|
||||
{
|
||||
let mut max_line_width = Font::Length::zero();
|
||||
let mut line_count: i16 = 0;
|
||||
let shape_buffer = ShapeBuffer::new(self, text);
|
||||
|
||||
for line in TextLineBreaker::<Font>::new(text, &shape_buffer, max_width, None, text_wrap) {
|
||||
max_line_width = euclid::approxord::max(max_line_width, line.text_width);
|
||||
line_count += 1;
|
||||
}
|
||||
|
||||
(max_line_width, self.font.height() * line_count.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PositionedGlyph<Length> {
|
||||
pub x: Length,
|
||||
pub y: Length,
|
||||
pub advance: Length,
|
||||
pub glyph_id: core::num::NonZeroU16,
|
||||
pub text_byte_offset: usize,
|
||||
}
|
||||
|
||||
pub struct TextParagraphLayout<'a, Font: AbstractFont> {
|
||||
pub string: &'a str,
|
||||
pub layout: TextLayout<'a, Font>,
|
||||
pub max_width: Font::Length,
|
||||
pub max_height: Font::Length,
|
||||
pub horizontal_alignment: TextHorizontalAlignment,
|
||||
pub vertical_alignment: TextVerticalAlignment,
|
||||
pub wrap: TextWrap,
|
||||
pub overflow: TextOverflow,
|
||||
pub single_line: bool,
|
||||
}
|
||||
|
||||
impl<Font: AbstractFont> TextParagraphLayout<'_, Font> {
|
||||
/// Layout the given string in lines, and call the `layout_line` callback with the line to draw at position y.
|
||||
/// The signature of the `layout_line` function is: `(glyph_iterator, line_x, line_y, text_line, selection)`.
|
||||
/// Returns the baseline y coordinate as Ok, or the break value if `line_callback` returns `core::ops::ControlFlow::Break`.
|
||||
pub fn layout_lines<R>(
|
||||
&self,
|
||||
mut line_callback: impl FnMut(
|
||||
&mut dyn Iterator<Item = PositionedGlyph<Font::Length>>,
|
||||
Font::Length,
|
||||
Font::Length,
|
||||
&TextLine<Font::Length>,
|
||||
Option<core::ops::Range<Font::Length>>,
|
||||
) -> core::ops::ControlFlow<R>,
|
||||
selection: Option<core::ops::Range<usize>>,
|
||||
) -> Result<Font::Length, R> {
|
||||
let wrap = self.wrap != TextWrap::NoWrap;
|
||||
let elide = self.overflow == TextOverflow::Elide;
|
||||
let elide_glyph = if elide {
|
||||
self.layout.font.glyph_for_char('…').filter(|glyph| glyph.glyph_id.is_some())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let elide_width = elide_glyph.as_ref().map_or(Font::Length::zero(), |g| g.advance);
|
||||
let max_width_without_elision = self.max_width - elide_width;
|
||||
|
||||
let shape_buffer = ShapeBuffer::new(&self.layout, self.string);
|
||||
|
||||
let new_line_break_iter = || {
|
||||
TextLineBreaker::<Font>::new(
|
||||
self.string,
|
||||
&shape_buffer,
|
||||
if wrap { Some(self.max_width) } else { None },
|
||||
if elide { Some(self.layout.font.max_lines(self.max_height)) } else { None },
|
||||
self.wrap,
|
||||
)
|
||||
};
|
||||
let mut text_lines = None;
|
||||
|
||||
let mut text_height = || {
|
||||
if self.single_line {
|
||||
self.layout.font.height()
|
||||
} else {
|
||||
text_lines = Some(new_line_break_iter().collect::<Vec<_>>());
|
||||
self.layout.font.height() * (text_lines.as_ref().unwrap().len() as i16).into()
|
||||
}
|
||||
};
|
||||
|
||||
let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
|
||||
|
||||
let baseline_y = match self.vertical_alignment {
|
||||
TextVerticalAlignment::Top => Font::Length::zero(),
|
||||
TextVerticalAlignment::Center => self.max_height / two - text_height() / two,
|
||||
TextVerticalAlignment::Bottom => self.max_height - text_height(),
|
||||
};
|
||||
|
||||
let mut y = baseline_y;
|
||||
|
||||
let mut process_line = |line: &TextLine<Font::Length>, glyphs: &[Glyph<Font::Length>]| {
|
||||
let elide_long_line =
|
||||
elide && (self.single_line || !wrap) && line.text_width > self.max_width;
|
||||
let elide_last_line = elide
|
||||
&& line.glyph_range.end < glyphs.len()
|
||||
&& y + self.layout.font.height() * two > self.max_height;
|
||||
|
||||
let text_width = || {
|
||||
if elide_long_line || elide_last_line {
|
||||
let mut text_width = Font::Length::zero();
|
||||
for glyph in &glyphs[line.glyph_range.clone()] {
|
||||
if text_width + glyph.advance > max_width_without_elision {
|
||||
break;
|
||||
}
|
||||
text_width += glyph.advance;
|
||||
}
|
||||
return text_width + elide_width;
|
||||
}
|
||||
euclid::approxord::min(self.max_width, line.text_width)
|
||||
};
|
||||
|
||||
let x = match self.horizontal_alignment {
|
||||
TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
|
||||
Font::Length::zero()
|
||||
}
|
||||
TextHorizontalAlignment::Center => self.max_width / two - text_width() / two,
|
||||
TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
|
||||
self.max_width - text_width()
|
||||
}
|
||||
};
|
||||
|
||||
let mut elide_glyph = elide_glyph.as_ref();
|
||||
|
||||
let selection = selection
|
||||
.as_ref()
|
||||
.filter(|selection| {
|
||||
line.byte_range.start < selection.end && selection.start < line.byte_range.end
|
||||
})
|
||||
.map(|selection| {
|
||||
let mut begin = Font::Length::zero();
|
||||
let mut end = Font::Length::zero();
|
||||
for glyph in glyphs[line.glyph_range.clone()].iter() {
|
||||
if glyph.text_byte_offset < selection.start {
|
||||
begin += glyph.advance;
|
||||
}
|
||||
if glyph.text_byte_offset >= selection.end {
|
||||
break;
|
||||
}
|
||||
end += glyph.advance;
|
||||
}
|
||||
begin..end
|
||||
});
|
||||
|
||||
let glyph_it = glyphs[line.glyph_range.clone()].iter();
|
||||
let mut glyph_x = Font::Length::zero();
|
||||
let mut positioned_glyph_it = glyph_it.enumerate().filter_map(|(index, glyph)| {
|
||||
// TODO: cut off at grapheme boundaries
|
||||
if glyph_x > self.max_width {
|
||||
return None;
|
||||
}
|
||||
let elide_long_line = (elide_long_line || elide_last_line)
|
||||
&& x + glyph_x + glyph.advance > max_width_without_elision;
|
||||
let elide_last_line =
|
||||
elide_last_line && line.glyph_range.start + index == line.glyph_range.end - 1;
|
||||
if elide_long_line || elide_last_line {
|
||||
if let Some(elide_glyph) = elide_glyph.take() {
|
||||
let x = glyph_x;
|
||||
glyph_x += elide_glyph.advance;
|
||||
return Some(PositionedGlyph {
|
||||
x,
|
||||
y: Font::Length::zero(),
|
||||
advance: elide_glyph.advance,
|
||||
glyph_id: elide_glyph.glyph_id.unwrap(), // checked earlier when initializing elide_glyph
|
||||
text_byte_offset: glyph.text_byte_offset,
|
||||
});
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let x = glyph_x;
|
||||
glyph_x += glyph.advance;
|
||||
|
||||
glyph.glyph_id.map(|existing_glyph_id| PositionedGlyph {
|
||||
x,
|
||||
y: Font::Length::zero(),
|
||||
advance: glyph.advance,
|
||||
glyph_id: existing_glyph_id,
|
||||
text_byte_offset: glyph.text_byte_offset,
|
||||
})
|
||||
});
|
||||
|
||||
if let core::ops::ControlFlow::Break(break_val) =
|
||||
line_callback(&mut positioned_glyph_it, x, y, line, selection)
|
||||
{
|
||||
return core::ops::ControlFlow::Break(break_val);
|
||||
}
|
||||
y += self.layout.font.height();
|
||||
|
||||
core::ops::ControlFlow::Continue(())
|
||||
};
|
||||
|
||||
if let Some(lines_vec) = text_lines.take() {
|
||||
for line in lines_vec {
|
||||
if let core::ops::ControlFlow::Break(break_val) =
|
||||
process_line(&line, &shape_buffer.glyphs)
|
||||
{
|
||||
return Err(break_val);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for line in new_line_break_iter() {
|
||||
if let core::ops::ControlFlow::Break(break_val) =
|
||||
process_line(&line, &shape_buffer.glyphs)
|
||||
{
|
||||
return Err(break_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(baseline_y)
|
||||
}
|
||||
|
||||
/// Returns the leading edge of the glyph at the given byte offset
|
||||
pub fn cursor_pos_for_byte_offset(&self, byte_offset: usize) -> (Font::Length, Font::Length) {
|
||||
let mut last_glyph_right_edge = Font::Length::zero();
|
||||
let mut last_line_y = Font::Length::zero();
|
||||
|
||||
match self.layout_lines(
|
||||
|glyphs, line_x, line_y, line, _| {
|
||||
last_glyph_right_edge = euclid::approxord::min(
|
||||
self.max_width,
|
||||
line_x + line.width_including_trailing_whitespace(),
|
||||
);
|
||||
last_line_y = line_y;
|
||||
if byte_offset >= line.byte_range.end + line.trailing_whitespace_bytes {
|
||||
return core::ops::ControlFlow::Continue(());
|
||||
}
|
||||
|
||||
for positioned_glyph in glyphs {
|
||||
if positioned_glyph.text_byte_offset == byte_offset {
|
||||
return core::ops::ControlFlow::Break((
|
||||
euclid::approxord::min(self.max_width, line_x + positioned_glyph.x),
|
||||
last_line_y,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
core::ops::ControlFlow::Break((last_glyph_right_edge, last_line_y))
|
||||
},
|
||||
None,
|
||||
) {
|
||||
Ok(_) => (last_glyph_right_edge, last_line_y),
|
||||
Err(position) => position,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the bytes offset for the given position
|
||||
pub fn byte_offset_for_position(&self, (pos_x, pos_y): (Font::Length, Font::Length)) -> usize {
|
||||
let mut byte_offset = 0;
|
||||
let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
|
||||
|
||||
match self.layout_lines(
|
||||
|glyphs, line_x, line_y, line, _| {
|
||||
if pos_y >= line_y + self.layout.font.height() {
|
||||
byte_offset = line.byte_range.end;
|
||||
return core::ops::ControlFlow::Continue(());
|
||||
}
|
||||
|
||||
if line.is_empty() {
|
||||
return core::ops::ControlFlow::Break(line.byte_range.start);
|
||||
}
|
||||
|
||||
while let Some(positioned_glyph) = glyphs.next() {
|
||||
if pos_x >= line_x + positioned_glyph.x
|
||||
&& pos_x <= line_x + positioned_glyph.x + positioned_glyph.advance
|
||||
{
|
||||
if pos_x < line_x + positioned_glyph.x + positioned_glyph.advance / two {
|
||||
return core::ops::ControlFlow::Break(
|
||||
positioned_glyph.text_byte_offset,
|
||||
);
|
||||
} else if let Some(next_glyph) = glyphs.next() {
|
||||
return core::ops::ControlFlow::Break(next_glyph.text_byte_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core::ops::ControlFlow::Break(line.byte_range.end)
|
||||
},
|
||||
None,
|
||||
) {
|
||||
Ok(_) => byte_offset,
|
||||
Err(position) => position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_linebreak_opportunity_at_eot() {
|
||||
let mut it = LineBreakIterator::new("Hello World");
|
||||
assert_eq!(it.next(), Some((6, BreakOpportunity::Allowed)));
|
||||
assert_eq!(it.next(), None);
|
||||
}
|
||||
|
||||
// All glyphs are 10 pixels wide, break on ascii rules
|
||||
#[cfg(test)]
|
||||
pub struct FixedTestFont;
|
||||
|
||||
#[cfg(test)]
|
||||
impl TextShaper for FixedTestFont {
|
||||
type LengthPrimitive = f32;
|
||||
type Length = f32;
|
||||
fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
|
||||
&self,
|
||||
text: &str,
|
||||
glyphs: &mut GlyphStorage,
|
||||
) {
|
||||
let glyph_iter = text.char_indices().map(|(byte_offset, char)| {
|
||||
let mut utf16_buf = [0; 2];
|
||||
let utf16_char_as_glyph_id = char.encode_utf16(&mut utf16_buf)[0];
|
||||
|
||||
Glyph {
|
||||
offset_x: 0.,
|
||||
offset_y: 0.,
|
||||
glyph_id: core::num::NonZeroU16::new(utf16_char_as_glyph_id),
|
||||
advance: 10.,
|
||||
text_byte_offset: byte_offset,
|
||||
}
|
||||
});
|
||||
glyphs.extend(glyph_iter);
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, ch: char) -> Option<Glyph<f32>> {
|
||||
let mut utf16_buf = [0; 2];
|
||||
let utf16_char_as_glyph_id = ch.encode_utf16(&mut utf16_buf)[0];
|
||||
|
||||
Glyph {
|
||||
offset_x: 0.,
|
||||
offset_y: 0.,
|
||||
glyph_id: core::num::NonZeroU16::new(utf16_char_as_glyph_id),
|
||||
advance: 10.,
|
||||
text_byte_offset: 0,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn max_lines(&self, max_height: f32) -> usize {
|
||||
let height = self.ascent() - self.descent();
|
||||
(max_height / height).floor() as _
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl FontMetrics<f32> for FixedTestFont {
|
||||
fn ascent(&self) -> f32 {
|
||||
5.
|
||||
}
|
||||
|
||||
fn descent(&self) -> f32 {
|
||||
-5.
|
||||
}
|
||||
|
||||
fn x_height(&self) -> f32 {
|
||||
3.
|
||||
}
|
||||
|
||||
fn cap_height(&self) -> f32 {
|
||||
4.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_elision() {
|
||||
let font = FixedTestFont;
|
||||
let text = "This is a longer piece of text";
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let paragraph = TextParagraphLayout {
|
||||
string: text,
|
||||
layout: TextLayout { font: &font, letter_spacing: None },
|
||||
max_width: 13. * 10.,
|
||||
max_height: 10.,
|
||||
horizontal_alignment: TextHorizontalAlignment::Left,
|
||||
vertical_alignment: TextVerticalAlignment::Top,
|
||||
wrap: TextWrap::NoWrap,
|
||||
overflow: TextOverflow::Elide,
|
||||
single_line: true,
|
||||
};
|
||||
paragraph
|
||||
.layout_lines::<()>(
|
||||
|glyphs, _, _, _, _| {
|
||||
lines.push(
|
||||
glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
|
||||
);
|
||||
core::ops::ControlFlow::Continue(())
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lines.len(), 1);
|
||||
let rendered_text = lines[0]
|
||||
.iter()
|
||||
.flat_map(|glyph_id| {
|
||||
core::char::decode_utf16(core::iter::once(glyph_id.get()))
|
||||
.map(|r| r.unwrap())
|
||||
.collect::<Vec<char>>()
|
||||
})
|
||||
.collect::<std::string::String>();
|
||||
debug_assert_eq!(rendered_text, "This is a lo…")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_fit() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Fits";
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let paragraph = TextParagraphLayout {
|
||||
string: text,
|
||||
layout: TextLayout { font: &font, letter_spacing: None },
|
||||
max_width: 4. * 10.,
|
||||
max_height: 10.,
|
||||
horizontal_alignment: TextHorizontalAlignment::Left,
|
||||
vertical_alignment: TextVerticalAlignment::Top,
|
||||
wrap: TextWrap::NoWrap,
|
||||
overflow: TextOverflow::Elide,
|
||||
single_line: true,
|
||||
};
|
||||
paragraph
|
||||
.layout_lines::<()>(
|
||||
|glyphs, _, _, _, _| {
|
||||
lines.push(
|
||||
glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
|
||||
);
|
||||
core::ops::ControlFlow::Continue(())
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lines.len(), 1);
|
||||
let rendered_text = lines[0]
|
||||
.iter()
|
||||
.flat_map(|glyph_id| {
|
||||
core::char::decode_utf16(core::iter::once(glyph_id.get()))
|
||||
.map(|r| r.unwrap())
|
||||
.collect::<Vec<char>>()
|
||||
})
|
||||
.collect::<std::string::String>();
|
||||
debug_assert_eq!(rendered_text, "Fits")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_line_separators_characters_rendered() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello\nWorld\n";
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let paragraph = TextParagraphLayout {
|
||||
string: text,
|
||||
layout: TextLayout { font: &font, letter_spacing: None },
|
||||
max_width: 13. * 10.,
|
||||
max_height: 10.,
|
||||
horizontal_alignment: TextHorizontalAlignment::Left,
|
||||
vertical_alignment: TextVerticalAlignment::Top,
|
||||
wrap: TextWrap::NoWrap,
|
||||
overflow: TextOverflow::Clip,
|
||||
single_line: true,
|
||||
};
|
||||
paragraph
|
||||
.layout_lines::<()>(
|
||||
|glyphs, _, _, _, _| {
|
||||
lines.push(
|
||||
glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
|
||||
);
|
||||
core::ops::ControlFlow::Continue(())
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(lines.len(), 2);
|
||||
let rendered_text = lines
|
||||
.iter()
|
||||
.map(|glyphs_per_line| {
|
||||
glyphs_per_line
|
||||
.iter()
|
||||
.flat_map(|glyph_id| {
|
||||
core::char::decode_utf16(core::iter::once(glyph_id.get()))
|
||||
.map(|r| r.unwrap())
|
||||
.collect::<Vec<char>>()
|
||||
})
|
||||
.collect::<std::string::String>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
debug_assert_eq!(rendered_text, std::vec!["Hello", "World"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cursor_position() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello World";
|
||||
|
||||
let paragraph = TextParagraphLayout {
|
||||
string: text,
|
||||
layout: TextLayout { font: &font, letter_spacing: None },
|
||||
max_width: 10. * 10.,
|
||||
max_height: 10.,
|
||||
horizontal_alignment: TextHorizontalAlignment::Left,
|
||||
vertical_alignment: TextVerticalAlignment::Top,
|
||||
wrap: TextWrap::WordWrap,
|
||||
overflow: TextOverflow::Clip,
|
||||
single_line: false,
|
||||
};
|
||||
|
||||
assert_eq!(paragraph.cursor_pos_for_byte_offset(0), (0., 0.));
|
||||
|
||||
let e_offset = text
|
||||
.char_indices()
|
||||
.find_map(|(offset, ch)| if ch == 'e' { Some(offset) } else { None })
|
||||
.unwrap();
|
||||
assert_eq!(paragraph.cursor_pos_for_byte_offset(e_offset), (10., 0.));
|
||||
|
||||
let w_offset = text
|
||||
.char_indices()
|
||||
.find_map(|(offset, ch)| if ch == 'W' { Some(offset) } else { None })
|
||||
.unwrap();
|
||||
assert_eq!(paragraph.cursor_pos_for_byte_offset(w_offset + 1), (10., 10.));
|
||||
|
||||
assert_eq!(paragraph.cursor_pos_for_byte_offset(text.len()), (10. * 5., 10.));
|
||||
|
||||
let first_space_offset =
|
||||
text.char_indices().find_map(|(offset, ch)| ch.is_whitespace().then_some(offset)).unwrap();
|
||||
assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset), (5. * 10., 0.));
|
||||
assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset + 15), (10. * 10., 0.));
|
||||
assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset + 16), (10. * 10., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cursor_position_with_newline() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello\nWorld";
|
||||
|
||||
let paragraph = TextParagraphLayout {
|
||||
string: text,
|
||||
layout: TextLayout { font: &font, letter_spacing: None },
|
||||
max_width: 100. * 10.,
|
||||
max_height: 10.,
|
||||
horizontal_alignment: TextHorizontalAlignment::Left,
|
||||
vertical_alignment: TextVerticalAlignment::Top,
|
||||
wrap: TextWrap::WordWrap,
|
||||
overflow: TextOverflow::Clip,
|
||||
single_line: false,
|
||||
};
|
||||
|
||||
assert_eq!(paragraph.cursor_pos_for_byte_offset(5), (5. * 10., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_offset_for_empty_line() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello\n\nWorld";
|
||||
|
||||
let paragraph = TextParagraphLayout {
|
||||
string: text,
|
||||
layout: TextLayout { font: &font, letter_spacing: None },
|
||||
max_width: 100. * 10.,
|
||||
max_height: 10.,
|
||||
horizontal_alignment: TextHorizontalAlignment::Left,
|
||||
vertical_alignment: TextVerticalAlignment::Top,
|
||||
wrap: TextWrap::WordWrap,
|
||||
overflow: TextOverflow::Clip,
|
||||
single_line: false,
|
||||
};
|
||||
|
||||
assert_eq!(paragraph.byte_offset_for_position((0., 10.)), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_byte_offset() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello World";
|
||||
let mut end_helper_text = std::string::String::from(text);
|
||||
end_helper_text.push('!');
|
||||
|
||||
let paragraph = TextParagraphLayout {
|
||||
string: text,
|
||||
layout: TextLayout { font: &font, letter_spacing: None },
|
||||
max_width: 10. * 10.,
|
||||
max_height: 10.,
|
||||
horizontal_alignment: TextHorizontalAlignment::Left,
|
||||
vertical_alignment: TextVerticalAlignment::Top,
|
||||
wrap: TextWrap::WordWrap,
|
||||
overflow: TextOverflow::Clip,
|
||||
single_line: false,
|
||||
};
|
||||
|
||||
assert_eq!(paragraph.byte_offset_for_position((0., 0.)), 0);
|
||||
|
||||
let e_offset = text
|
||||
.char_indices()
|
||||
.find_map(|(offset, ch)| if ch == 'e' { Some(offset) } else { None })
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(paragraph.byte_offset_for_position((14., 0.)), e_offset);
|
||||
|
||||
let l_offset = text
|
||||
.char_indices()
|
||||
.find_map(|(offset, ch)| if ch == 'l' { Some(offset) } else { None })
|
||||
.unwrap();
|
||||
assert_eq!(paragraph.byte_offset_for_position((15., 0.)), l_offset);
|
||||
|
||||
let w_offset = text
|
||||
.char_indices()
|
||||
.find_map(|(offset, ch)| if ch == 'W' { Some(offset) } else { None })
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(paragraph.byte_offset_for_position((10., 10.)), w_offset + 1);
|
||||
|
||||
let o_offset = text
|
||||
.char_indices()
|
||||
.rev()
|
||||
.find_map(|(offset, ch)| if ch == 'o' { Some(offset) } else { None })
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(paragraph.byte_offset_for_position((15., 10.)), o_offset + 1);
|
||||
|
||||
let d_offset = text
|
||||
.char_indices()
|
||||
.rev()
|
||||
.find_map(|(offset, ch)| if ch == 'd' { Some(offset) } else { None })
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(paragraph.byte_offset_for_position((40., 10.)), d_offset);
|
||||
|
||||
let end_offset = end_helper_text
|
||||
.char_indices()
|
||||
.rev()
|
||||
.find_map(|(offset, ch)| if ch == '!' { Some(offset) } else { None })
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(paragraph.byte_offset_for_position((45., 10.)), end_offset);
|
||||
assert_eq!(paragraph.byte_offset_for_position((0., 20.)), end_offset);
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use core::ops::Range;
|
||||
|
||||
use euclid::num::Zero;
|
||||
|
||||
use super::glyphclusters::GlyphClusterIterator;
|
||||
use super::{BreakOpportunity, LineBreakIterator, ShapeBuffer};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Default)]
|
||||
pub struct TextFragment<Length> {
|
||||
pub byte_range: Range<usize>,
|
||||
pub glyph_range: Range<usize>,
|
||||
pub width: Length,
|
||||
pub trailing_whitespace_width: Length,
|
||||
pub trailing_whitespace_bytes: usize,
|
||||
pub trailing_mandatory_break: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TextFragmentIterator<'a, Length> {
|
||||
line_breaks: LineBreakIterator<'a>,
|
||||
glyph_clusters: GlyphClusterIterator<'a, Length>,
|
||||
text_len: usize,
|
||||
pub break_anywhere: bool,
|
||||
}
|
||||
|
||||
impl<'a, Length> TextFragmentIterator<'a, Length> {
|
||||
pub fn new(text: &'a str, shape_buffer: &'a ShapeBuffer<Length>) -> Self {
|
||||
Self {
|
||||
line_breaks: LineBreakIterator::new(text),
|
||||
glyph_clusters: GlyphClusterIterator::new(text, shape_buffer),
|
||||
text_len: text.len(),
|
||||
break_anywhere: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Length: Clone + Default + core::ops::AddAssign + Zero + Copy> Iterator
|
||||
for TextFragmentIterator<'_, Length>
|
||||
{
|
||||
type Item = TextFragment<Length>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let first_glyph_cluster = self.glyph_clusters.next()?;
|
||||
|
||||
let mut fragment = Self::Item::default();
|
||||
|
||||
let next_break_offset = if self.break_anywhere {
|
||||
if first_glyph_cluster.is_line_or_paragraph_separator {
|
||||
fragment.trailing_mandatory_break = true;
|
||||
}
|
||||
0
|
||||
} else if let Some((next_break_offset, break_type)) = self.line_breaks.next() {
|
||||
if matches!(break_type, BreakOpportunity::Mandatory) {
|
||||
fragment.trailing_mandatory_break = true;
|
||||
}
|
||||
next_break_offset
|
||||
} else {
|
||||
self.text_len
|
||||
};
|
||||
|
||||
if first_glyph_cluster.is_whitespace {
|
||||
fragment.trailing_whitespace_width = first_glyph_cluster.width;
|
||||
fragment.trailing_whitespace_bytes = first_glyph_cluster.byte_range.len();
|
||||
fragment.byte_range.start = first_glyph_cluster.byte_range.start;
|
||||
fragment.byte_range.end = first_glyph_cluster.byte_range.start;
|
||||
} else {
|
||||
fragment.width = first_glyph_cluster.width;
|
||||
fragment.byte_range = first_glyph_cluster.byte_range.clone();
|
||||
}
|
||||
|
||||
let start = first_glyph_cluster.glyph_range.start;
|
||||
let mut last_glyph_cluster = first_glyph_cluster;
|
||||
|
||||
while last_glyph_cluster.byte_range.end < next_break_offset {
|
||||
let next_glyph_cluster = match self.glyph_clusters.next() {
|
||||
Some(cluster) => cluster,
|
||||
None => break,
|
||||
};
|
||||
|
||||
if next_glyph_cluster.is_line_or_paragraph_separator {
|
||||
break;
|
||||
}
|
||||
|
||||
if next_glyph_cluster.is_whitespace {
|
||||
fragment.trailing_whitespace_width += next_glyph_cluster.width;
|
||||
fragment.trailing_whitespace_bytes += next_glyph_cluster.byte_range.len();
|
||||
} else {
|
||||
// transition from whitespace to characters by treating previous trailing whitespace
|
||||
// as regular characters
|
||||
if last_glyph_cluster.is_whitespace {
|
||||
fragment.width += core::mem::take(&mut fragment.trailing_whitespace_width);
|
||||
fragment.width += next_glyph_cluster.width;
|
||||
fragment.byte_range.end = next_glyph_cluster.byte_range.end;
|
||||
fragment.trailing_whitespace_bytes = 0;
|
||||
} else {
|
||||
fragment.width += next_glyph_cluster.width;
|
||||
fragment.byte_range.end = next_glyph_cluster.byte_range.end;
|
||||
}
|
||||
}
|
||||
|
||||
last_glyph_cluster = next_glyph_cluster.clone();
|
||||
}
|
||||
|
||||
fragment.glyph_range = Range { start, end: last_glyph_cluster.glyph_range.end };
|
||||
|
||||
// Make sure that adjacent fragments are advanced in their byte range:
|
||||
// this assertion should hold: fragment.byte_range.end + fragment.trailing_whitespace_bytes == next_fragment.byte_range.start
|
||||
// That means characters causing mandatory breaks need to be included.
|
||||
if fragment.trailing_mandatory_break && !self.break_anywhere {
|
||||
fragment.trailing_whitespace_bytes = next_break_offset - fragment.byte_range.end;
|
||||
}
|
||||
|
||||
Some(fragment)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use super::{FixedTestFont, TextLayout};
|
||||
#[cfg(test)]
|
||||
use std::{vec, vec::Vec};
|
||||
|
||||
#[test]
|
||||
fn fragment_iterator_simple() {
|
||||
let font = FixedTestFont;
|
||||
let text = "H WX";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
|
||||
let expected = vec![
|
||||
TextFragment {
|
||||
byte_range: Range { start: 0, end: 1 },
|
||||
glyph_range: Range { start: 0, end: 2 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 10.,
|
||||
trailing_mandatory_break: false,
|
||||
trailing_whitespace_bytes: 1,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 2, end: text.len() },
|
||||
glyph_range: Range { start: 2, end: text.len() },
|
||||
width: 20.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_mandatory_break: false,
|
||||
trailing_whitespace_bytes: 0,
|
||||
},
|
||||
];
|
||||
assert_eq!(fragments, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_iterator_simple_v2() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello World";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
|
||||
let expected = vec![
|
||||
TextFragment {
|
||||
byte_range: Range { start: 0, end: 5 },
|
||||
glyph_range: Range { start: 0, end: 6 },
|
||||
width: 50.,
|
||||
trailing_whitespace_width: 10.,
|
||||
trailing_mandatory_break: false,
|
||||
trailing_whitespace_bytes: 1,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 6, end: text.len() },
|
||||
glyph_range: Range { start: 6, end: text.len() },
|
||||
width: 10. * (text.len() - 6) as f32,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 0,
|
||||
trailing_mandatory_break: false,
|
||||
},
|
||||
];
|
||||
assert_eq!(fragments, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_iterator_forced_break() {
|
||||
let font = FixedTestFont;
|
||||
let text = "H\nW";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
fragments,
|
||||
vec![
|
||||
TextFragment {
|
||||
byte_range: Range { start: 0, end: 1 },
|
||||
glyph_range: Range { start: 0, end: 1 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 1,
|
||||
trailing_mandatory_break: true,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 2, end: 3 },
|
||||
glyph_range: Range { start: 2, end: 3 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 0,
|
||||
trailing_mandatory_break: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_iterator_forced_break_multi() {
|
||||
let font = FixedTestFont;
|
||||
let text = "H\n\n\nW";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
fragments,
|
||||
vec![
|
||||
TextFragment {
|
||||
byte_range: Range { start: 0, end: 1 },
|
||||
glyph_range: Range { start: 0, end: 1 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 1,
|
||||
trailing_mandatory_break: true,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 2, end: 2 },
|
||||
glyph_range: Range { start: 2, end: 3 },
|
||||
width: 0.,
|
||||
trailing_whitespace_width: 10.,
|
||||
trailing_whitespace_bytes: 1,
|
||||
trailing_mandatory_break: true,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 3, end: 3 },
|
||||
glyph_range: Range { start: 3, end: 4 },
|
||||
width: 0.,
|
||||
trailing_whitespace_width: 10.,
|
||||
trailing_whitespace_bytes: 1,
|
||||
trailing_mandatory_break: true,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 4, end: 5 },
|
||||
glyph_range: Range { start: 4, end: 5 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 0,
|
||||
trailing_mandatory_break: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_iterator_nbsp() {
|
||||
let font = FixedTestFont;
|
||||
let text = "X H\u{00a0}W";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
fragments,
|
||||
vec![
|
||||
TextFragment {
|
||||
byte_range: Range { start: 0, end: 1 },
|
||||
glyph_range: Range { start: 0, end: 2 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 10.,
|
||||
trailing_whitespace_bytes: 1,
|
||||
trailing_mandatory_break: false,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 2, end: 6 },
|
||||
glyph_range: Range { start: 2, end: 5 },
|
||||
width: 30.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 0,
|
||||
trailing_mandatory_break: false,
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_iterator_break_anywhere() {
|
||||
let font = FixedTestFont;
|
||||
let text = "AB\nCD\nEF";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let mut fragments = TextFragmentIterator::new(text, &shape_buffer);
|
||||
assert_eq!(
|
||||
fragments.next(),
|
||||
Some(TextFragment {
|
||||
byte_range: Range { start: 0, end: 2 },
|
||||
glyph_range: Range { start: 0, end: 2 },
|
||||
width: 20.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 1,
|
||||
trailing_mandatory_break: true,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
fragments.next(),
|
||||
Some(TextFragment {
|
||||
byte_range: Range { start: 3, end: 5 },
|
||||
glyph_range: Range { start: 3, end: 5 },
|
||||
width: 20.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 1,
|
||||
trailing_mandatory_break: true,
|
||||
},)
|
||||
);
|
||||
fragments.break_anywhere = true;
|
||||
let last_two = fragments.by_ref().take(2).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
last_two,
|
||||
vec![
|
||||
TextFragment {
|
||||
byte_range: Range { start: 6, end: 7 },
|
||||
glyph_range: Range { start: 6, end: 7 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 0,
|
||||
trailing_mandatory_break: false,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 7, end: 8 },
|
||||
glyph_range: Range { start: 7, end: 8 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 0,
|
||||
trailing_mandatory_break: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_iterator_leading_nbsp() {
|
||||
let font = FixedTestFont;
|
||||
let text = "A\n\u{00a0}\u{00a0}AB";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let fragments = TextFragmentIterator::new(text, &shape_buffer).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
fragments,
|
||||
vec![
|
||||
TextFragment {
|
||||
byte_range: Range { start: 0, end: 1 },
|
||||
glyph_range: Range { start: 0, end: 1 },
|
||||
width: 10.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 1,
|
||||
trailing_mandatory_break: true,
|
||||
},
|
||||
TextFragment {
|
||||
byte_range: Range { start: 2, end: 8 },
|
||||
glyph_range: Range { start: 2, end: 6 },
|
||||
width: 40.,
|
||||
trailing_whitespace_width: 0.,
|
||||
trailing_whitespace_bytes: 0,
|
||||
trailing_mandatory_break: false,
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use core::{marker::PhantomData, ops::Range};
|
||||
|
||||
use euclid::num::Zero;
|
||||
|
||||
use super::ShapeBuffer;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GlyphCluster<Length: Clone> {
|
||||
pub byte_range: Range<usize>,
|
||||
pub glyph_range: Range<usize>,
|
||||
pub width: Length,
|
||||
pub is_whitespace: bool,
|
||||
pub is_line_or_paragraph_separator: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GlyphClusterIterator<'a, Length> {
|
||||
text: &'a str,
|
||||
shaped_text: &'a ShapeBuffer<Length>,
|
||||
current_run: usize,
|
||||
// absolute byte offset in the entire text
|
||||
byte_offset: usize,
|
||||
glyph_index: usize,
|
||||
marker: PhantomData<Length>,
|
||||
}
|
||||
|
||||
impl<'a, Length> GlyphClusterIterator<'a, Length> {
|
||||
pub fn new(text: &'a str, shaped_text: &'a ShapeBuffer<Length>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
shaped_text,
|
||||
current_run: 0,
|
||||
byte_offset: 0,
|
||||
glyph_index: 0,
|
||||
marker: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Length: Copy + Clone + Zero + core::ops::AddAssign> Iterator
|
||||
for GlyphClusterIterator<'_, Length>
|
||||
{
|
||||
type Item = GlyphCluster<Length>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_run >= self.shaped_text.text_runs.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_run =
|
||||
if self.byte_offset < self.shaped_text.text_runs[self.current_run].byte_range.end {
|
||||
&self.shaped_text.text_runs[self.current_run]
|
||||
} else {
|
||||
self.current_run += 1;
|
||||
self.shaped_text.text_runs.get(self.current_run)?
|
||||
};
|
||||
|
||||
let mut cluster_width: Length = Length::zero();
|
||||
|
||||
let cluster_start = self.glyph_index;
|
||||
|
||||
let mut cluster_byte_offset;
|
||||
loop {
|
||||
let glyph = &self.shaped_text.glyphs[self.glyph_index];
|
||||
// Rustybuzz uses a relative byte offset as cluster index
|
||||
cluster_byte_offset = current_run.byte_range.start + glyph.text_byte_offset;
|
||||
if cluster_byte_offset != self.byte_offset {
|
||||
break;
|
||||
}
|
||||
cluster_width += glyph.advance;
|
||||
|
||||
self.glyph_index += 1;
|
||||
|
||||
if self.glyph_index >= current_run.glyph_range.end {
|
||||
cluster_byte_offset = current_run.byte_range.end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let byte_range = self.byte_offset..cluster_byte_offset;
|
||||
let (is_whitespace, is_line_or_paragraph_separator) = self.text[self.byte_offset..]
|
||||
.chars()
|
||||
.next()
|
||||
.map(|ch| {
|
||||
let is_line_or_paragraph_separator =
|
||||
ch == '\n' || ch == '\u{2028}' || ch == '\u{2029}';
|
||||
(ch.is_whitespace(), is_line_or_paragraph_separator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.byte_offset = cluster_byte_offset;
|
||||
|
||||
Some(GlyphCluster {
|
||||
byte_range,
|
||||
glyph_range: Range { start: cluster_start, end: self.glyph_index },
|
||||
width: cluster_width,
|
||||
is_whitespace,
|
||||
is_line_or_paragraph_separator,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum BreakOpportunity {
|
||||
Allowed,
|
||||
Mandatory,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LineBreakIterator<'a> {
|
||||
it: core::str::CharIndices<'a>,
|
||||
}
|
||||
|
||||
impl<'a> LineBreakIterator<'a> {
|
||||
pub fn new(text: &'a str) -> Self {
|
||||
Self { it: text.char_indices() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for LineBreakIterator<'a> {
|
||||
type Item = (usize, BreakOpportunity);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
for (byte_offset, char) in self.it.by_ref() {
|
||||
let maybe_opportunity = match char {
|
||||
'\u{2028}' | '\u{2029}' => Some(BreakOpportunity::Mandatory), // unicode line- and paragraph separators
|
||||
'\n' => Some(BreakOpportunity::Mandatory), // ascii line break
|
||||
_ if char.is_ascii_whitespace() => Some(BreakOpportunity::Allowed),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(opportunity) = maybe_opportunity {
|
||||
return Some((byte_offset + 1, opportunity));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use core::marker::PhantomData;
|
||||
|
||||
pub use unicode_linebreak::BreakOpportunity;
|
||||
|
||||
use crate::SharedVector;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LineBreakIterator<'a> {
|
||||
breaks: SharedVector<(usize, unicode_linebreak::BreakOpportunity)>,
|
||||
pos: usize,
|
||||
phantom: PhantomData<&'a str>,
|
||||
}
|
||||
|
||||
impl LineBreakIterator<'_> {
|
||||
pub fn new(text: &str) -> Self {
|
||||
let iterator = unicode_linebreak::linebreaks(text).filter(|(offset, opportunity)| {
|
||||
// unicode-linebreaks emits a mandatory break at the end of the text. We're not interested
|
||||
// in that.
|
||||
*offset != text.len() || !matches!(opportunity, BreakOpportunity::Mandatory)
|
||||
});
|
||||
|
||||
Self { breaks: iterator.collect(), pos: 0, phantom: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for LineBreakIterator<'_> {
|
||||
type Item = (usize, unicode_linebreak::BreakOpportunity);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.pos < self.breaks.len() {
|
||||
let i = self.pos;
|
||||
self.pos += 1;
|
||||
Some(self.breaks[i])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use core::ops::Range;
|
||||
|
||||
use euclid::num::Zero;
|
||||
|
||||
use crate::items::TextWrap;
|
||||
|
||||
use super::fragments::{TextFragment, TextFragmentIterator};
|
||||
use super::{ShapeBuffer, TextShaper};
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct TextLine<Length: Default + Clone> {
|
||||
// The range excludes trailing whitespace
|
||||
pub byte_range: Range<usize>,
|
||||
// number of bytes in text after byte_range occupied by trailing whitespace
|
||||
pub trailing_whitespace_bytes: usize,
|
||||
pub(crate) glyph_range: Range<usize>,
|
||||
trailing_whitespace: Length,
|
||||
pub(crate) text_width: Length, // with as occupied by the glyphs
|
||||
}
|
||||
|
||||
impl<
|
||||
Length: Default + Copy + Clone + Zero + core::ops::Add<Output = Length> + core::cmp::PartialOrd,
|
||||
> TextLine<Length>
|
||||
{
|
||||
pub fn line_text<'a>(&self, paragraph: &'a str) -> &'a str {
|
||||
¶graph[self.byte_range.clone()]
|
||||
}
|
||||
|
||||
pub fn width_including_trailing_whitespace(&self) -> Length {
|
||||
if self.text_width > Length::zero() {
|
||||
self.text_width + self.trailing_whitespace
|
||||
} else {
|
||||
Length::zero()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.byte_range.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Length: Clone + Copy + Default + core::ops::AddAssign> TextLine<Length> {
|
||||
pub fn add_fragment(&mut self, fragment: &TextFragment<Length>) {
|
||||
if self.byte_range.is_empty() {
|
||||
self.byte_range = fragment.byte_range.clone();
|
||||
} else if !fragment.byte_range.is_empty() {
|
||||
self.byte_range.end = fragment.byte_range.end;
|
||||
}
|
||||
if self.glyph_range.is_empty() {
|
||||
self.glyph_range = fragment.glyph_range.clone();
|
||||
} else {
|
||||
self.glyph_range.end = fragment.glyph_range.end;
|
||||
}
|
||||
if !fragment.byte_range.is_empty() {
|
||||
self.text_width += self.trailing_whitespace;
|
||||
self.trailing_whitespace = Length::default();
|
||||
self.trailing_whitespace_bytes = 0;
|
||||
}
|
||||
self.text_width += fragment.width;
|
||||
self.trailing_whitespace += fragment.trailing_whitespace_width;
|
||||
self.trailing_whitespace_bytes += fragment.trailing_whitespace_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextLineBreaker<'a, Font: TextShaper> {
|
||||
fragments: TextFragmentIterator<'a, Font::Length>,
|
||||
available_width: Option<Font::Length>,
|
||||
current_line: TextLine<Font::Length>,
|
||||
num_emitted_lines: usize,
|
||||
mandatory_line_break_on_next_iteration: bool,
|
||||
max_lines: Option<usize>,
|
||||
text_wrap: TextWrap,
|
||||
}
|
||||
|
||||
impl<'a, Font: TextShaper> TextLineBreaker<'a, Font> {
|
||||
pub fn new(
|
||||
text: &'a str,
|
||||
shape_buffer: &'a ShapeBuffer<Font::Length>,
|
||||
available_width: Option<Font::Length>,
|
||||
max_lines: Option<usize>,
|
||||
text_wrap: TextWrap,
|
||||
) -> Self {
|
||||
Self {
|
||||
fragments: TextFragmentIterator::new(text, shape_buffer),
|
||||
available_width,
|
||||
current_line: Default::default(),
|
||||
num_emitted_lines: 0,
|
||||
mandatory_line_break_on_next_iteration: false,
|
||||
max_lines,
|
||||
text_wrap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Font: TextShaper> Iterator for TextLineBreaker<'_, Font> {
|
||||
type Item = TextLine<Font::Length>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(max_lines) = self.max_lines
|
||||
&& self.num_emitted_lines >= max_lines
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if core::mem::take(&mut self.mandatory_line_break_on_next_iteration) {
|
||||
self.num_emitted_lines += 1;
|
||||
return Some(core::mem::take(&mut self.current_line));
|
||||
}
|
||||
|
||||
self.fragments.break_anywhere = false;
|
||||
|
||||
let mut next_line = loop {
|
||||
// Clone the fragment iterator so that we can roll back in case we must break down the first
|
||||
// word with `break_anywhere = true`.
|
||||
let mut fragments = self.fragments.clone();
|
||||
|
||||
let fragment = match fragments.next() {
|
||||
Some(fragment) => fragment,
|
||||
None => {
|
||||
break None;
|
||||
}
|
||||
};
|
||||
|
||||
// As trailing_mandatory_break is only set if break_anywhere is false, the fragment must
|
||||
// be first processed with break_anywhere = false and if no mandatory break is found, the
|
||||
// loop is re-run with break_anywhere = true when using CharWrap.
|
||||
if self.text_wrap == TextWrap::CharWrap
|
||||
&& !fragment.trailing_mandatory_break
|
||||
&& !self.fragments.break_anywhere
|
||||
{
|
||||
self.fragments.break_anywhere = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(available_width) = self.available_width
|
||||
&& self.current_line.width_including_trailing_whitespace() + fragment.width
|
||||
> available_width
|
||||
{
|
||||
if self.current_line.is_empty() {
|
||||
if !self.fragments.break_anywhere {
|
||||
// Try again but break anywhere this time. self.fragments is cloned at the beginning
|
||||
// of the loop.
|
||||
self.fragments.break_anywhere = true;
|
||||
continue;
|
||||
} else {
|
||||
// Even if we allow to break anywhere, there is still no room for the next fragment.
|
||||
// Just use it anywhere otherwise we would return many empty lines
|
||||
self.fragments = fragments;
|
||||
self.current_line.add_fragment(&fragment);
|
||||
break Some(core::mem::take(&mut self.current_line));
|
||||
}
|
||||
}
|
||||
|
||||
let next_line = core::mem::take(&mut self.current_line);
|
||||
self.mandatory_line_break_on_next_iteration = fragment.trailing_mandatory_break;
|
||||
|
||||
if self.text_wrap != TextWrap::CharWrap
|
||||
&& !fragments.break_anywhere
|
||||
&& fragment.width < available_width
|
||||
{
|
||||
self.current_line.add_fragment(&fragment);
|
||||
self.fragments = fragments;
|
||||
}
|
||||
|
||||
break Some(next_line);
|
||||
};
|
||||
|
||||
self.fragments = fragments;
|
||||
self.current_line.add_fragment(&fragment);
|
||||
|
||||
if fragment.trailing_mandatory_break {
|
||||
break Some(core::mem::take(&mut self.current_line));
|
||||
}
|
||||
};
|
||||
|
||||
// Emit at least one single line
|
||||
if next_line.is_none()
|
||||
&& (!self.current_line.byte_range.is_empty() || self.num_emitted_lines == 0)
|
||||
{
|
||||
next_line = Some(core::mem::take(&mut self.current_line));
|
||||
}
|
||||
|
||||
if next_line.is_some() {
|
||||
self.num_emitted_lines += 1;
|
||||
}
|
||||
|
||||
next_line
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use super::{FixedTestFont, TextLayout};
|
||||
|
||||
#[test]
|
||||
fn test_empty_line_break() {
|
||||
let font = FixedTestFont;
|
||||
let text = "";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(lines[0].line_text(text), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_char_wrap() {
|
||||
// The available width is half-way into the next word
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello World";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(80.),
|
||||
None,
|
||||
TextWrap::CharWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "Hello Wo");
|
||||
assert_eq!(lines[1].line_text(text), "rld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello World";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_max_lines() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello World";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
Some(1),
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_linebreak_trailing_space() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello ";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forced_break() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello\nWorld";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines =
|
||||
TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forced_break_multi() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello\n\n\nWorld";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines =
|
||||
TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 4);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "");
|
||||
assert_eq!(lines[2].line_text(text), "");
|
||||
assert_eq!(lines[3].line_text(text), "World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forced_break_multi_char_wrap() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello\n\n\nWorld";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(30.),
|
||||
None,
|
||||
TextWrap::CharWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 6);
|
||||
assert_eq!(lines[0].line_text(text), "Hel");
|
||||
assert_eq!(lines[1].line_text(text), "lo");
|
||||
assert_eq!(lines[2].line_text(text), "");
|
||||
assert_eq!(lines[3].line_text(text), "");
|
||||
assert_eq!(lines[4].line_text(text), "Wor");
|
||||
assert_eq!(lines[5].line_text(text), "ld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forced_break_max_lines() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello\n\n\nWorld";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
None,
|
||||
Some(2),
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nbsp_break() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Ok Hello\u{00a0}World";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(110.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "Ok");
|
||||
assert_eq!(lines[1].line_text(text), "Hello\u{00a0}World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_line_multi_break_opportunity() {
|
||||
let font = FixedTestFont;
|
||||
let text = "a b c";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines =
|
||||
TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(lines[0].line_text(text), "a b c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_anywhere_fallback() {
|
||||
let font = FixedTestFont;
|
||||
let text = "HelloWorld";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_anywhere_fallback_multi_line() {
|
||||
let font = FixedTestFont;
|
||||
let text = "HelloWorld\nHelloWorld";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 4);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "World");
|
||||
assert_eq!(lines[2].line_text(text), "Hello");
|
||||
assert_eq!(lines[3].line_text(text), "World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_anywhere_fallback_multi_line_char_wrap() {
|
||||
let font = FixedTestFont;
|
||||
let text = "HelloWorld\nHelloWorld";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
None,
|
||||
TextWrap::CharWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 4);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "World");
|
||||
assert_eq!(lines[2].line_text(text), "Hello");
|
||||
assert_eq!(lines[3].line_text(text), "World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_anywhere_fallback_multi_line_v2() {
|
||||
let font = FixedTestFont;
|
||||
let text = "HelloW orldHellow";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 4);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "W");
|
||||
assert_eq!(lines[2].line_text(text), "orldH");
|
||||
assert_eq!(lines[3].line_text(text), "ellow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_anywhere_fallback_max_lines() {
|
||||
let font = FixedTestFont;
|
||||
let text = "HelloW orldHellow";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(50.),
|
||||
Some(3),
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 3);
|
||||
assert_eq!(lines[0].line_text(text), "Hello");
|
||||
assert_eq!(lines[1].line_text(text), "W");
|
||||
assert_eq!(lines[2].line_text(text), "orldH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_space() {
|
||||
// The available width is half-way into the trailing "W"
|
||||
let font = FixedTestFont;
|
||||
let text = "H W";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(25.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "H");
|
||||
assert_eq!(lines[1].line_text(text), "W");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_space_char_wrap() {
|
||||
// The available width is half-way into the trailing "W"
|
||||
let font = FixedTestFont;
|
||||
let text = "H W";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(25.),
|
||||
None,
|
||||
TextWrap::CharWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "H");
|
||||
assert_eq!(lines[1].line_text(text), "W");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_space_v2() {
|
||||
// The available width is half-way into the trailing "W"
|
||||
let font = FixedTestFont;
|
||||
let text = "B B W";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(45.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "B B");
|
||||
assert_eq!(lines[1].line_text(text), "W");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_space_v3() {
|
||||
// The available width is half-way into the trailing "W"
|
||||
let font = FixedTestFont;
|
||||
let text = "H W";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(15.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!(lines[0].line_text(text), "H");
|
||||
assert_eq!(lines[1].line_text(text), "W");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_line_break_space_v4() {
|
||||
// The available width is half-way into the trailing space
|
||||
let font = FixedTestFont;
|
||||
let text = "H W H ";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(65.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(lines[0].line_text(text), "H W H");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line_width_with_whitespace() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello World";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(200.),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(lines[0].text_width, text.len() as f32 * 10.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_width() {
|
||||
let font = FixedTestFont;
|
||||
let text = "He\nHe o";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(0.0001),
|
||||
None,
|
||||
TextWrap::WordWrap,
|
||||
)
|
||||
.map(|t| t.line_text(text))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines, ["H", "e", "", "H", "e", "o"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_width_char_wrap() {
|
||||
let font = FixedTestFont;
|
||||
let text = "He\nHe o";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(0.0001),
|
||||
None,
|
||||
TextWrap::CharWrap,
|
||||
)
|
||||
.map(|t| t.line_text(text))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines, ["H", "e", "", "H", "e", "o"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn char_wrap_sentences() {
|
||||
let font = FixedTestFont;
|
||||
let text = "Hello world\nHow are you?";
|
||||
let shape_buffer = ShapeBuffer::new(&TextLayout { font: &font, letter_spacing: None }, text);
|
||||
let lines = TextLineBreaker::<FixedTestFont>::new(
|
||||
text,
|
||||
&shape_buffer,
|
||||
Some(80.),
|
||||
None,
|
||||
TextWrap::CharWrap,
|
||||
)
|
||||
.map(|t| t.line_text(text))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(lines, ["Hello wo", "rld", "How are", "you?"]);
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use core::ops::Range;
|
||||
|
||||
use super::TextLayout;
|
||||
|
||||
/// This struct describes a glyph from shaping to rendering. This includes the relative shaping
|
||||
/// offsets, advance (in abstract lengths) and platform specific glyph data.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Glyph<Length> {
|
||||
pub advance: Length,
|
||||
pub offset_x: Length,
|
||||
pub offset_y: Length,
|
||||
/// Glyph IDs are font specific identifiers. In TrueType fonts zero indicates the missing glyph, which
|
||||
/// is mapped to an Option here.
|
||||
pub glyph_id: Option<core::num::NonZeroU16>,
|
||||
/// The byte offset back in the original (Rust) string to the character that
|
||||
/// "produced" this glyph. When one character produces multiple glyphs (for example
|
||||
/// decomposed ligature), then all glyphs have the same offset.
|
||||
pub text_byte_offset: usize,
|
||||
}
|
||||
|
||||
/// This trait defines the interface between the text layout and the platform specific
|
||||
/// mapping of text to glyphs. An implementation of the TextShaper trait must provide
|
||||
/// metric types (Length, LengthPrimitive), which is used for the line breaking calculation
|
||||
/// and glyph positioning, as well as an opaque platform specific glyph data type.
|
||||
///
|
||||
/// Functionality wise it provides the ability to convert a string into a set of glyphs,
|
||||
/// each of which has basic metric fields as well as an offset back into the original string.
|
||||
/// Typically this is implemented by using a general text shaper, which performs an M:N mapping
|
||||
/// from unicode characters to glyphs, via glyph substitutions and script specific rules. In addition
|
||||
/// the glyphs may be positioned for the required appearance (such as stacked diacritics).
|
||||
///
|
||||
/// Finally, for convenience the TextShaper also provides a single glyph_for_char function, for example
|
||||
/// used to lookup single glyphs (such as the elision character) as well as additional metrics
|
||||
/// used for text paragraph layout.
|
||||
pub trait TextShaper {
|
||||
type LengthPrimitive: core::ops::Mul
|
||||
+ core::ops::Div
|
||||
+ core::ops::Add<Output = Self::LengthPrimitive>
|
||||
+ core::ops::AddAssign
|
||||
+ euclid::num::Zero
|
||||
+ euclid::num::One
|
||||
+ core::convert::From<i16>
|
||||
+ Copy
|
||||
+ core::fmt::Debug;
|
||||
type Length: euclid::num::Zero
|
||||
+ core::ops::AddAssign
|
||||
+ core::ops::Add<Output = Self::Length>
|
||||
+ core::ops::Sub<Output = Self::Length>
|
||||
+ Default
|
||||
+ Clone
|
||||
+ Copy
|
||||
+ core::cmp::PartialOrd
|
||||
+ core::ops::Mul<Self::LengthPrimitive, Output = Self::Length>
|
||||
+ core::ops::Div<Self::LengthPrimitive, Output = Self::Length>
|
||||
+ core::fmt::Debug;
|
||||
// Shapes the given string and emits the result into the given glyphs buffer.
|
||||
fn shape_text<GlyphStorage: core::iter::Extend<Glyph<Self::Length>>>(
|
||||
&self,
|
||||
text: &str,
|
||||
glyphs: &mut GlyphStorage,
|
||||
);
|
||||
fn glyph_for_char(&self, ch: char) -> Option<Glyph<Self::Length>>;
|
||||
fn max_lines(&self, max_height: Self::Length) -> usize;
|
||||
}
|
||||
|
||||
pub trait FontMetrics<Length: Copy + core::ops::Sub<Output = Length>> {
|
||||
fn height(&self) -> Length {
|
||||
self.ascent() - self.descent()
|
||||
}
|
||||
fn ascent(&self) -> Length;
|
||||
fn descent(&self) -> Length;
|
||||
fn x_height(&self) -> Length;
|
||||
fn cap_height(&self) -> Length;
|
||||
}
|
||||
|
||||
pub trait AbstractFont: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
|
||||
|
||||
impl<T> AbstractFont for T where T: TextShaper + FontMetrics<<Self as TextShaper>::Length> {}
|
||||
|
||||
pub struct ShapeBoundaries<'a> {
|
||||
text: &'a str,
|
||||
#[cfg(feature = "unicode-script")]
|
||||
// TODO: We should do a better analysis to find boundaries for text shaping; including
|
||||
// boundaries when the bidi level changes or an explicit separator like
|
||||
// paragraph/lineseparator/space is encountered.
|
||||
chars: core::str::CharIndices<'a>,
|
||||
next_boundary_start: Option<usize>,
|
||||
#[cfg(feature = "unicode-script")]
|
||||
last_script: Option<unicode_script::Script>,
|
||||
}
|
||||
|
||||
impl<'a> ShapeBoundaries<'a> {
|
||||
pub fn new(text: &'a str) -> Self {
|
||||
let next_boundary_start = if !text.is_empty() { Some(0) } else { None };
|
||||
Self {
|
||||
text,
|
||||
#[cfg(feature = "unicode-script")]
|
||||
chars: text.char_indices(),
|
||||
next_boundary_start,
|
||||
#[cfg(feature = "unicode-script")]
|
||||
last_script: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ShapeBoundaries<'_> {
|
||||
type Item = usize;
|
||||
|
||||
#[cfg(feature = "unicode-script")]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.next_boundary_start?;
|
||||
|
||||
let (next_offset, script) = loop {
|
||||
match self.chars.next() {
|
||||
Some((byte_offset, ch)) => {
|
||||
use unicode_script::UnicodeScript;
|
||||
let next_script = ch.script();
|
||||
let previous_script = *self.last_script.get_or_insert(next_script);
|
||||
|
||||
if next_script == previous_script {
|
||||
continue;
|
||||
}
|
||||
if matches!(
|
||||
next_script,
|
||||
unicode_script::Script::Unknown
|
||||
| unicode_script::Script::Common
|
||||
| unicode_script::Script::Inherited,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break (Some(byte_offset), Some(next_script));
|
||||
}
|
||||
None => {
|
||||
break (None, None);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.last_script = script;
|
||||
self.next_boundary_start = next_offset;
|
||||
|
||||
Some(self.next_boundary_start.unwrap_or(self.text.len()))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unicode-script"))]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.next_boundary_start {
|
||||
Some(_) => {
|
||||
self.next_boundary_start = None;
|
||||
Some(self.text.len())
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextRun {
|
||||
pub byte_range: Range<usize>,
|
||||
pub glyph_range: Range<usize>,
|
||||
// TODO: direction, etc.
|
||||
}
|
||||
|
||||
pub struct ShapeBuffer<Length> {
|
||||
pub glyphs: Vec<Glyph<Length>>,
|
||||
pub text_runs: Vec<TextRun>,
|
||||
}
|
||||
|
||||
impl<Length> ShapeBuffer<Length> {
|
||||
pub fn new<Font>(layout: &TextLayout<Font>, text: &str) -> Self
|
||||
where
|
||||
Font: AbstractFont<Length = Length>,
|
||||
Length: Copy + core::ops::AddAssign,
|
||||
{
|
||||
let mut glyphs = Vec::new();
|
||||
let text_runs = ShapeBoundaries::new(text)
|
||||
.scan(0, |run_start, run_end| {
|
||||
let glyphs_start = glyphs.len();
|
||||
|
||||
layout.font.shape_text(&text[*run_start..run_end], &mut glyphs);
|
||||
|
||||
if let Some(letter_spacing) = layout.letter_spacing
|
||||
&& glyphs.len() > glyphs_start
|
||||
{
|
||||
let mut last_byte_offset = glyphs[glyphs_start].text_byte_offset;
|
||||
for index in glyphs_start + 1..glyphs.len() {
|
||||
let current_glyph_byte_offset = glyphs[index].text_byte_offset;
|
||||
if current_glyph_byte_offset != last_byte_offset {
|
||||
let previous_glyph = &mut glyphs[index - 1];
|
||||
previous_glyph.advance += letter_spacing;
|
||||
}
|
||||
last_byte_offset = current_glyph_byte_offset;
|
||||
}
|
||||
|
||||
glyphs.last_mut().unwrap().advance += letter_spacing;
|
||||
}
|
||||
|
||||
let run = TextRun {
|
||||
byte_range: Range { start: *run_start, end: run_end },
|
||||
glyph_range: Range { start: glyphs_start, end: glyphs.len() },
|
||||
};
|
||||
*run_start = run_end;
|
||||
|
||||
Some(run)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self { glyphs, text_runs }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shape_boundaries_simple() {
|
||||
{
|
||||
let simple_text = "Hello World";
|
||||
let mut itemizer = ShapeBoundaries::new(simple_text);
|
||||
assert_eq!(itemizer.next(), Some(simple_text.len()));
|
||||
assert_eq!(itemizer.next(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shape_boundaries_empty() {
|
||||
{
|
||||
let mut itemizer = ShapeBoundaries::new("");
|
||||
assert_eq!(itemizer.next(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
not(feature = "unicode-script"),
|
||||
ignore = "Not supported without the unicode-script feature"
|
||||
)]
|
||||
fn test_shape_boundaries_script_change() {
|
||||
{
|
||||
let text = "abc🍌🐒defதோசை.";
|
||||
let mut itemizer = ShapeBoundaries::new(text).scan(0, |start, end| {
|
||||
let str = &text[*start..end];
|
||||
*start = end;
|
||||
Some(str)
|
||||
});
|
||||
assert_eq!(itemizer.next(), Some("abc🍌🐒def"));
|
||||
assert_eq!(itemizer.next(), Some("தோசை."));
|
||||
assert_eq!(itemizer.next(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl TextShaper for &rustybuzz::Face<'_> {
|
||||
type LengthPrimitive = f32;
|
||||
type Length = f32;
|
||||
fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
|
||||
&self,
|
||||
text: &str,
|
||||
glyphs: &mut GlyphStorage,
|
||||
) {
|
||||
let mut buffer = rustybuzz::UnicodeBuffer::new();
|
||||
buffer.push_str(text);
|
||||
let glyph_buffer = rustybuzz::shape(self, &[], buffer);
|
||||
|
||||
let output_glyph_generator =
|
||||
glyph_buffer.glyph_infos().iter().zip(glyph_buffer.glyph_positions().iter()).map(
|
||||
|(info, position)| {
|
||||
let mut out_glyph = Glyph::default();
|
||||
out_glyph.glyph_id = core::num::NonZeroU16::new(info.glyph_id as u16);
|
||||
out_glyph.offset_x = position.x_offset as _;
|
||||
out_glyph.offset_y = position.y_offset as _;
|
||||
out_glyph.advance = position.x_advance as _;
|
||||
out_glyph.text_byte_offset = info.cluster as usize;
|
||||
out_glyph
|
||||
},
|
||||
);
|
||||
|
||||
// Cannot return impl Iterator, so extend argument instead
|
||||
glyphs.extend(output_glyph_generator);
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, _ch: char) -> Option<Glyph<f32>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn max_lines(&self, max_height: f32) -> usize {
|
||||
(max_height / self.height()).floor() as _
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl FontMetrics<f32> for &rustybuzz::Face<'_> {
|
||||
fn ascent(&self) -> f32 {
|
||||
self.ascender() as _
|
||||
}
|
||||
|
||||
fn descent(&self) -> f32 {
|
||||
self.descender() as _
|
||||
}
|
||||
|
||||
fn x_height(&self) -> f32 {
|
||||
rustybuzz::ttf_parser::Face::x_height(self).unwrap_or_default() as _
|
||||
}
|
||||
|
||||
fn cap_height(&self) -> f32 {
|
||||
rustybuzz::ttf_parser::Face::capital_height(self).unwrap_or_default() as _
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn with_default_font<R>(mut callback: impl FnMut(&rustybuzz::Face<'_>) -> R) -> R {
|
||||
let mut collection = fontique::Collection::default();
|
||||
let font_path: std::path::PathBuf =
|
||||
[env!("CARGO_MANIFEST_DIR"), "..", "common", "sharedfontique", "Inter-VariableFont.ttf"]
|
||||
.iter()
|
||||
.collect();
|
||||
let registered_fonts =
|
||||
collection.register_fonts(std::fs::read(&font_path).unwrap().into(), None);
|
||||
let mut cache = fontique::SourceCache::default();
|
||||
let mut query = collection.query(&mut cache);
|
||||
query.set_families(std::iter::once(fontique::QueryFamily::from(registered_fonts[0].0)));
|
||||
let mut font = None;
|
||||
query.matches_with(|query_font| {
|
||||
font = Some(query_font.clone());
|
||||
fontique::QueryStatus::Stop
|
||||
});
|
||||
let font = font.unwrap();
|
||||
let face =
|
||||
rustybuzz::Face::from_slice(font.blob.data(), font.index).expect("unable to parse font");
|
||||
callback(&face)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shaping() {
|
||||
use TextShaper;
|
||||
|
||||
with_default_font(|face| {
|
||||
{
|
||||
let mut shaped_glyphs = Vec::new();
|
||||
// two glyph clusters: ā́b
|
||||
face.shape_text("a\u{0304}\u{0301}b", &mut shaped_glyphs);
|
||||
|
||||
assert_eq!(shaped_glyphs.len(), 3);
|
||||
assert!(shaped_glyphs[0].glyph_id.is_some());
|
||||
assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
|
||||
|
||||
assert!(shaped_glyphs[1].glyph_id.is_some());
|
||||
assert_eq!(shaped_glyphs[1].text_byte_offset, 0);
|
||||
|
||||
assert!(shaped_glyphs[2].glyph_id.is_some());
|
||||
assert_eq!(shaped_glyphs[2].text_byte_offset, 5);
|
||||
}
|
||||
|
||||
{
|
||||
let mut shaped_glyphs = Vec::new();
|
||||
// two glyph clusters: ā́b
|
||||
face.shape_text("a b", &mut shaped_glyphs);
|
||||
|
||||
assert_eq!(shaped_glyphs.len(), 3);
|
||||
assert!(shaped_glyphs[0].glyph_id.is_some());
|
||||
assert_eq!(shaped_glyphs[0].text_byte_offset, 0);
|
||||
|
||||
assert_eq!(shaped_glyphs[1].text_byte_offset, 1);
|
||||
|
||||
assert!(shaped_glyphs[2].glyph_id.is_some());
|
||||
assert_eq!(shaped_glyphs[2].text_byte_offset, 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_letter_spacing() {
|
||||
use TextShaper;
|
||||
|
||||
with_default_font(|face| {
|
||||
// two glyph clusters: ā́b
|
||||
let text = "a\u{0304}\u{0301}b";
|
||||
let advances = {
|
||||
let mut shaped_glyphs = Vec::new();
|
||||
face.shape_text(text, &mut shaped_glyphs);
|
||||
|
||||
assert_eq!(shaped_glyphs.len(), 3);
|
||||
|
||||
shaped_glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let layout = TextLayout { font: &face, letter_spacing: Some(20.) };
|
||||
let buffer = ShapeBuffer::new(&layout, text);
|
||||
|
||||
assert_eq!(buffer.glyphs.len(), advances.len());
|
||||
|
||||
let mut expected_advances = advances;
|
||||
expected_advances[1] += layout.letter_spacing.unwrap();
|
||||
*expected_advances.last_mut().unwrap() += layout.letter_spacing.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer.glyphs.iter().map(|glyph| glyph.advance).collect::<Vec<_>>(),
|
||||
expected_advances
|
||||
);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+1209
File diff suppressed because it is too large.
Load diff
+558
@@ -0,0 +1,558 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
use crate::SharedString;
|
||||
use core::fmt::Display;
|
||||
pub use formatter::FormatArgs;
|
||||
|
||||
#[cfg(feature = "tr")]
|
||||
pub use tr::Translator;
|
||||
|
||||
mod formatter {
|
||||
use core::fmt::{Display, Formatter, Result};
|
||||
|
||||
pub trait FormatArgs {
|
||||
type Output<'a>: Display
|
||||
where
|
||||
Self: 'a;
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn from_index(&self, index: usize) -> Option<Self::Output<'_>>;
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn from_name(&self, _name: &str) -> Option<Self::Output<'_>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Display> FormatArgs for [T] {
|
||||
type Output<'a>
|
||||
= &'a T
|
||||
where
|
||||
T: 'a;
|
||||
fn from_index(&self, index: usize) -> Option<&T> {
|
||||
self.get(index)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, T: Display> FormatArgs for [T; N] {
|
||||
type Output<'a>
|
||||
= &'a T
|
||||
where
|
||||
T: 'a;
|
||||
fn from_index(&self, index: usize) -> Option<&T> {
|
||||
self.get(index)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format<'a>(
|
||||
format_str: &'a str,
|
||||
args: &'a (impl FormatArgs + ?Sized),
|
||||
) -> impl Display + 'a {
|
||||
FormatResult { format_str, args }
|
||||
}
|
||||
|
||||
struct FormatResult<'a, T: ?Sized> {
|
||||
format_str: &'a str,
|
||||
args: &'a T,
|
||||
}
|
||||
|
||||
impl<T: FormatArgs + ?Sized> Display for FormatResult<'_, T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
let mut arg_idx = 0;
|
||||
let mut pos = 0;
|
||||
while let Some(mut p) = self.format_str[pos..].find(['{', '}']) {
|
||||
if self.format_str.len() - pos < p + 1 {
|
||||
break;
|
||||
}
|
||||
p += pos;
|
||||
|
||||
// Skip escaped }
|
||||
if self.format_str.get(p..=p) == Some("}") {
|
||||
self.format_str[pos..=p].fmt(f)?;
|
||||
if self.format_str.get(p + 1..=p + 1) == Some("}") {
|
||||
pos = p + 2;
|
||||
} else {
|
||||
// FIXME! this is an error, it should be reported ('}' must be escaped)
|
||||
pos = p + 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip escaped {
|
||||
if self.format_str.get(p + 1..=p + 1) == Some("{") {
|
||||
self.format_str[pos..=p].fmt(f)?;
|
||||
pos = p + 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the argument
|
||||
let end = if let Some(end) = self.format_str[p..].find('}') {
|
||||
end + p
|
||||
} else {
|
||||
// FIXME! this is an error, it should be reported
|
||||
self.format_str[pos..=p].fmt(f)?;
|
||||
pos = p + 1;
|
||||
continue;
|
||||
};
|
||||
let argument = self.format_str[p + 1..end].trim();
|
||||
let pa = if p == end - 1 {
|
||||
arg_idx += 1;
|
||||
self.args.from_index(arg_idx - 1)
|
||||
} else if let Ok(n) = argument.parse::<usize>() {
|
||||
self.args.from_index(n)
|
||||
} else {
|
||||
self.args.from_name(argument)
|
||||
};
|
||||
|
||||
// format the part before the '{'
|
||||
self.format_str[pos..p].fmt(f)?;
|
||||
if let Some(a) = pa {
|
||||
a.fmt(f)?;
|
||||
} else {
|
||||
// FIXME! this is an error, it should be reported
|
||||
self.format_str[p..=end].fmt(f)?;
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
self.format_str[pos..].fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::format;
|
||||
use core::fmt::Display;
|
||||
use std::string::{String, ToString};
|
||||
#[test]
|
||||
fn test_format() {
|
||||
assert_eq!(format("Hello", (&[]) as &[String]).to_string(), "Hello");
|
||||
assert_eq!(format("Hello {}!", &["world"]).to_string(), "Hello world!");
|
||||
assert_eq!(format("Hello {0}!", &["world"]).to_string(), "Hello world!");
|
||||
assert_eq!(
|
||||
format("Hello -{1}- -{0}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(),
|
||||
"Hello -World- -45-"
|
||||
);
|
||||
assert_eq!(
|
||||
format(
|
||||
format("Hello {{}}!", (&[]) as &[String]).to_string().as_str(),
|
||||
&[format("{}", &["world"])]
|
||||
)
|
||||
.to_string(),
|
||||
"Hello world!"
|
||||
);
|
||||
assert_eq!(
|
||||
format("Hello -{}- -{}-", &[&(40 + 5) as &dyn Display, &"World"]).to_string(),
|
||||
"Hello -45- -World-"
|
||||
);
|
||||
assert_eq!(format("Hello {{0}} {}", &["world"]).to_string(), "Hello {0} world");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WithPlural<'a, T: ?Sized>(&'a T, i32);
|
||||
|
||||
enum DisplayOrInt<T> {
|
||||
Display(T),
|
||||
Int(i32),
|
||||
}
|
||||
impl<T: Display> Display for DisplayOrInt<T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
DisplayOrInt::Display(d) => d.fmt(f),
|
||||
DisplayOrInt::Int(i) => i.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FormatArgs + ?Sized> FormatArgs for WithPlural<'_, T> {
|
||||
type Output<'b>
|
||||
= DisplayOrInt<T::Output<'b>>
|
||||
where
|
||||
Self: 'b;
|
||||
|
||||
fn from_index(&self, index: usize) -> Option<Self::Output<'_>> {
|
||||
self.0.from_index(index).map(DisplayOrInt::Display)
|
||||
}
|
||||
|
||||
fn from_name<'b>(&'b self, name: &str) -> Option<Self::Output<'b>> {
|
||||
if name == "n" {
|
||||
Some(DisplayOrInt::Int(self.1))
|
||||
} else {
|
||||
self.0.from_name(name).map(DisplayOrInt::Display)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Do the translation and formatting
|
||||
pub fn translate(
|
||||
original: &str,
|
||||
contextid: &str,
|
||||
domain: &str,
|
||||
arguments: &(impl FormatArgs + ?Sized),
|
||||
n: i32,
|
||||
plural: &str,
|
||||
) -> SharedString {
|
||||
#![allow(unused)]
|
||||
let mut output = SharedString::default();
|
||||
|
||||
// Register a dependency so that language changes trigger a re-evaluation of all relevant bindings
|
||||
// and this function is called again.
|
||||
#[cfg(any(feature = "tr", all(target_family = "unix", feature = "gettext-rs")))]
|
||||
global_translation_property();
|
||||
|
||||
let mut translated: Option<alloc::borrow::Cow<'_, str>> = None;
|
||||
|
||||
#[cfg(feature = "tr")]
|
||||
{
|
||||
translated = crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let ctx = ctx.get()?;
|
||||
let external_translator = ctx.external_translator()?;
|
||||
let context = if !contextid.is_empty() { Some(contextid) } else { None };
|
||||
Some(
|
||||
if plural.is_empty() {
|
||||
external_translator.translate(original, context)
|
||||
} else {
|
||||
external_translator.ntranslate(n.try_into().ok()?, original, plural, context)
|
||||
}
|
||||
.into_owned()
|
||||
.into(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(all(target_family = "unix", feature = "gettext-rs"))]
|
||||
if translated.is_none() {
|
||||
translated = Some(alloc::borrow::Cow::Owned(translate_gettext(
|
||||
original, contextid, domain, n, plural,
|
||||
)));
|
||||
}
|
||||
|
||||
let translated = translated
|
||||
.unwrap_or_else(|| if plural.is_empty() || n == 1 { original } else { plural }.into());
|
||||
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(&translated, &WithPlural(arguments, n))).unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(all(target_family = "unix", feature = "gettext-rs"))]
|
||||
fn translate_gettext(
|
||||
string: &str,
|
||||
ctx: &str,
|
||||
domain: &str,
|
||||
n: i32,
|
||||
plural: &str,
|
||||
) -> std::string::String {
|
||||
use std::string::String;
|
||||
fn mangle_context(ctx: &str, s: &str) -> String {
|
||||
std::format!("{ctx}\u{4}{s}")
|
||||
}
|
||||
fn demangle_context(r: String) -> String {
|
||||
if let Some(x) = r.split('\u{4}').next_back() {
|
||||
return x.into();
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
if plural.is_empty() {
|
||||
if !ctx.is_empty() {
|
||||
demangle_context(gettextrs::dgettext(domain, mangle_context(ctx, string)))
|
||||
} else {
|
||||
gettextrs::dgettext(domain, string)
|
||||
}
|
||||
} else if !ctx.is_empty() {
|
||||
demangle_context(gettextrs::dngettext(
|
||||
domain,
|
||||
mangle_context(ctx, string),
|
||||
mangle_context(ctx, plural),
|
||||
n as u32,
|
||||
))
|
||||
} else {
|
||||
gettextrs::dngettext(domain, string, plural, n as u32)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the language index and make sure to register a dependency
|
||||
fn global_translation_property() -> usize {
|
||||
crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let Some(ctx) = ctx.get() else { return 0 };
|
||||
ctx.0.translations_dirty.as_ref().get()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mark_all_translations_dirty() {
|
||||
#[cfg(all(feature = "gettext-rs", target_family = "unix"))]
|
||||
{
|
||||
// SAFETY: This trick from https://www.gnu.org/software/gettext/manual/html_node/gettext-grok.html
|
||||
// is merely incrementing a generational counter that will invalidate gettext's internal cache for translations.
|
||||
// If in the worst case it won't invalidate, then old translations are shown.
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
unsafe extern "C" {
|
||||
static mut _nl_msg_cat_cntr: std::ffi::c_int;
|
||||
}
|
||||
_nl_msg_cat_cntr += 1;
|
||||
}
|
||||
}
|
||||
|
||||
crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let Some(ctx) = ctx.get() else { return };
|
||||
ctx.0.translations_dirty.mark_dirty();
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "gettext-rs")]
|
||||
/// Initialize the translation by calling the [`bindtextdomain`](https://man7.org/linux/man-pages/man3/bindtextdomain.3.html) function from gettext
|
||||
pub fn gettext_bindtextdomain(_domain: &str, _dirname: std::path::PathBuf) -> std::io::Result<()> {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
gettextrs::bindtextdomain(_domain, _dirname)?;
|
||||
static START: std::sync::Once = std::sync::Once::new();
|
||||
START.call_once(|| {
|
||||
gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, "");
|
||||
});
|
||||
mark_all_translations_dirty();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn translate_from_bundle(
|
||||
strs: &[Option<&str>],
|
||||
arguments: &(impl FormatArgs + ?Sized),
|
||||
) -> SharedString {
|
||||
let idx = global_translation_property();
|
||||
let mut output = SharedString::default();
|
||||
let Some(translated) = strs.get(idx).and_then(|x| *x).or_else(|| strs.first().and_then(|x| *x))
|
||||
else {
|
||||
return output;
|
||||
};
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(translated, arguments)).unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
pub fn translate_from_bundle_with_plural(
|
||||
strs: &[Option<&[&str]>],
|
||||
plural_rules: &[Option<fn(i32) -> usize>],
|
||||
arguments: &(impl FormatArgs + ?Sized),
|
||||
n: i32,
|
||||
) -> SharedString {
|
||||
let idx = global_translation_property();
|
||||
let mut output = SharedString::default();
|
||||
let en = |n| (n != 1) as usize;
|
||||
let (translations, rule) = match strs.get(idx) {
|
||||
Some(Some(x)) => (x, plural_rules.get(idx).and_then(|x| *x).unwrap_or(en)),
|
||||
_ => match strs.first() {
|
||||
Some(Some(x)) => (x, plural_rules.first().and_then(|x| *x).unwrap_or(en)),
|
||||
_ => return output,
|
||||
},
|
||||
};
|
||||
let Some(translated) = translations.get(rule(n)).or_else(|| translations.first()).cloned()
|
||||
else {
|
||||
return output;
|
||||
};
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(translated, &WithPlural(arguments, n))).unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
/// This function is called by the generated code to assign the list of bundled languages.
|
||||
/// Do nothing if the list is already assigned.
|
||||
pub fn set_bundled_languages(languages: &[&'static str]) {
|
||||
crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let Some(ctx) = ctx.get() else { return };
|
||||
if ctx.0.translations_bundle_languages.borrow().is_none() {
|
||||
ctx.0.translations_bundle_languages.replace(Some(languages.to_vec()));
|
||||
#[cfg(feature = "std")]
|
||||
if let Some(idx) = index_for_locale(languages) {
|
||||
ctx.0.translations_dirty.as_ref().set(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// attempt to select the right bundled translation based on the current locale
|
||||
#[cfg(feature = "std")]
|
||||
fn index_for_locale(languages: &[&'static str]) -> Option<usize> {
|
||||
let locale = sys_locale::get_locale()?;
|
||||
// first, try an exact match
|
||||
let idx = languages.iter().position(|x| *x == locale);
|
||||
// else, only match the language part
|
||||
fn base(l: &str) -> &str {
|
||||
l.find(['-', '_', '@']).map_or(l, |i| &l[..i])
|
||||
}
|
||||
idx.or_else(|| {
|
||||
let locale = base(&locale);
|
||||
languages.iter().position(|x| base(x) == locale)
|
||||
})
|
||||
}
|
||||
|
||||
#[i_slint_core_macros::slint_doc]
|
||||
/// Select the current translation language when using bundled translations.
|
||||
///
|
||||
/// This function requires that the application's `.slint` file was compiled with bundled translations..
|
||||
/// It must be called after creating the first component.
|
||||
///
|
||||
/// The language string is the locale, which matches the name of the folder that contains the `LC_MESSAGES` folder.
|
||||
/// An empty string or `"en"` will select the default language.
|
||||
///
|
||||
/// Returns `Ok` if the language was selected; [`SelectBundledTranslationError`] otherwise.
|
||||
///
|
||||
/// See also the [Translation documentation](slint:translations).
|
||||
pub fn select_bundled_translation(language: &str) -> Result<(), SelectBundledTranslationError> {
|
||||
crate::context::GLOBAL_CONTEXT.with(|ctx| {
|
||||
let Some(ctx) = ctx.get() else {
|
||||
return Err(SelectBundledTranslationError::NoTranslationsBundled);
|
||||
};
|
||||
let languages = ctx.0.translations_bundle_languages.borrow();
|
||||
let Some(languages) = &*languages else {
|
||||
return Err(SelectBundledTranslationError::NoTranslationsBundled);
|
||||
};
|
||||
let idx = languages.iter().position(|x| *x == language);
|
||||
if let Some(idx) = idx {
|
||||
ctx.0.translations_dirty.as_ref().set(idx);
|
||||
Ok(())
|
||||
} else if language.is_empty() || language == "en" {
|
||||
ctx.0.translations_dirty.as_ref().set(0);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SelectBundledTranslationError::LanguageNotFound {
|
||||
available_languages: languages.iter().map(|x| (*x).into()).collect(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Error type returned from the [`select_bundled_translation`] function.
|
||||
#[derive(Debug)]
|
||||
pub enum SelectBundledTranslationError {
|
||||
/// The language was not found. The list of available languages is included in this error variant.
|
||||
LanguageNotFound { available_languages: crate::SharedVector<SharedString> },
|
||||
/// There are no bundled translations. Either [`select_bundled_translation`] was called before creating a component,
|
||||
/// or the application's `.slint` file was compiled without the bundle translation option.
|
||||
NoTranslationsBundled,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SelectBundledTranslationError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
SelectBundledTranslationError::LanguageNotFound { available_languages } => {
|
||||
write!(
|
||||
f,
|
||||
"The specified language was not found. Available languages are: {available_languages:?}"
|
||||
)
|
||||
}
|
||||
SelectBundledTranslationError::NoTranslationsBundled => {
|
||||
write!(
|
||||
f,
|
||||
"There are no bundled translations. Either select_bundled_translation was called before creating a component, or the application's `.slint` file was compiled without the bundle translation option"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for SelectBundledTranslationError {}
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
mod ffi {
|
||||
#![allow(unsafe_code)]
|
||||
use super::*;
|
||||
use crate::slice::Slice;
|
||||
|
||||
/// Perform the translation and formatting.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_translate(
|
||||
to_translate: &mut SharedString,
|
||||
context: &SharedString,
|
||||
domain: &SharedString,
|
||||
arguments: Slice<SharedString>,
|
||||
n: i32,
|
||||
plural: &SharedString,
|
||||
) {
|
||||
*to_translate =
|
||||
translate(to_translate.as_str(), context, domain, arguments.as_slice(), n, plural)
|
||||
}
|
||||
|
||||
/// Mark all translated string as dirty to perform re-translation in case the language change
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_translations_mark_dirty() {
|
||||
mark_all_translations_dirty();
|
||||
}
|
||||
|
||||
/// Safety: The slice must contain valid null-terminated utf-8 strings
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_translate_from_bundle(
|
||||
strs: Slice<*const core::ffi::c_char>,
|
||||
arguments: Slice<SharedString>,
|
||||
output: &mut SharedString,
|
||||
) {
|
||||
*output = SharedString::default();
|
||||
let idx = global_translation_property();
|
||||
let Some(translated) = strs
|
||||
.get(idx)
|
||||
.filter(|x| !x.is_null())
|
||||
.or_else(|| strs.first())
|
||||
.map(|x| unsafe { core::ffi::CStr::from_ptr(*x) }.to_str().unwrap())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(translated, arguments.as_slice())).unwrap();
|
||||
}
|
||||
/// strs is all the strings variant of all languages.
|
||||
/// indices is the array of indices such that for each language, the corresponding indice is one past the last index of the string for that language.
|
||||
/// So to get the string array for that language, one would do `strs[indices[lang-1]..indices[lang]]`
|
||||
/// (where indices[-1] is 0)
|
||||
///
|
||||
/// Safety; the strs must be pointer to valid null-terminated utf-8 strings
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn slint_translate_from_bundle_with_plural(
|
||||
strs: Slice<*const core::ffi::c_char>,
|
||||
indices: Slice<u32>,
|
||||
plural_rules: Slice<Option<fn(i32) -> usize>>,
|
||||
arguments: Slice<SharedString>,
|
||||
n: i32,
|
||||
output: &mut SharedString,
|
||||
) {
|
||||
*output = SharedString::default();
|
||||
let idx = global_translation_property();
|
||||
let en = |n| (n != 1) as usize;
|
||||
let begin = *indices.get(idx.wrapping_sub(1)).unwrap_or(&0);
|
||||
let (translations, rule) = match indices.get(idx) {
|
||||
Some(end) if *end != begin => (
|
||||
&strs.as_slice()[begin as usize..*end as usize],
|
||||
plural_rules.get(idx).and_then(|x| *x).unwrap_or(en),
|
||||
),
|
||||
_ => (
|
||||
&strs.as_slice()[..*indices.first().unwrap_or(&0) as usize],
|
||||
plural_rules.first().and_then(|x| *x).unwrap_or(en),
|
||||
),
|
||||
};
|
||||
let Some(translated) = translations
|
||||
.get(rule(n))
|
||||
.or_else(|| translations.first())
|
||||
.map(|x| unsafe { core::ffi::CStr::from_ptr(*x) }.to_str().unwrap())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
use core::fmt::Write;
|
||||
write!(output, "{}", formatter::format(translated, &WithPlural(arguments.as_slice(), n)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_translate_set_bundled_languages(languages: Slice<Slice<'static, u8>>) {
|
||||
let languages = languages
|
||||
.iter()
|
||||
.map(|x| core::str::from_utf8(x.as_slice()).unwrap())
|
||||
.collect::<alloc::vec::Vec<_>>();
|
||||
set_bundled_languages(&languages);
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn slint_translate_select_bundled_translation(language: Slice<u8>) -> bool {
|
||||
let language = core::str::from_utf8(&language).unwrap();
|
||||
select_bundled_translation(language).is_ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! Unsafe module that is only enabled when the `std` feature is off.
|
||||
//! It re-implements the thread_local macro with statics
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
#[macro_export]
|
||||
macro_rules! SLINT__thread_local_inner {
|
||||
($(#[$($meta:tt)*])* $vis:vis $ident:ident $ty:ty $block:block) => {
|
||||
$(#[$($meta)*])*
|
||||
$vis static $ident: $crate::unsafe_single_threaded::FakeThreadStorage<$ty> = {
|
||||
fn init() -> $ty $block
|
||||
$crate::unsafe_single_threaded::FakeThreadStorage::new(init)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! thread_local_ {
|
||||
// Taken from stdlib!
|
||||
|
||||
// empty (base case for the recursion)
|
||||
() => {};
|
||||
|
||||
($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = const $init:block; $($rest:tt)*) => (
|
||||
$crate::SLINT__thread_local_inner!($(#[$attr])* $vis $name $t $init);
|
||||
$crate::thread_local!($($rest)*);
|
||||
);
|
||||
|
||||
($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = const $init:block) => (
|
||||
$crate::SLINT__thread_local_inner!($(#[$attr])* $vis $name $t $init);
|
||||
);
|
||||
|
||||
// process multiple declarations
|
||||
($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr; $($rest:tt)*) => (
|
||||
$crate::SLINT__thread_local_inner!($(#[$attr])* $vis $name $t { $init });
|
||||
$crate::thread_local!($($rest)*);
|
||||
);
|
||||
|
||||
// handle a single declaration
|
||||
($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr) => (
|
||||
$crate::SLINT__thread_local_inner!($(#[$attr])* $vis $name $t { $init });
|
||||
);
|
||||
}
|
||||
|
||||
pub struct FakeThreadStorage<T, F = fn() -> T>(once_cell::unsync::OnceCell<T>, F);
|
||||
impl<T, F> FakeThreadStorage<T, F> {
|
||||
pub const fn new(f: F) -> Self {
|
||||
Self(once_cell::unsync::OnceCell::new(), f)
|
||||
}
|
||||
}
|
||||
impl<T> FakeThreadStorage<T> {
|
||||
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
|
||||
f(self.0.get_or_init(self.1))
|
||||
}
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn try_with<R>(&self, f: impl FnOnce(&T) -> R) -> Result<R, ()> {
|
||||
Ok(self.with(f))
|
||||
}
|
||||
}
|
||||
// Safety: the unsafe_single_threaded feature means we will only be called from a single thread
|
||||
unsafe impl<T, F> Send for FakeThreadStorage<T, F> {}
|
||||
unsafe impl<T, F> Sync for FakeThreadStorage<T, F> {}
|
||||
|
||||
pub use thread_local_ as thread_local;
|
||||
|
||||
pub struct OnceCell<T>(once_cell::unsync::OnceCell<T>);
|
||||
impl<T> Default for OnceCell<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
impl<T> OnceCell<T> {
|
||||
pub const fn new() -> Self {
|
||||
Self(once_cell::unsync::OnceCell::new())
|
||||
}
|
||||
pub fn get(&self) -> Option<&T> {
|
||||
self.0.get()
|
||||
}
|
||||
pub fn set(&self, value: T) -> Result<(), T> {
|
||||
self.0.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: the unsafe_single_threaded feature means we will only be called from a single thread
|
||||
unsafe impl<T> Send for OnceCell<T> {}
|
||||
unsafe impl<T> Sync for OnceCell<T> {}
|
||||
+2490
File diff suppressed because it is too large.
Load diff
+293
@@ -0,0 +1,293 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
//! Pupup window handling helpers
|
||||
|
||||
use crate::lengths::{LogicalPoint, LogicalRect, LogicalSize};
|
||||
|
||||
/// A collection of data that might influence the placement of a `Popup`.
|
||||
pub enum Placement {
|
||||
/// Request a fixed position
|
||||
Fixed(LogicalRect),
|
||||
}
|
||||
|
||||
/// Find a placement for the `Popup`, using the provided `Placement`.
|
||||
/// When a `clip_region` is provided, then the `Popup` will stay within those bounds.
|
||||
/// The `clip_region` typically is the window or the screen the window is on.
|
||||
pub fn place_popup(placement: Placement, clip_region: &Option<LogicalRect>) -> LogicalRect {
|
||||
match placement {
|
||||
Placement::Fixed(rect) => {
|
||||
let clip = clip_region.unwrap_or(rect);
|
||||
if clip.contains_rect(&rect) {
|
||||
rect
|
||||
} else {
|
||||
let size = LogicalSize::new(
|
||||
crate::Coord::min(rect.size.width, clip.size.width),
|
||||
crate::Coord::min(rect.size.height, clip.size.height),
|
||||
);
|
||||
let origin = LogicalPoint::new(
|
||||
rect.origin
|
||||
.x
|
||||
.clamp(clip.origin.x, clip.origin.x + clip.size.width - size.width),
|
||||
rect.origin
|
||||
.y
|
||||
.clamp(clip.origin.y, clip.origin.y + clip.size.height - size.height),
|
||||
);
|
||||
LogicalRect::new(origin, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn r(x: i32, y: i32, w: i32, h: i32) -> LogicalRect {
|
||||
LogicalRect::new(LogicalPoint::new(x as f32, y as f32), LogicalSize::new(w as f32, h as f32))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
fn fixed_placement(input: LogicalRect, expected: LogicalRect, clip: Option<LogicalRect>) {
|
||||
std::eprintln!("fixed: {input:?}, clip({clip:?}) => {expected:?}");
|
||||
let result = place_popup(Placement::Fixed(input), &clip);
|
||||
if let Some(clip) = clip {
|
||||
clip.contains_rect(&result);
|
||||
}
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_place_popup_fixed_unclipped() {
|
||||
let data = r(5, 5, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
|
||||
let data = r(5, -20, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
let data = r(2000, -20, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
let data = r(2000, 5, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
let data = r(2000, 2000, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
let data = r(5, 2000, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
let data = r(-20, 2000, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
let data = r(-20, 5, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
let data = r(-20, -20, 100, 100);
|
||||
fixed_placement(data, data, None);
|
||||
|
||||
let data = r(-20, -20, 2000, 2000);
|
||||
fixed_placement(data, data, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_place_popup_fixed_clipped() {
|
||||
for (clip_offset_x, clip_offset_y) in [
|
||||
(-200, -200),
|
||||
(-200, 0),
|
||||
(-200, 200),
|
||||
(0, -200),
|
||||
(0, 0),
|
||||
(0, 200),
|
||||
(200, -200),
|
||||
(200, 0),
|
||||
(200, 200),
|
||||
] {
|
||||
for (clip_width, clip_height) in [(110, 110), (800, 600)] {
|
||||
let clip = r(clip_offset_x, clip_offset_y, clip_width, clip_height);
|
||||
|
||||
let x_w = clip_offset_x - 10;
|
||||
let x_c = clip_offset_x + 5;
|
||||
let x_e = clip_offset_x + clip_width - 80;
|
||||
|
||||
let y_n = clip_offset_y - 10;
|
||||
let y_c = clip_offset_y + 5;
|
||||
let y_s = clip_offset_y + clip_height - 80;
|
||||
|
||||
let x_min = clip_offset_x;
|
||||
let x_max = clip_offset_x + clip_width;
|
||||
let y_min = clip_offset_y;
|
||||
let y_max = clip_offset_y + clip_height;
|
||||
|
||||
assert!(clip_width > 105 && clip_width < 1000);
|
||||
assert!(clip_height > 105 && clip_height < 1000);
|
||||
|
||||
// smaller, inside
|
||||
fixed_placement(r(x_c, y_c, 100, 100), r(x_c, y_c, 100, 100), Some(clip));
|
||||
|
||||
// smaller, partial outside
|
||||
fixed_placement(r(x_c, y_n, 100, 100), r(x_c, y_min, 100, 100), Some(clip));
|
||||
fixed_placement(r(x_e, y_n, 100, 100), r(x_max - 100, y_min, 100, 100), Some(clip));
|
||||
fixed_placement(r(x_e, y_c, 100, 100), r(x_max - 100, y_c, 100, 100), Some(clip));
|
||||
fixed_placement(
|
||||
r(x_e, y_s, 100, 100),
|
||||
r(x_max - 100, y_max - 100, 100, 100),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(r(x_c, y_s, 100, 100), r(x_c, y_max - 100, 100, 100), Some(clip));
|
||||
fixed_placement(r(x_c, y_s, 100, 100), r(x_c, y_max - 100, 100, 100), Some(clip));
|
||||
fixed_placement(r(x_w, y_s, 100, 100), r(x_min, y_max - 100, 100, 100), Some(clip));
|
||||
fixed_placement(r(x_w, y_c, 100, 100), r(x_min, y_c, 100, 100), Some(clip));
|
||||
fixed_placement(r(x_w, y_n, 100, 100), r(x_min, y_min, 100, 100), Some(clip));
|
||||
|
||||
// smaller, totally outside
|
||||
fixed_placement(r(x_c, -2000, 100, 100), r(x_c, y_min, 100, 100), Some(clip));
|
||||
fixed_placement(r(2000, -2000, 100, 100), r(x_max - 100, y_min, 100, 100), Some(clip));
|
||||
fixed_placement(r(2000, y_c, 100, 100), r(x_max - 100, y_c, 100, 100), Some(clip));
|
||||
fixed_placement(
|
||||
r(2000, 2000, 100, 100),
|
||||
r(x_max - 100, y_max - 100, 100, 100),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(r(x_c, 2000, 100, 100), r(x_c, y_max - 100, 100, 100), Some(clip));
|
||||
fixed_placement(r(-2000, 2000, 100, 100), r(x_min, y_max - 100, 100, 100), Some(clip));
|
||||
fixed_placement(r(-2000, y_c, 100, 100), r(x_min, y_c, 100, 100), Some(clip));
|
||||
fixed_placement(r(-2000, -2000, 100, 100), r(x_min, y_min, 100, 100), Some(clip));
|
||||
|
||||
// matching size, covering
|
||||
fixed_placement(
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
|
||||
// matching size, overlapping
|
||||
fixed_placement(
|
||||
r(x_c, y_c, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_c, y_n, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
|
||||
fixed_placement(
|
||||
r(x_e, y_n, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_e, y_c, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_e, y_s, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_c, y_s, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_w, y_s, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_w, y_c, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_w, y_n, clip_width, clip_height),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
|
||||
// too big, overlapping
|
||||
fixed_placement(
|
||||
r(x_c, y_c, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_c, y_n, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_e, y_n, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_e, y_c, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_e, y_s, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_c, y_s, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_w, y_s, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_w, y_c, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_w, y_n, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
|
||||
// too big, outside
|
||||
fixed_placement(
|
||||
r(x_c, -3000, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(3000, -3000, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(3000, y_c, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(3000, 3000, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(x_c, 3000, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(-3000, 3000, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(-3000, y_c, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
fixed_placement(
|
||||
r(-3000, -3000, clip_width + 5, clip_height + 5),
|
||||
r(x_min, y_min, clip_width, clip_height),
|
||||
Some(clip),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user