mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-28 04:45:13 +00:00
Add a shared string that can be used in properties
This commit is contained in:
parent
e6be2c91b8
commit
25bf149e13
14 changed files with 208 additions and 52 deletions
|
@ -1,16 +1,9 @@
|
||||||
|
|
||||||
namespace sixtyfps::internal {
|
namespace sixtyfps::internal {
|
||||||
// FIXME: this is just required because of something wrong
|
|
||||||
// with &str in cbindgen, but one should not have &str anyway
|
|
||||||
using str = char;
|
|
||||||
|
|
||||||
// Workaround https://github.com/eqrion/cbindgen/issues/43
|
// Workaround https://github.com/eqrion/cbindgen/issues/43
|
||||||
struct ComponentVTable;
|
struct ComponentVTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#include "sixtyfps_gl_internal.h"
|
|
||||||
#include "sixtyfps_internal.h"
|
#include "sixtyfps_internal.h"
|
||||||
|
#include "sixtyfps_gl_internal.h"
|
||||||
|
|
||||||
namespace sixtyfps {
|
namespace sixtyfps {
|
||||||
|
|
||||||
|
|
36
api/sixtyfps-cpp/include/sixtyfps_string.h
Normal file
36
api/sixtyfps-cpp/include/sixtyfps_string.h
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#include <string_view>
|
||||||
|
#include "sixtyfps_string_internal.h"
|
||||||
|
|
||||||
|
namespace sixtyfps {
|
||||||
|
|
||||||
|
struct SharedString
|
||||||
|
{
|
||||||
|
SharedString() { internal::sixtyfps_shared_string_from_bytes(this, "", 0); }
|
||||||
|
SharedString(std::string_view s)
|
||||||
|
{
|
||||||
|
internal::sixtyfps_shared_string_from_bytes(this, s.data(), s.size());
|
||||||
|
}
|
||||||
|
SharedString(const SharedString &other)
|
||||||
|
{
|
||||||
|
internal::sixtyfps_shared_string_clone(this, &other);
|
||||||
|
}
|
||||||
|
~SharedString() { internal::sixtyfps_shared_string_drop(this); }
|
||||||
|
SharedString &operator=(const SharedString &other)
|
||||||
|
{
|
||||||
|
internal::sixtyfps_shared_string_drop(this);
|
||||||
|
internal::sixtyfps_shared_string_clone(this, &other);
|
||||||
|
}
|
||||||
|
SharedString &operator=(std::string_view s)
|
||||||
|
{
|
||||||
|
internal::sixtyfps_shared_string_drop(this);
|
||||||
|
internal::sixtyfps_shared_string_from_bytes(this, s.data(), s.size());
|
||||||
|
}
|
||||||
|
SharedString &operator=(SharedString &&other) { std::swap(inner, other.inner); }
|
||||||
|
|
||||||
|
operator std::string_view() const { return internal::sixtyfps_shared_string_bytes(this); }
|
||||||
|
auto data() const -> const char * { return internal::sixtyfps_shared_string_bytes(this); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void *inner; // opaque
|
||||||
|
};
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ pub mod re_exports {
|
||||||
pub use corelib::abi::datastructures::{Component, ComponentTO, ComponentVTable, ItemTreeNode};
|
pub use corelib::abi::datastructures::{Component, ComponentTO, ComponentVTable, ItemTreeNode};
|
||||||
pub use corelib::abi::primitives::{Image, ImageVTable, Rectangle, RectangleVTable};
|
pub use corelib::abi::primitives::{Image, ImageVTable, Rectangle, RectangleVTable};
|
||||||
pub use corelib::ComponentVTable_static;
|
pub use corelib::ComponentVTable_static;
|
||||||
|
pub use corelib::SharedString;
|
||||||
pub use gl::sixtyfps_runtime_run_component_with_gl_renderer;
|
pub use gl::sixtyfps_runtime_run_component_with_gl_renderer;
|
||||||
pub use once_cell::sync::Lazy;
|
pub use once_cell::sync::Lazy;
|
||||||
pub use vtable::{self, *};
|
pub use vtable::{self, *};
|
||||||
|
|
|
@ -10,6 +10,5 @@ path = "lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
proc-macro2 = "1.0"
|
|
||||||
sixtyfps_compiler = { path = "../../../sixtyfps_compiler", features = ["proc_macro_span"] }
|
sixtyfps_compiler = { path = "../../../sixtyfps_compiler", features = ["proc_macro_span"] }
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
use object_tree::Expression;
|
use object_tree::Expression;
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::{Literal, TokenTree};
|
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use sixtyfps_compiler::*;
|
use sixtyfps_compiler::*;
|
||||||
|
|
||||||
|
@ -126,9 +125,7 @@ pub fn sixtyfps(stream: TokenStream) -> TokenStream {
|
||||||
// That's an error
|
// That's an error
|
||||||
Expression::Identifier(_) => quote!(),
|
Expression::Identifier(_) => quote!(),
|
||||||
Expression::StringLiteral(s) => {
|
Expression::StringLiteral(s) => {
|
||||||
let c_str: std::ffi::CString = std::ffi::CString::new(s.as_bytes()).unwrap();
|
quote!(sixtyfps::re_exports::SharedString::from(#s))
|
||||||
let tok = TokenTree::Literal(Literal::byte_string(c_str.as_bytes_with_nul()));
|
|
||||||
quote!(#tok as *const u8).into()
|
|
||||||
}
|
}
|
||||||
Expression::NumberLiteral(n) => quote!(#n),
|
Expression::NumberLiteral(n) => quote!(#n),
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ SuperSimple = Rectangle {
|
||||||
Image {
|
Image {
|
||||||
x: 200;
|
x: 200;
|
||||||
y: 200;
|
y: 200;
|
||||||
source: "../../../examples/graphicstest/logo.png";
|
source: "../../examples/graphicstest/logo.png";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ const-field-offset = { path = "../../helper_crates/const-field-offset" }
|
||||||
vtable = { path = "../../helper_crates/vtable" }
|
vtable = { path = "../../helper_crates/vtable" }
|
||||||
winit = "0.22.1"
|
winit = "0.22.1"
|
||||||
lyon = { version = "0.15.8" }
|
lyon = { version = "0.15.8" }
|
||||||
|
servo_arc = "0.1.1" #we need the Arc::from_header_and_iter
|
||||||
|
once_cell = "1.4"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cbindgen = "0.14.2"
|
cbindgen = "0.14.2"
|
||||||
|
|
|
@ -103,7 +103,7 @@ pub struct LayoutInfo {
|
||||||
pub enum RenderingInfo {
|
pub enum RenderingInfo {
|
||||||
NoContents,
|
NoContents,
|
||||||
Rectangle(f32, f32, f32, f32, u32), // Should be a beret structure
|
Rectangle(f32, f32, f32, f32, u32), // Should be a beret structure
|
||||||
Image(f32, f32, &'static str),
|
Image(f32, f32, crate::SharedString),
|
||||||
/*Path(Vec<PathElement>),
|
/*Path(Vec<PathElement>),
|
||||||
Image(OpaqueImageHandle, AspectRatio),
|
Image(OpaqueImageHandle, AspectRatio),
|
||||||
Text(String)*/
|
Text(String)*/
|
||||||
|
|
|
@ -38,15 +38,11 @@ impl ItemConsts for Rectangle {
|
||||||
> = Rectangle::field_offsets().cached_rendering_data;
|
> = Rectangle::field_offsets().cached_rendering_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: remove (or use the libc one)
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
type c_char = i8;
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(const_field_offset::FieldOffsets)]
|
#[derive(const_field_offset::FieldOffsets, Default)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
/// FIXME: make it a image source
|
/// FIXME: make it a image source
|
||||||
pub source: *const c_char,
|
pub source: crate::SharedString,
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
pub y: f32,
|
pub y: f32,
|
||||||
pub width: f32,
|
pub width: f32,
|
||||||
|
@ -54,29 +50,10 @@ pub struct Image {
|
||||||
pub cached_rendering_data: super::datastructures::CachedRenderingData,
|
pub cached_rendering_data: super::datastructures::CachedRenderingData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Image {
|
|
||||||
fn default() -> Self {
|
|
||||||
Image {
|
|
||||||
source: (b"\0").as_ptr() as *const _,
|
|
||||||
x: 0.,
|
|
||||||
y: 0.,
|
|
||||||
width: 0.,
|
|
||||||
height: 0.,
|
|
||||||
cached_rendering_data: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item for Image {
|
impl Item for Image {
|
||||||
fn geometry(&self) {}
|
fn geometry(&self) {}
|
||||||
fn rendering_info(&self) -> RenderingInfo {
|
fn rendering_info(&self) -> RenderingInfo {
|
||||||
unsafe {
|
RenderingInfo::Image(self.x, self.y, self.source.clone())
|
||||||
RenderingInfo::Image(
|
|
||||||
self.x,
|
|
||||||
self.y,
|
|
||||||
std::ffi::CStr::from_ptr(self.source).to_str().unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layouting_info(&self) -> LayoutInfo {
|
fn layouting_info(&self) -> LayoutInfo {
|
||||||
|
|
136
sixtyfps_runtime/corelib/abi/string.rs
Normal file
136
sixtyfps_runtime/corelib/abi/string.rs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
use core::mem::MaybeUninit;
|
||||||
|
use servo_arc::ThinArc;
|
||||||
|
use std::{fmt::Debug, ops::Deref};
|
||||||
|
|
||||||
|
/// The string type suitable for properties. It is shared meaning passing copies
|
||||||
|
/// around will not allocate, and that different properties with the same string
|
||||||
|
/// can share the same buffer.
|
||||||
|
/// It is also ffi-friendly as the buffer always ends with `'\0'`
|
||||||
|
/// Internally, this is an implicitly shared type to a null terminated string
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SharedString {
|
||||||
|
/// Invariant: The usize header is the `len` of the vector, the contained buffer is [MaybeUninit<u8>]
|
||||||
|
/// buffer[0..=len] is initialized and valid utf8, and buffer[len] is '\0'
|
||||||
|
inner: ThinArc<usize, MaybeUninit<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedString {
|
||||||
|
fn as_ptr(&self) -> *const u8 {
|
||||||
|
self.inner.slice.as_ptr() as *const u8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.inner.header.header
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
unsafe {
|
||||||
|
core::str::from_utf8_unchecked(core::slice::from_raw_parts(self.as_ptr(), self.len()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SharedString {
|
||||||
|
type Target = str;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SharedString {
|
||||||
|
fn default() -> Self {
|
||||||
|
// Unfortunately, the Arc constructor is not const, so we must use a Lazy static for that
|
||||||
|
static NULL: once_cell::sync::Lazy<ThinArc<usize, MaybeUninit<u8>>> =
|
||||||
|
once_cell::sync::Lazy::new(|| {
|
||||||
|
servo_arc::Arc::into_thin(servo_arc::Arc::from_header_and_iter(
|
||||||
|
servo_arc::HeaderWithLength::new(0, core::mem::align_of::<usize>()),
|
||||||
|
[MaybeUninit::new(0); core::mem::align_of::<usize>()].iter().cloned(),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
SharedString { inner: NULL.clone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for SharedString {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
struct AddNullIter<'a> {
|
||||||
|
pos: usize,
|
||||||
|
str: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for AddNullIter<'a> {
|
||||||
|
type Item = MaybeUninit<u8>;
|
||||||
|
fn next(&mut self) -> Option<MaybeUninit<u8>> {
|
||||||
|
let pos = self.pos;
|
||||||
|
self.pos += 1;
|
||||||
|
let align = core::mem::align_of::<usize>();
|
||||||
|
if pos < self.str.len() {
|
||||||
|
Some(MaybeUninit::new(self.str[pos]))
|
||||||
|
} else if pos < (self.str.len() + align) & !(align - 1) {
|
||||||
|
Some(MaybeUninit::new(0))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let l = self.str.len() + 1;
|
||||||
|
// add some padding at the end since the sice of the inner will anyway have to be padded
|
||||||
|
let align = core::mem::align_of::<usize>();
|
||||||
|
let l = (l + align - 1) & !(align - 1);
|
||||||
|
let l = l - self.pos;
|
||||||
|
(l, Some(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> core::iter::ExactSizeIterator for AddNullIter<'a> {}
|
||||||
|
|
||||||
|
let iter = AddNullIter { str: value.as_bytes(), pos: 0 };
|
||||||
|
|
||||||
|
SharedString {
|
||||||
|
inner: servo_arc::Arc::into_thin(servo_arc::Arc::from_header_and_iter(
|
||||||
|
servo_arc::HeaderWithLength::new(value.len(), iter.size_hint().0),
|
||||||
|
iter,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for SharedString {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Debug::fmt(self.as_str(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// for cbingen.
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
type c_char = u8;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn sixtyfps_shared_string_bytes(ss: &SharedString) -> *const c_char {
|
||||||
|
ss.as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
/// Destroy the shared string
|
||||||
|
pub unsafe extern "C" fn sixtyfps_shared_string_drop(ss: *const SharedString) {
|
||||||
|
core::ptr::read(ss);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
/// Increment the reference count of the string.
|
||||||
|
/// the resulting structure must be passed to sixtyfps_shared_string_drop
|
||||||
|
pub unsafe extern "C" fn sixtyfps_shared_string_clone(out: *mut SharedString, ss: &SharedString) {
|
||||||
|
core::ptr::write(out, ss.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
/// Safety: bytes must be a valid utf-8 string of size len wihout null inside.
|
||||||
|
/// the resulting structure must be passed to sixtyfps_shared_string_drop
|
||||||
|
pub unsafe extern "C" fn sixtyfps_shared_string_from_bytes(
|
||||||
|
out: *mut SharedString,
|
||||||
|
bytes: *const c_char,
|
||||||
|
len: usize,
|
||||||
|
) {
|
||||||
|
let str = core::str::from_utf8_unchecked(core::slice::from_raw_parts(bytes, len));
|
||||||
|
core::ptr::write(out, SharedString::from(str));
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ fn main() {
|
||||||
.map(|x| x.to_string())
|
.map(|x| x.to_string())
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let exclude = ["SharedString"].iter().map(|x| x.to_string()).collect::<Vec<String>>();
|
||||||
|
|
||||||
let config = cbindgen::Config {
|
let config = cbindgen::Config {
|
||||||
pragma_once: true,
|
pragma_once: true,
|
||||||
include_version: true,
|
include_version: true,
|
||||||
|
@ -18,15 +20,26 @@ fn main() {
|
||||||
language: cbindgen::Language::Cxx,
|
language: cbindgen::Language::Cxx,
|
||||||
cpp_compat: true,
|
cpp_compat: true,
|
||||||
documentation: true,
|
documentation: true,
|
||||||
export: cbindgen::ExportConfig { include, ..Default::default() },
|
export: cbindgen::ExportConfig { include, exclude, ..Default::default() },
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
cbindgen::Builder::new()
|
||||||
|
.with_config(config.clone())
|
||||||
|
.with_src(format!("{}/abi/string.rs", crate_dir))
|
||||||
|
.with_after_include("namespace sixtyfps { struct SharedString; }")
|
||||||
|
.generate()
|
||||||
|
.expect("Unable to generate bindings")
|
||||||
|
.write_to_file(env::var("OUT_DIR").unwrap() + "/sixtyfps_string_internal.h");
|
||||||
|
|
||||||
cbindgen::Builder::new()
|
cbindgen::Builder::new()
|
||||||
.with_config(config)
|
.with_config(config)
|
||||||
.with_crate(crate_dir)
|
.with_src(format!("{}/abi/datastructures.rs", crate_dir))
|
||||||
.with_header("#include <vtable.h>")
|
.with_src(format!("{}/abi/primitives.rs", crate_dir))
|
||||||
|
.with_src(format!("{}/abi/model.rs", crate_dir))
|
||||||
|
.with_include("vtable.h")
|
||||||
|
.with_include("sixtyfps_string.h")
|
||||||
.generate()
|
.generate()
|
||||||
.expect("Unable to generate bindings")
|
.expect("Unable to generate bindings")
|
||||||
.write_to_file(env::var("OUT_DIR").unwrap() + "/sixtyfps_internal.h");
|
.write_to_file(env::var("OUT_DIR").unwrap() + "/sixtyfps_internal.h");
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub(crate) fn update_item_rendering_data<Backend: GraphicsBackend>(
|
||||||
{
|
{
|
||||||
let mut image_path = std::env::current_exe().unwrap();
|
let mut image_path = std::env::current_exe().unwrap();
|
||||||
image_path.pop(); // pop of executable name
|
image_path.pop(); // pop of executable name
|
||||||
image_path.push(_source);
|
image_path.push(&*_source);
|
||||||
let image = image::open(image_path.as_path()).unwrap().into_rgba();
|
let image = image::open(image_path.as_path()).unwrap().into_rgba();
|
||||||
let source_size = image.dimensions();
|
let source_size = image.dimensions();
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,12 @@ pub mod abi {
|
||||||
pub mod datastructures;
|
pub mod datastructures;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
|
pub mod string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use abi::string::SharedString;
|
||||||
|
|
||||||
mod item_rendering;
|
mod item_rendering;
|
||||||
|
|
||||||
pub struct MainWindow<GraphicsBackend>
|
pub struct MainWindow<GraphicsBackend>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use core::ptr::NonNull;
|
use core::ptr::NonNull;
|
||||||
use corelib::abi::datastructures::{ComponentBox, ComponentRef, ComponentRefMut, ComponentVTable};
|
use corelib::abi::datastructures::{ComponentBox, ComponentRef, ComponentRefMut, ComponentVTable};
|
||||||
|
use corelib::SharedString;
|
||||||
use sixtyfps_compiler::object_tree::Expression;
|
use sixtyfps_compiler::object_tree::Expression;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
@ -34,16 +35,13 @@ impl PropertyWriter for u32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyWriter for *const i8 {
|
impl PropertyWriter for SharedString {
|
||||||
unsafe fn write(ptr: *mut u8, value: &Expression) {
|
unsafe fn write(ptr: *mut u8, value: &Expression) {
|
||||||
let val: Self = match value {
|
let val: Self = match value {
|
||||||
Expression::StringLiteral(v) => {
|
Expression::StringLiteral(v) => (**v).into(),
|
||||||
// FIXME that's a leak
|
|
||||||
std::ffi::CString::new(v.as_str()).unwrap().into_raw() as _
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
};
|
};
|
||||||
std::ptr::write(ptr as *mut Self, val);
|
*(ptr as *mut Self) = val.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +126,7 @@ fn main() -> std::io::Result<()> {
|
||||||
("y", (offsets.y.get_byte_offset(), set_property::<f32> as _)),
|
("y", (offsets.y.get_byte_offset(), set_property::<f32> as _)),
|
||||||
("width", (offsets.width.get_byte_offset(), set_property::<f32> as _)),
|
("width", (offsets.width.get_byte_offset(), set_property::<f32> as _)),
|
||||||
("height", (offsets.height.get_byte_offset(), set_property::<f32> as _)),
|
("height", (offsets.height.get_byte_offset(), set_property::<f32> as _)),
|
||||||
("source", (offsets.source.get_byte_offset(), set_property::<*const i8> as _)),
|
("source", (offsets.source.get_byte_offset(), set_property::<SharedString> as _)),
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue