mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Refactor and fix style folding
This commit is contained in:
parent
90fb28b4b6
commit
5d05c3f68a
@ -148,7 +148,7 @@ fn process_const(
|
|||||||
if attr.path.is_ident("fold") {
|
if attr.path.is_ident("fold") {
|
||||||
let func: syn::Expr = attr.parse_args()?;
|
let func: syn::Expr = attr.parse_args()?;
|
||||||
folder = Some(quote! {
|
folder = Some(quote! {
|
||||||
const FOLDABLE: bool = true;
|
const FOLDING: bool = true;
|
||||||
|
|
||||||
fn fold(inner: Self::Value, outer: Self::Value) -> Self::Value {
|
fn fold(inner: Self::Value, outer: Self::Value) -> Self::Value {
|
||||||
let f: fn(Self::Value, Self::Value) -> Self::Value = #func;
|
let f: fn(Self::Value, Self::Value) -> Self::Value = #func;
|
||||||
@ -179,7 +179,7 @@ fn process_const(
|
|||||||
|
|
||||||
const NAME: &'static str = #name;
|
const NAME: &'static str = #name;
|
||||||
|
|
||||||
fn class_id() -> TypeId {
|
fn node_id() -> TypeId {
|
||||||
TypeId::of::<#self_ty>()
|
TypeId::of::<#self_ty>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,6 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// TODO(style): Possible optimizations:
|
|
||||||
// - Ref-count map for cheaper cloning and smaller footprint
|
|
||||||
// - Store map in `Option` to make empty maps non-allocating
|
|
||||||
// - Store small properties inline
|
|
||||||
|
|
||||||
/// An item with associated styles.
|
/// An item with associated styles.
|
||||||
#[derive(PartialEq, Clone, Hash)]
|
#[derive(PartialEq, Clone, Hash)]
|
||||||
pub struct Styled<T> {
|
pub struct Styled<T> {
|
||||||
@ -68,15 +63,6 @@ impl StyleMap {
|
|||||||
|
|
||||||
/// Set the value for a style property.
|
/// Set the value for a style property.
|
||||||
pub fn set<P: Property>(&mut self, key: P, value: P::Value) {
|
pub fn set<P: Property>(&mut self, key: P, value: P::Value) {
|
||||||
for entry in &mut self.0 {
|
|
||||||
if entry.is::<P>() {
|
|
||||||
let prev = entry.downcast::<P>().unwrap();
|
|
||||||
let folded = P::fold(value, prev.clone());
|
|
||||||
*entry = Entry::new(key, folded);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.0.push(Entry::new(key, value));
|
self.0.push(Entry::new(key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,14 +107,7 @@ impl StyleMap {
|
|||||||
/// style maps, whereas `chain` would be used during layouting to combine
|
/// style maps, whereas `chain` would be used during layouting to combine
|
||||||
/// immutable style maps from different levels of the hierarchy.
|
/// immutable style maps from different levels of the hierarchy.
|
||||||
pub fn apply(&mut self, outer: &Self) {
|
pub fn apply(&mut self, outer: &Self) {
|
||||||
for outer in &outer.0 {
|
self.0.splice(0 .. 0, outer.0.clone());
|
||||||
if let Some(inner) = self.0.iter_mut().find(|inner| inner.is_same(outer)) {
|
|
||||||
*inner = inner.fold(outer);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.0.push(outer.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subtract `other` from `self` in-place, keeping only styles that are in
|
/// Subtract `other` from `self` in-place, keeping only styles that are in
|
||||||
@ -144,7 +123,7 @@ impl StyleMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether two style maps are equal when filtered down to properties of the
|
/// Whether two style maps are equal when filtered down to properties of the
|
||||||
/// class `T`.
|
/// node `T`.
|
||||||
pub fn compatible<T: 'static>(&self, other: &Self) -> bool {
|
pub fn compatible<T: 'static>(&self, other: &Self) -> bool {
|
||||||
let f = |entry: &&Entry| entry.is_of::<T>();
|
let f = |entry: &&Entry| entry.is_of::<T>();
|
||||||
self.0.iter().filter(f).count() == other.0.iter().filter(f).count()
|
self.0.iter().filter(f).count() == other.0.iter().filter(f).count()
|
||||||
@ -154,7 +133,7 @@ impl StyleMap {
|
|||||||
|
|
||||||
impl Debug for StyleMap {
|
impl Debug for StyleMap {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
for entry in &self.0 {
|
for entry in self.0.iter().rev() {
|
||||||
writeln!(f, "{:#?}", entry)?;
|
writeln!(f, "{:#?}", entry)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -176,23 +155,26 @@ impl PartialEq for StyleMap {
|
|||||||
/// matches further up the chain.
|
/// matches further up the chain.
|
||||||
#[derive(Clone, Copy, Hash)]
|
#[derive(Clone, Copy, Hash)]
|
||||||
pub struct StyleChain<'a> {
|
pub struct StyleChain<'a> {
|
||||||
/// The first map in the chain.
|
/// The first link in the chain.
|
||||||
first: Link<'a>,
|
first: Link<'a>,
|
||||||
/// The remaining maps in the chain.
|
/// The remaining links in the chain.
|
||||||
outer: Option<&'a Self>,
|
outer: Option<&'a Self>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The two kinds of links in the chain.
|
/// The two kinds of links in the chain.
|
||||||
#[derive(Clone, Copy, Hash)]
|
#[derive(Clone, Copy, Hash)]
|
||||||
enum Link<'a> {
|
enum Link<'a> {
|
||||||
|
/// Just a map with styles.
|
||||||
Map(&'a StyleMap),
|
Map(&'a StyleMap),
|
||||||
|
/// A barrier that, in combination with one more such barrier, stops scoped
|
||||||
|
/// styles for the node with this type id.
|
||||||
Barrier(TypeId),
|
Barrier(TypeId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StyleChain<'a> {
|
impl<'a> StyleChain<'a> {
|
||||||
/// Start a new style chain with a root map.
|
/// Start a new style chain with a root map.
|
||||||
pub fn new(map: &'a StyleMap) -> Self {
|
pub fn new(first: &'a StyleMap) -> Self {
|
||||||
Self { first: Link::Map(map), outer: None }
|
Self { first: Link::Map(first), outer: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the (folded) value of a copyable style property.
|
/// Get the (folded) value of a copyable style property.
|
||||||
@ -207,7 +189,7 @@ impl<'a> StyleChain<'a> {
|
|||||||
where
|
where
|
||||||
P::Value: Copy,
|
P::Value: Copy,
|
||||||
{
|
{
|
||||||
self.get_impl(key, 0)
|
self.get_cloned(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to a style property's value.
|
/// Get a reference to a style property's value.
|
||||||
@ -222,7 +204,7 @@ impl<'a> StyleChain<'a> {
|
|||||||
where
|
where
|
||||||
P: Nonfolding,
|
P: Nonfolding,
|
||||||
{
|
{
|
||||||
self.get_ref_impl(key, 0)
|
self.values(key).next().unwrap_or_else(|| P::default_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the (folded) value of any style property.
|
/// Get the (folded) value of any style property.
|
||||||
@ -234,7 +216,15 @@ impl<'a> StyleChain<'a> {
|
|||||||
/// Returns the property's default value if no map in the chain contains an
|
/// Returns the property's default value if no map in the chain contains an
|
||||||
/// entry for it.
|
/// entry for it.
|
||||||
pub fn get_cloned<P: Property>(self, key: P) -> P::Value {
|
pub fn get_cloned<P: Property>(self, key: P) -> P::Value {
|
||||||
self.get_impl(key, 0)
|
if P::FOLDING {
|
||||||
|
self.values(key)
|
||||||
|
.cloned()
|
||||||
|
.chain(std::iter::once(P::default()))
|
||||||
|
.reduce(P::fold)
|
||||||
|
.unwrap()
|
||||||
|
} else {
|
||||||
|
self.values(key).next().cloned().unwrap_or_else(P::default)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a barrier into the style chain.
|
/// Insert a barrier into the style chain.
|
||||||
@ -242,81 +232,60 @@ impl<'a> StyleChain<'a> {
|
|||||||
/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style
|
/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style
|
||||||
/// can still be read through a single barrier (the one of the node it
|
/// can still be read through a single barrier (the one of the node it
|
||||||
/// _should_ apply to), but a second barrier will make it invisible.
|
/// _should_ apply to), but a second barrier will make it invisible.
|
||||||
pub fn barred<'b>(&'b self, class: TypeId) -> StyleChain<'b> {
|
pub fn barred<'b>(&'b self, node: TypeId) -> StyleChain<'b> {
|
||||||
if self.needs_barrier(class) {
|
if self
|
||||||
|
.maps()
|
||||||
|
.any(|map| map.0.iter().any(|entry| entry.scoped && entry.is_of_same(node)))
|
||||||
|
{
|
||||||
StyleChain {
|
StyleChain {
|
||||||
first: Link::Barrier(class),
|
first: Link::Barrier(node),
|
||||||
outer: Some(self),
|
outer: Some(self),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
*self
|
*self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> StyleChain<'a> {
|
/// Iterate over all values for the given property in the chain.
|
||||||
fn get_impl<P: Property>(self, key: P, depth: usize) -> P::Value {
|
fn values<P: Property>(self, _: P) -> impl Iterator<Item = &'a P::Value> {
|
||||||
let (value, depth) = self.process(key, depth);
|
let mut depth = 0;
|
||||||
if let Some(value) = value.cloned() {
|
self.links().flat_map(move |link| {
|
||||||
if P::FOLDABLE {
|
let mut entries: &[Entry] = &[];
|
||||||
if let Some(outer) = self.outer {
|
match link {
|
||||||
P::fold(value, outer.get_cloned(key))
|
Link::Map(map) => entries = &map.0,
|
||||||
} else {
|
Link::Barrier(id) => depth += (id == P::node_id()) as usize,
|
||||||
P::fold(value, P::default())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value
|
|
||||||
}
|
}
|
||||||
} else if let Some(outer) = self.outer {
|
entries
|
||||||
outer.get_impl(key, depth)
|
.iter()
|
||||||
} else {
|
.rev()
|
||||||
P::default()
|
.filter(move |entry| entry.is::<P>() && (!entry.scoped || depth <= 1))
|
||||||
}
|
.filter_map(|entry| entry.downcast::<P>())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ref_impl<P: Property>(self, key: P, depth: usize) -> &'a P::Value
|
/// Iterate over the links of the chain.
|
||||||
where
|
fn links(self) -> impl Iterator<Item = Link<'a>> {
|
||||||
P: Nonfolding,
|
let mut cursor = Some(self);
|
||||||
{
|
std::iter::from_fn(move || {
|
||||||
let (value, depth) = self.process(key, depth);
|
let Self { first, outer } = cursor?;
|
||||||
if let Some(value) = value {
|
cursor = outer.copied();
|
||||||
value
|
Some(first)
|
||||||
} else if let Some(outer) = self.outer {
|
})
|
||||||
outer.get_ref_impl(key, depth)
|
|
||||||
} else {
|
|
||||||
P::default_ref()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process<P: Property>(self, _: P, depth: usize) -> (Option<&'a P::Value>, usize) {
|
/// Iterate over the map links of the chain.
|
||||||
match self.first {
|
fn maps(self) -> impl Iterator<Item = &'a StyleMap> {
|
||||||
Link::Map(map) => (
|
self.links().filter_map(|link| match link {
|
||||||
map.0
|
Link::Map(map) => Some(map),
|
||||||
.iter()
|
Link::Barrier(_) => None,
|
||||||
.find(|entry| entry.is::<P>() && (!entry.scoped || depth <= 1))
|
})
|
||||||
.and_then(|entry| entry.downcast::<P>()),
|
|
||||||
depth,
|
|
||||||
),
|
|
||||||
Link::Barrier(class) => (None, depth + (P::class_id() == class) as usize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_barrier(self, class: TypeId) -> bool {
|
|
||||||
if let Link::Map(map) = self.first {
|
|
||||||
if map.0.iter().any(|entry| entry.is_of_same(class)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.outer.map_or(false, |outer| outer.needs_barrier(class))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for StyleChain<'_> {
|
impl Debug for StyleChain<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
self.first.fmt(f)?;
|
for link in self.links() {
|
||||||
if let Some(outer) = self.outer {
|
link.fmt(f)?;
|
||||||
outer.fmt(f)?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -334,55 +303,56 @@ impl Debug for Link<'_> {
|
|||||||
/// An entry for a single style property.
|
/// An entry for a single style property.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Entry {
|
struct Entry {
|
||||||
p: Arc<dyn Bounds>,
|
pair: Arc<dyn Bounds>,
|
||||||
scoped: bool,
|
scoped: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
fn new<P: Property>(key: P, value: P::Value) -> Self {
|
fn new<P: Property>(key: P, value: P::Value) -> Self {
|
||||||
Self { p: Arc::new((key, value)), scoped: false }
|
Self {
|
||||||
|
pair: Arc::new((key, value)),
|
||||||
|
scoped: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is<P: Property>(&self) -> bool {
|
fn is<P: Property>(&self) -> bool {
|
||||||
self.p.style_id() == TypeId::of::<P>()
|
self.pair.style_id() == TypeId::of::<P>()
|
||||||
}
|
|
||||||
|
|
||||||
fn is_same(&self, other: &Self) -> bool {
|
|
||||||
self.p.style_id() == other.p.style_id()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_of<T: 'static>(&self) -> bool {
|
fn is_of<T: 'static>(&self) -> bool {
|
||||||
self.p.class_id() == TypeId::of::<T>()
|
self.pair.node_id() == TypeId::of::<T>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_of_same(&self, class: TypeId) -> bool {
|
fn is_of_same(&self, node: TypeId) -> bool {
|
||||||
self.p.class_id() == class
|
self.pair.node_id() == node
|
||||||
}
|
}
|
||||||
|
|
||||||
fn downcast<P: Property>(&self) -> Option<&P::Value> {
|
fn downcast<P: Property>(&self) -> Option<&P::Value> {
|
||||||
self.p.as_any().downcast_ref()
|
self.pair.as_any().downcast_ref()
|
||||||
}
|
|
||||||
|
|
||||||
fn fold(&self, outer: &Self) -> Self {
|
|
||||||
self.p.fold(outer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Entry {
|
impl Debug for Entry {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
self.p.dyn_fmt(f)
|
f.write_str("#[")?;
|
||||||
|
self.pair.dyn_fmt(f)?;
|
||||||
|
if self.scoped {
|
||||||
|
f.write_str(" (scoped)")?;
|
||||||
|
}
|
||||||
|
f.write_str("]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Entry {
|
impl PartialEq for Entry {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.p.dyn_eq(other)
|
self.pair.dyn_eq(other) && self.scoped == other.scoped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for Entry {
|
impl Hash for Entry {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
state.write_u64(self.p.hash64());
|
state.write_u64(self.pair.hash64());
|
||||||
|
state.write_u8(self.scoped as u8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,7 +360,7 @@ impl Hash for Entry {
|
|||||||
///
|
///
|
||||||
/// This trait is not intended to be implemented manually, but rather through
|
/// This trait is not intended to be implemented manually, but rather through
|
||||||
/// the `#[properties]` proc-macro.
|
/// the `#[properties]` proc-macro.
|
||||||
pub trait Property: Copy + Sync + Send + 'static {
|
pub trait Property: Sync + Send + 'static {
|
||||||
/// The type of value that is returned when getting this property from a
|
/// The type of value that is returned when getting this property from a
|
||||||
/// style map. For example, this could be [`Length`](crate::geom::Length)
|
/// style map. For example, this could be [`Length`](crate::geom::Length)
|
||||||
/// for a `WIDTH` property.
|
/// for a `WIDTH` property.
|
||||||
@ -400,10 +370,10 @@ pub trait Property: Copy + Sync + Send + 'static {
|
|||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
|
|
||||||
/// Whether the property needs folding.
|
/// Whether the property needs folding.
|
||||||
const FOLDABLE: bool = false;
|
const FOLDING: bool = false;
|
||||||
|
|
||||||
/// The type id of the class this property belongs to.
|
/// The type id of the node this property belongs to.
|
||||||
fn class_id() -> TypeId;
|
fn node_id() -> TypeId;
|
||||||
|
|
||||||
/// The default value of the property.
|
/// The default value of the property.
|
||||||
fn default() -> Self::Value;
|
fn default() -> Self::Value;
|
||||||
@ -437,9 +407,8 @@ trait Bounds: Sync + Send + 'static {
|
|||||||
fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result;
|
fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result;
|
||||||
fn dyn_eq(&self, other: &Entry) -> bool;
|
fn dyn_eq(&self, other: &Entry) -> bool;
|
||||||
fn hash64(&self) -> u64;
|
fn hash64(&self) -> u64;
|
||||||
fn class_id(&self) -> TypeId;
|
fn node_id(&self) -> TypeId;
|
||||||
fn style_id(&self) -> TypeId;
|
fn style_id(&self) -> TypeId;
|
||||||
fn fold(&self, outer: &Entry) -> Entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Property> Bounds for (P, P::Value) {
|
impl<P: Property> Bounds for (P, P::Value) {
|
||||||
@ -448,11 +417,11 @@ impl<P: Property> Bounds for (P, P::Value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "#[{} = {:?}]", P::NAME, self.1)
|
write!(f, "{} = {:?}", P::NAME, self.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_eq(&self, other: &Entry) -> bool {
|
fn dyn_eq(&self, other: &Entry) -> bool {
|
||||||
self.style_id() == other.p.style_id()
|
self.style_id() == other.pair.style_id()
|
||||||
&& if let Some(other) = other.downcast::<P>() {
|
&& if let Some(other) = other.downcast::<P>() {
|
||||||
&self.1 == other
|
&self.1 == other
|
||||||
} else {
|
} else {
|
||||||
@ -467,17 +436,11 @@ impl<P: Property> Bounds for (P, P::Value) {
|
|||||||
state.finish()
|
state.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn class_id(&self) -> TypeId {
|
fn node_id(&self) -> TypeId {
|
||||||
P::class_id()
|
P::node_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style_id(&self) -> TypeId {
|
fn style_id(&self) -> TypeId {
|
||||||
TypeId::of::<P>()
|
TypeId::of::<P>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fold(&self, outer: &Entry) -> Entry {
|
|
||||||
let outer = outer.downcast::<P>().unwrap();
|
|
||||||
let combined = P::fold(self.1.clone(), outer.clone());
|
|
||||||
Entry::new(self.0, combined)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 935 B After Width: | Height: | Size: 2.0 KiB |
@ -25,8 +25,8 @@ D
|
|||||||
|
|
||||||
#set page(width: 80pt, height: 30pt)
|
#set page(width: 80pt, height: 30pt)
|
||||||
|
|
||||||
Fi[#set page(width: 80pt);rst]
|
[#set page(width: 80pt); First]
|
||||||
[#set page(width: 70pt); Second]
|
#pagebreak()
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
#pagebreak()
|
#pagebreak()
|
||||||
Fourth
|
Fourth
|
||||||
|
@ -8,3 +8,10 @@
|
|||||||
[First],
|
[First],
|
||||||
list([A], [B])
|
list([A], [B])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
// Ensure that constructor styles win, but not over outer styles.
|
||||||
|
#set par(align: center)
|
||||||
|
#par(align: right)[
|
||||||
|
A #rect(width: 2cm, fill: conifer, padding: 4pt)[B]
|
||||||
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user