mirror of
https://github.com/touchHLE/rust-sdl2.git
synced 2026-01-31 01:25:23 +01:00
Render geometry binding (#1472)
* make `pixels::Color` have a stable representation (`repr(C)`) this allows for faster code and is also needed for SDL_RenderGeometry * add binding for SDL_RenderGeometryRaw * add example of advanced use of `Canvas::render_geometry` * update unsafe trait docs to include a needed guarantee and refer to one doc comment instead of duplicating similar ones * rework almost everything - the macro was very hard to get sound, auto deref has to be prevented somehow - the unsafe traits could not be implemented for types defined in other crates the unsafety must be kept simple, users now only need to provide the right offsets, which core::mem::offset_of does update example * add convenience impls for &Vec<_> to VertexIndices * update example to use 2 ways of using render_geometry_raw, and use render_geometry * mention alignment in safety comments in render_geometry example * fix and improve docs * update changelog
This commit is contained in:
committed by
GitHub
parent
f0cdbb32b5
commit
ecd03de215
@@ -135,6 +135,9 @@ name = "renderer-target"
|
||||
[[example]]
|
||||
name = "events"
|
||||
|
||||
[[example]]
|
||||
name = "render-geometry"
|
||||
|
||||
[[example]]
|
||||
name = "renderer-texture"
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ when upgrading from a version of rust-sdl2 to another.
|
||||
|
||||
### Next
|
||||
|
||||
[PR #1472](https://github.com/Rust-SDL2/rust-sdl2/pull/1472) Add `Canvas::render_geometry`, `Canvas::render_geometry_raw` and an example that uses them both. Both these bindings use `SDL_RenderGeometryRaw`.
|
||||
|
||||
[PR #1473](https://github.com/Rust-SDL2/rust-sdl2/pull/1473) **BREAKING CHANGE** Fix UB in `ToColor` implementations and remove implementation for `isize`.
|
||||
|
||||
[PR #1414](https://github.com/Rust-SDL2/rust-sdl2/pull/1414) Use `TTF_GlyphIsProvided32` and `TTF_GlyphMetrics32` instead of the 16 bit ones.
|
||||
|
||||
146
examples/render-geometry.rs
Normal file
146
examples/render-geometry.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
extern crate sdl2;
|
||||
|
||||
use sdl2::event::Event;
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::pixels::Color;
|
||||
use sdl2::rect::FPoint;
|
||||
use sdl2::render::{RenderGeometryTextureParams, Vertex, VertexIndices};
|
||||
use std::mem::offset_of;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let sdl_context = sdl2::init().unwrap();
|
||||
let video_subsystem = sdl_context.video().unwrap();
|
||||
|
||||
let window = video_subsystem
|
||||
.window("Rust SDL2 render_geometry custom struct example", 800, 600)
|
||||
.position_centered()
|
||||
.opengl()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut canvas = window.into_canvas().build().unwrap();
|
||||
|
||||
let mut event_pump = sdl_context.event_pump().unwrap();
|
||||
let mut running = true;
|
||||
|
||||
while running {
|
||||
for event in event_pump.poll_iter() {
|
||||
match event {
|
||||
Event::Quit { .. }
|
||||
| Event::KeyDown {
|
||||
keycode: Some(Keycode::Escape),
|
||||
..
|
||||
} => {
|
||||
running = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// black background
|
||||
canvas.set_draw_color(Color::BLACK);
|
||||
canvas.clear();
|
||||
|
||||
// First, draw a triangle using `render_geometry`. The `tex_coord` fields are unused but
|
||||
// must be provided, `render_geometry` only supports `sdl2::render::Vertex`.
|
||||
let vertices = [
|
||||
Vertex {
|
||||
position: FPoint::new(100.0, 200.0),
|
||||
color: Color::RED,
|
||||
tex_coord: FPoint::new(0.0, 0.0),
|
||||
},
|
||||
Vertex {
|
||||
position: FPoint::new(200.0, 200.0),
|
||||
color: Color::GREEN,
|
||||
tex_coord: FPoint::new(0.0, 0.0),
|
||||
},
|
||||
Vertex {
|
||||
position: FPoint::new(150.0, 100.0),
|
||||
color: Color::BLUE,
|
||||
tex_coord: FPoint::new(0.0, 0.0),
|
||||
},
|
||||
];
|
||||
canvas
|
||||
.render_geometry(&vertices, None, VertexIndices::Sequential)
|
||||
.expect("render_geometry failed (probably unsupported, see error message)");
|
||||
|
||||
// `render_geometry_raw` supports any custom struct as long as it contains the needed data
|
||||
// (or other layout compatible of the needed data).
|
||||
// The struct does not need to be `repr(C)` or `Copy` for example.
|
||||
struct MyVertex {
|
||||
// For demonstration purposes color is `[u8; 4]` here. `[u8; 4]` is layout-compatible
|
||||
// with `sdl2::pixels::Color`
|
||||
color: [u8; 4],
|
||||
// The struct may contain data not needed by SDL.
|
||||
#[expect(dead_code)]
|
||||
foo: Vec<u8>,
|
||||
// When defining your own vertex struct, using `FPoint` for position and tex_coord
|
||||
// (and `Color` for color) is the easiest way. These are obviously layout-compatible
|
||||
// with `FPoint` and `Color`, respectively.
|
||||
pos: FPoint,
|
||||
}
|
||||
|
||||
// Define the vertices of a square
|
||||
let vertices = [
|
||||
MyVertex {
|
||||
color: [0, 0, 0, 0xff],
|
||||
foo: b"some".to_vec(),
|
||||
pos: FPoint::new(300.0, 100.0),
|
||||
},
|
||||
MyVertex {
|
||||
color: [0, 0xff, 0, 0xff],
|
||||
foo: b"unrelated".to_vec(),
|
||||
pos: FPoint::new(400.0, 100.0),
|
||||
},
|
||||
MyVertex {
|
||||
color: [0xff, 0, 0, 0xff],
|
||||
foo: b"data".to_vec(),
|
||||
pos: FPoint::new(300.0, 200.0),
|
||||
},
|
||||
MyVertex {
|
||||
color: [0xff, 0xff, 0, 0xff],
|
||||
foo: b"!".to_vec(),
|
||||
pos: FPoint::new(400.0, 200.0),
|
||||
},
|
||||
];
|
||||
|
||||
// A square is rendered as two triangles (see indices)
|
||||
// SAFETY: core::mem::offset_of makes sure the offsets are right and alignment is respected.
|
||||
unsafe {
|
||||
canvas.render_geometry_raw(
|
||||
&vertices,
|
||||
offset_of!(MyVertex, pos),
|
||||
&vertices,
|
||||
offset_of!(MyVertex, color),
|
||||
None::<RenderGeometryTextureParams<()>>,
|
||||
&[[0, 1, 2], [1, 2, 3]],
|
||||
)
|
||||
}
|
||||
.expect("render_geometry_raw failed (probably unsupported, see error message)");
|
||||
|
||||
// Parameters can be reused, here only the positions are swapped out for new ones.
|
||||
// SAFETY: core::mem::offset_of makes sure the offsets are right and alignment is respected.
|
||||
// The offset 0 is correct because the element type of positions is `[f32; 2]`.
|
||||
unsafe {
|
||||
canvas.render_geometry_raw(
|
||||
&[
|
||||
[500.0f32, 100.0],
|
||||
[600.0, 100.0],
|
||||
[500.0, 200.0],
|
||||
[600.0, 200.0],
|
||||
],
|
||||
0,
|
||||
&vertices,
|
||||
offset_of!(MyVertex, color),
|
||||
None::<RenderGeometryTextureParams<()>>,
|
||||
&[[0, 1, 2], [1, 2, 3]],
|
||||
)
|
||||
}
|
||||
.expect("render_geometry_raw failed (probably unsupported, see error message)");
|
||||
|
||||
canvas.present();
|
||||
thread::sleep(Duration::from_millis(16));
|
||||
}
|
||||
}
|
||||
@@ -48,9 +48,14 @@ impl Palette {
|
||||
// Already validated, so don't check again
|
||||
let ncolors = colors.len() as ::libc::c_int;
|
||||
|
||||
let colors = colors.iter().map(|color| color.raw()).collect::<Vec<_>>();
|
||||
|
||||
let result = unsafe { sys::SDL_SetPaletteColors(pal.raw, colors.as_ptr(), 0, ncolors) };
|
||||
let result = unsafe {
|
||||
sys::SDL_SetPaletteColors(
|
||||
pal.raw,
|
||||
colors.as_ptr().cast::<sys::SDL_Color>(),
|
||||
0,
|
||||
ncolors,
|
||||
)
|
||||
};
|
||||
|
||||
if result < 0 {
|
||||
Err(get_error())
|
||||
@@ -88,6 +93,7 @@ fn create_palette() {
|
||||
assert!(palette.len() == 255);
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct Color {
|
||||
pub r: u8,
|
||||
|
||||
@@ -41,7 +41,7 @@ use crate::surface::{Surface, SurfaceContext, SurfaceRef};
|
||||
use crate::video::{Window, WindowContext};
|
||||
use libc::c_void;
|
||||
use libc::{c_double, c_int};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::error::Error;
|
||||
use std::ffi::CStr;
|
||||
use std::fmt;
|
||||
@@ -52,6 +52,7 @@ use std::mem::{transmute, MaybeUninit};
|
||||
use std::ops::Deref;
|
||||
use std::ptr;
|
||||
use std::rc::Rc;
|
||||
use std::slice;
|
||||
|
||||
use crate::sys;
|
||||
use crate::sys::SDL_BlendMode;
|
||||
@@ -1964,6 +1965,288 @@ impl<T: RenderTarget> Canvas<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// `repr(C)` is not needed for soundness in this crate, but users may expect it to be
|
||||
// layout-compatible with `sys::SDL_Vertex`.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Vertex {
|
||||
pub position: FPoint,
|
||||
pub color: pixels::Color,
|
||||
pub tex_coord: FPoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum VertexIndices<'a> {
|
||||
/// Vertices are rendered in sequential order.
|
||||
Sequential,
|
||||
/// [`u8`] vertex indices.
|
||||
U8(&'a [u8]),
|
||||
/// [`u16`] vertex indices.
|
||||
U16(&'a [u16]),
|
||||
/// [`u32`] vertex indices. If any index is larger than [`i32::MAX`], SDL will return an error.
|
||||
U32(&'a [u32]),
|
||||
}
|
||||
|
||||
impl VertexIndices<'_> {
|
||||
/// Returns (indices, num_indices, size_indices)
|
||||
fn into_raw(self) -> (*const c_void, c_int, c_int) {
|
||||
match self {
|
||||
Self::Sequential => (ptr::null(), 0, 0),
|
||||
Self::U8(indices) => (indices.as_ptr().cast::<c_void>(), indices.len() as c_int, 1),
|
||||
Self::U16(indices) => (indices.as_ptr().cast::<c_void>(), indices.len() as c_int, 2),
|
||||
Self::U32(indices) => (indices.as_ptr().cast::<c_void>(), indices.len() as c_int, 4),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [u8]) -> Self {
|
||||
Self::U8(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u16]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [u16]) -> Self {
|
||||
Self::U16(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u32]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [u32]) -> Self {
|
||||
Self::U32(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [i32]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [i32]) -> Self {
|
||||
Self::U32(unsafe { slice::from_raw_parts(value.as_ptr().cast::<u32>(), value.len()) })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [[u8; 3]]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [[u8; 3]]) -> Self {
|
||||
Self::U8(unsafe { slice::from_raw_parts(value.as_ptr().cast::<u8>(), value.len() * 3) })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [[u16; 3]]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [[u16; 3]]) -> Self {
|
||||
Self::U16(unsafe { slice::from_raw_parts(value.as_ptr().cast::<u16>(), value.len() * 3) })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [[u32; 3]]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [[u32; 3]]) -> Self {
|
||||
Self::U32(unsafe { slice::from_raw_parts(value.as_ptr().cast::<u32>(), value.len() * 3) })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [[i32; 3]]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [[i32; 3]]) -> Self {
|
||||
Self::U32(unsafe { slice::from_raw_parts(value.as_ptr().cast::<u32>(), value.len() * 3) })
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_into_vertex_indices_forward {
|
||||
($($ty:ty)*) => {
|
||||
$(
|
||||
impl<'a> From<&'a Vec<$ty>> for VertexIndices<'a> {
|
||||
fn from(value: &'a Vec<$ty>) -> Self {
|
||||
Self::from(value.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const N: usize> From<&'a [$ty; N]> for VertexIndices<'a> {
|
||||
fn from(value: &'a [$ty; N]) -> Self {
|
||||
Self::from(value.as_slice())
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_into_vertex_indices_forward!(u8 u16 u32 i32 [u8; 3] [u16; 3] [u32; 3] [i32; 3]);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RenderGeometryTextureParams<'a, TexCoordVertex> {
|
||||
#[cfg(not(feature = "unsafe_textures"))]
|
||||
pub texture: &'a Texture<'a>,
|
||||
#[cfg(feature = "unsafe_textures")]
|
||||
pub texture: &'a Texture,
|
||||
pub tex_coords: &'a [TexCoordVertex],
|
||||
pub tex_coord_offset: usize,
|
||||
}
|
||||
|
||||
impl<T: RenderTarget> Canvas<T> {
|
||||
/// Render triangles, optionally using a texture.
|
||||
///
|
||||
/// If you have vertices in a different format, [`Canvas::render_geometry_raw`] (the advanced
|
||||
/// version of this function) might allow you to do the same as this function without having to
|
||||
/// convert the parameters to use [`Vertex`].
|
||||
///
|
||||
/// `indices` can be of any of the following:
|
||||
/// - [`VertexIndices::Sequential`]: Vertices are rendered in sequential order. The number of
|
||||
/// *vertices* must be divisible by 3, when using a list of indices, the number of *indices*
|
||||
/// must be divisible by 3. SDL will check this and return an error if this is not the case.
|
||||
/// - `&[u8]`: Indices of the type [`u8`], note that this only works when using [`u8::MAX`]
|
||||
/// (255) or less vertices.
|
||||
/// - `&[u16]`: Indices of the type [`u16`], note that this only works when using [`u16::MAX`]
|
||||
/// (65535) or less vertices.
|
||||
/// - `&[u32]` or `&[i32]`: Indices of the type [`i32`], possibly represented using [`u32`].
|
||||
/// Note that regardless of the type, indices must be positive and fit in [`i32`].
|
||||
/// - `&[[u8; 3]]`, `&[[u16; 3]]`, `&[[u32; 3]]` or `&[[i32; 3]]`, which work like `&[u8]`,
|
||||
/// `&[u16]`, `&[u32]` or `&[i32]` but ensure at compile time that the number of indices is
|
||||
/// divisible by 3.
|
||||
/// - Any other (possibly user defined) type that implements
|
||||
/// [`Into<VertexIndices<'_>>`][VertexIndices#trait-implementations].
|
||||
#[doc(alias = "SDL_RenderGeometry")]
|
||||
pub fn render_geometry<'a>(
|
||||
&mut self,
|
||||
vertices: &[Vertex],
|
||||
texture: Option<&Texture>,
|
||||
indices: impl Into<VertexIndices<'a>>,
|
||||
) -> Result<(), String> {
|
||||
unsafe {
|
||||
self.render_geometry_raw(
|
||||
vertices,
|
||||
mem::offset_of!(Vertex, position),
|
||||
vertices,
|
||||
mem::offset_of!(Vertex, color),
|
||||
texture.map(|texture| RenderGeometryTextureParams {
|
||||
texture,
|
||||
tex_coords: vertices,
|
||||
tex_coord_offset: mem::offset_of!(Vertex, tex_coord),
|
||||
}),
|
||||
indices,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render triangles, optionally using a texture.
|
||||
///
|
||||
/// In the documentation of this function field names of [`RenderGeometryTextureParams`] are
|
||||
/// used as if they are parameters. When they are used it only applies when a texture is used
|
||||
/// (`texture_params` is [`Some(_)`][Some]).
|
||||
///
|
||||
/// Vertices may be passed as up to 3 separate lists. Elements of the lists may contain
|
||||
/// anything, but must contain the valid values at the given offset (in bytes).
|
||||
/// Using [`offset_of`][core::mem::offset_of] is advised to get the right offset of a field,
|
||||
/// especially when the element type does not have a stable layout.
|
||||
///
|
||||
/// See the documentation of [`Canvas::render_geometry`] for usage of the `indices` parameter.
|
||||
///
|
||||
/// When a texture is not used, the vertex type must still be specified (because of a Rust
|
||||
/// limitation, more info [here](https://github.com/rust-lang/rust/issues/36887)). This can be
|
||||
/// done with turbofish syntax after `None` like so: `None::<RenderGeometryTextureParams<()>>`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// It must be sound to [create references to](core::ptr#pointer-to-reference-conversion):
|
||||
/// - `positions` offset by `position_offset` bytes, of type [`FPoint`] (or `[f32; 2]`),
|
||||
/// - `colors` offset by `color_offset` bytes, of type [`Color`][pixels::Color] (or `[u8; 4]`)
|
||||
/// - `tex_coords` offset by `tex_coord_offset` bytes, of type [`FPoint`] (or `[f32; 2]`).
|
||||
///
|
||||
/// For the above to hold, make sure that the hypothetical references are properly aligned and
|
||||
/// point to valid values ([`f32`] and [`u8`] allow any bit pattern, but not
|
||||
/// uninitialized/padding bytes).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The following conditions are checked. All must hold or the function panics.
|
||||
///
|
||||
/// Vertices are passed as separate lists, all lists must have the same length.
|
||||
/// - `positions.len() == colors.len()`
|
||||
/// - `positions.len() == tex_coords.len()`
|
||||
///
|
||||
/// Offsets must be correct.
|
||||
/// - `position_offset + size_of::<FPoint>() <= size_of::<PosVertex>()`: an [`FPoint`] must fit
|
||||
/// in `PosVertex` at `position_offset` bytes from the start.
|
||||
/// - `color_offset + size_of::<Color>() <= size_of::<ColorVertex>()`: a
|
||||
/// [`Color`][pixels::Color] must fit in `ColorVertex` at `color_offset` bytes from the
|
||||
/// start.
|
||||
/// - `tex_coord_offset + size_of::<FPoint>() <= size_of::<TexCoordVertex>()`: an [`FPoint`]
|
||||
/// must fit in `TexCoordVertex` at `tex_coord_offset` bytes from the start.
|
||||
///
|
||||
/// Various sizes must fit in a [C `int`][c_int].
|
||||
/// - `positions.len() <= c_int::MAX`
|
||||
/// - `size_of::<PosVertex>() <= c_int::MAX`
|
||||
/// - `size_of::<ColorVertex>() <= c_int::MAX`
|
||||
/// - `size_of::<TexCoordVertex>() <= c_int::MAX`
|
||||
#[inline]
|
||||
#[doc(alias = "SDL_RenderGeometryRaw")]
|
||||
pub unsafe fn render_geometry_raw<'a, PosVertex, ColorVertex, TexCoordVertex>(
|
||||
&mut self,
|
||||
positions: &[PosVertex],
|
||||
position_offset: usize,
|
||||
colors: &[ColorVertex],
|
||||
color_offset: usize,
|
||||
texture_params: Option<RenderGeometryTextureParams<TexCoordVertex>>,
|
||||
indices: impl Into<VertexIndices<'a>>,
|
||||
) -> Result<(), String> {
|
||||
let num_vertices = positions.len();
|
||||
assert_eq!(num_vertices, colors.len());
|
||||
|
||||
assert!(position_offset + size_of::<FPoint>() <= size_of::<PosVertex>());
|
||||
assert!(color_offset + size_of::<pixels::Color>() <= size_of::<ColorVertex>());
|
||||
let (texture, uv, uv_stride) = if let Some(texture_params) = texture_params {
|
||||
assert_eq!(num_vertices, texture_params.tex_coords.len());
|
||||
assert!(
|
||||
texture_params.tex_coord_offset + size_of::<FPoint>()
|
||||
<= size_of::<TexCoordVertex>(),
|
||||
);
|
||||
|
||||
(
|
||||
texture_params.texture.raw,
|
||||
texture_params
|
||||
.tex_coords
|
||||
.as_ptr()
|
||||
.cast::<f32>()
|
||||
.byte_offset(texture_params.tex_coord_offset.try_into().unwrap()),
|
||||
size_of::<TexCoordVertex>().try_into().unwrap(),
|
||||
)
|
||||
} else {
|
||||
(ptr::null_mut(), ptr::null(), 0)
|
||||
};
|
||||
|
||||
let xy = positions
|
||||
.as_ptr()
|
||||
.cast::<f32>()
|
||||
.byte_offset(position_offset.try_into().unwrap());
|
||||
let xy_stride = size_of::<PosVertex>().try_into().unwrap();
|
||||
|
||||
let color = colors
|
||||
.as_ptr()
|
||||
.cast::<sys::SDL_Color>()
|
||||
.byte_offset(color_offset.try_into().unwrap());
|
||||
let color_stride = size_of::<ColorVertex>().try_into().unwrap();
|
||||
|
||||
let (indices, num_indices, size_indices) = indices.into().into_raw();
|
||||
|
||||
let ret = unsafe {
|
||||
sys::SDL_RenderGeometryRaw(
|
||||
self.context.raw,
|
||||
texture,
|
||||
xy,
|
||||
xy_stride,
|
||||
color,
|
||||
color_stride,
|
||||
uv,
|
||||
uv_stride,
|
||||
num_vertices.try_into().unwrap(),
|
||||
indices,
|
||||
num_indices,
|
||||
size_indices,
|
||||
)
|
||||
};
|
||||
|
||||
if ret == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(get_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct TextureQuery {
|
||||
pub format: pixels::PixelFormatEnum,
|
||||
|
||||
Reference in New Issue
Block a user