add offset / scrolling + clipboard support
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
mod align;
|
||||
mod image;
|
||||
mod offset;
|
||||
mod pad;
|
||||
mod rect;
|
||||
mod scroll;
|
||||
mod sense;
|
||||
mod sized;
|
||||
mod span;
|
||||
@@ -12,8 +14,10 @@ mod trait_fns;
|
||||
|
||||
pub use align::*;
|
||||
pub use image::*;
|
||||
pub use offset::*;
|
||||
pub use pad::*;
|
||||
pub use rect::*;
|
||||
pub use scroll::*;
|
||||
pub use sense::*;
|
||||
pub use sized::*;
|
||||
pub use span::*;
|
||||
|
||||
17
src/core/offset.rs
Normal file
17
src/core/offset.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Offset {
|
||||
pub inner: WidgetId,
|
||||
pub amt: UiVec2,
|
||||
}
|
||||
|
||||
impl Widget for Offset {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let region = UiRegion::full().offset(self.amt);
|
||||
painter.widget_within(&self.inner, region);
|
||||
}
|
||||
|
||||
fn desired_size(&mut self, ctx: &mut SizeCtx) -> UiVec2 {
|
||||
ctx.size(&self.inner)
|
||||
}
|
||||
}
|
||||
@@ -31,10 +31,10 @@ impl Widget for Padded {
|
||||
}
|
||||
|
||||
pub struct Padding {
|
||||
left: f32,
|
||||
right: f32,
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
pub left: f32,
|
||||
pub right: f32,
|
||||
pub top: f32,
|
||||
pub bottom: f32,
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
|
||||
19
src/core/scroll.rs
Normal file
19
src/core/scroll.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Scroll {
|
||||
pub inner: Offset,
|
||||
pub size: UiVec2,
|
||||
}
|
||||
|
||||
impl Widget for Scroll {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
self.inner.draw(painter);
|
||||
}
|
||||
fn desired_size(&mut self, _: &mut SizeCtx) -> UiVec2 {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollModule {
|
||||
|
||||
}
|
||||
@@ -29,6 +29,7 @@ pub enum Sense {
|
||||
HoverStart,
|
||||
Hovering,
|
||||
HoverEnd,
|
||||
Scroll,
|
||||
}
|
||||
|
||||
pub struct Senses(Vec<Sense>);
|
||||
@@ -47,6 +48,7 @@ pub struct CursorState {
|
||||
pub pos: Vec2,
|
||||
pub exists: bool,
|
||||
pub buttons: CursorButtons,
|
||||
pub scroll_delta: Vec2,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
@@ -72,6 +74,13 @@ impl CursorButtons {
|
||||
}
|
||||
}
|
||||
|
||||
impl CursorState {
|
||||
pub fn end_frame(&mut self) {
|
||||
self.buttons.end_frame();
|
||||
self.scroll_delta = Vec2::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub enum ActivationState {
|
||||
Start,
|
||||
@@ -81,29 +90,27 @@ pub enum ActivationState {
|
||||
Off,
|
||||
}
|
||||
|
||||
pub struct Sensor<Ctx> {
|
||||
pub struct Sensor<Ctx, Data> {
|
||||
pub senses: Senses,
|
||||
pub f: Rc<dyn SenseFn<Ctx>>,
|
||||
pub f: Rc<dyn EventFn<Ctx, Data>>,
|
||||
}
|
||||
|
||||
pub type SensorMap<Ctx> = HashMap<Id, SensorGroup<Ctx>>;
|
||||
pub type SensorMap<Ctx, Data> = HashMap<Id, SensorGroup<Ctx, Data>>;
|
||||
pub type SenseShape = UiRegion;
|
||||
pub struct SensorGroup<Ctx> {
|
||||
pub struct SensorGroup<Ctx, Data> {
|
||||
pub hover: ActivationState,
|
||||
pub sensors: Vec<Sensor<Ctx>>,
|
||||
pub sensors: Vec<Sensor<Ctx, Data>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SenseData {
|
||||
pub struct CursorData {
|
||||
pub cursor: Vec2,
|
||||
pub size: Vec2,
|
||||
pub scroll_delta: Vec2,
|
||||
}
|
||||
|
||||
pub trait SenseFn<Ctx>: Fn(&mut Ctx, SenseData) + 'static {}
|
||||
impl<F: Fn(&mut Ctx, SenseData) + 'static, Ctx> SenseFn<Ctx> for F {}
|
||||
|
||||
pub struct SensorModule<Ctx> {
|
||||
map: SensorMap<Ctx>,
|
||||
map: SensorMap<Ctx, CursorData>,
|
||||
active: HashMap<usize, HashMap<Id, SenseShape>>,
|
||||
}
|
||||
|
||||
@@ -125,6 +132,9 @@ impl<Ctx: 'static> UiModule for SensorModule<Ctx> {
|
||||
|
||||
fn on_remove(&mut self, id: &Id) {
|
||||
self.map.remove(id);
|
||||
for layer in self.active.values_mut() {
|
||||
layer.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_move(&mut self, inst: &WidgetInstance) {
|
||||
@@ -165,7 +175,7 @@ impl<Ctx: UiCtx + 'static> SensorModule<Ctx> {
|
||||
let Some(list) = module.active.get_mut(&i) else {
|
||||
continue;
|
||||
};
|
||||
let mut ran = false;
|
||||
let mut sensed = false;
|
||||
for (id, shape) in list.iter() {
|
||||
let group = module.map.get_mut(id).unwrap();
|
||||
let region = shape.to_screen(window_size);
|
||||
@@ -174,19 +184,20 @@ impl<Ctx: UiCtx + 'static> SensorModule<Ctx> {
|
||||
if group.hover == ActivationState::Off {
|
||||
continue;
|
||||
}
|
||||
sensed = true;
|
||||
|
||||
for sensor in &mut group.sensors {
|
||||
if should_run(&sensor.senses, &cursor.buttons, group.hover) {
|
||||
ran = true;
|
||||
let sctx = SenseData {
|
||||
if should_run(&sensor.senses, cursor, group.hover) {
|
||||
let data = CursorData {
|
||||
cursor: cursor.pos - region.top_left,
|
||||
size: region.bot_right - region.top_left,
|
||||
scroll_delta: cursor.scroll_delta,
|
||||
};
|
||||
(sensor.f)(ctx, sctx);
|
||||
(sensor.f)(ctx, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ran {
|
||||
if sensed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -198,15 +209,16 @@ impl<Ctx: UiCtx + 'static> SensorModule<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_run(senses: &Senses, cursor: &CursorButtons, hover: ActivationState) -> bool {
|
||||
pub fn should_run(senses: &Senses, cursor: &CursorState, hover: ActivationState) -> bool {
|
||||
for sense in senses.iter() {
|
||||
if match sense {
|
||||
Sense::PressStart(button) => cursor.select(button).is_start(),
|
||||
Sense::Pressing(button) => cursor.select(button).is_on(),
|
||||
Sense::PressEnd(button) => cursor.select(button).is_end(),
|
||||
Sense::PressStart(button) => cursor.buttons.select(button).is_start(),
|
||||
Sense::Pressing(button) => cursor.buttons.select(button).is_on(),
|
||||
Sense::PressEnd(button) => cursor.buttons.select(button).is_end(),
|
||||
Sense::HoverStart => hover.is_start(),
|
||||
Sense::Hovering => hover.is_on(),
|
||||
Sense::HoverEnd => hover.is_end(),
|
||||
Sense::Scroll => cursor.scroll_delta != Vec2::ZERO,
|
||||
} {
|
||||
return true;
|
||||
}
|
||||
@@ -259,12 +271,12 @@ impl ActivationState {
|
||||
|
||||
impl Event for Senses {
|
||||
type Module<Ctx: 'static> = SensorModule<Ctx>;
|
||||
type Data = SenseData;
|
||||
type Data = CursorData;
|
||||
}
|
||||
|
||||
impl Event for Sense {
|
||||
type Module<Ctx: 'static> = SensorModule<Ctx>;
|
||||
type Data = SenseData;
|
||||
type Data = CursorData;
|
||||
}
|
||||
|
||||
impl<E: Event<Data = <Senses as Event>::Data> + Into<Senses>, Ctx: 'static> EventModule<E, Ctx>
|
||||
@@ -292,7 +304,7 @@ impl<E: Event<Data = <Senses as Event>::Data> + Into<Senses>, Ctx: 'static> Even
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Some(move |ctx: &mut Ctx, data: SenseData| {
|
||||
Some(move |ctx: &mut Ctx, data: CursorData| {
|
||||
for f in &fs {
|
||||
f(ctx, data.clone());
|
||||
}
|
||||
@@ -303,7 +315,7 @@ impl<E: Event<Data = <Senses as Event>::Data> + Into<Senses>, Ctx: 'static> Even
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx> Default for SensorGroup<Ctx> {
|
||||
impl<Ctx, Data> Default for SensorGroup<Ctx, Data> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hover: Default::default(),
|
||||
|
||||
@@ -43,7 +43,7 @@ impl Widget for TextEdit {
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
UiRegion::from_size_align(size, Align::TopLeft)
|
||||
.shifted(offset)
|
||||
.offset(offset)
|
||||
.within(®ion),
|
||||
);
|
||||
} else {
|
||||
@@ -247,7 +247,13 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
_ => return TextInputResult::Unused,
|
||||
},
|
||||
Key::Character(text) => self.insert(text),
|
||||
Key::Character(text) => {
|
||||
if modifiers.control && text == "v" {
|
||||
return TextInputResult::Paste;
|
||||
} else {
|
||||
self.insert(text)
|
||||
}
|
||||
}
|
||||
_ => return TextInputResult::Unused,
|
||||
}
|
||||
TextInputResult::Used
|
||||
@@ -272,6 +278,7 @@ pub enum TextInputResult {
|
||||
Unused,
|
||||
Unfocus,
|
||||
Submit,
|
||||
Paste,
|
||||
}
|
||||
|
||||
impl TextInputResult {
|
||||
|
||||
@@ -7,6 +7,7 @@ pub trait CoreWidget<W, Tag> {
|
||||
fn center(self) -> impl WidgetFn<Aligned>;
|
||||
fn label(self, label: impl Into<String>) -> impl WidgetIdFn<W>;
|
||||
fn sized(self, size: impl Into<Vec2>) -> impl WidgetFn<Sized>;
|
||||
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Offset>;
|
||||
}
|
||||
|
||||
impl<W: WidgetLike<Tag>, Tag> CoreWidget<W::Widget, Tag> for W {
|
||||
@@ -42,6 +43,13 @@ impl<W: WidgetLike<Tag>, Tag> CoreWidget<W::Widget, Tag> for W {
|
||||
size: size.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Offset> {
|
||||
move |ui| Offset {
|
||||
inner: self.add(ui).any(),
|
||||
amt: amt.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CoreWidgetArr<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> {
|
||||
|
||||
@@ -85,7 +85,7 @@ pub enum Align {
|
||||
}
|
||||
|
||||
impl Align {
|
||||
pub const fn anchor(&self) -> Vec2 {
|
||||
pub const fn rel(&self) -> Vec2 {
|
||||
match self {
|
||||
Self::TopLeft => vec2(0.0, 0.0),
|
||||
Self::Top => vec2(0.5, 0.0),
|
||||
|
||||
@@ -131,7 +131,9 @@ impl<'a> PainterCtx<'a> {
|
||||
if active.region == region {
|
||||
return;
|
||||
} else if active.region.size() == region.size() {
|
||||
self.mov(id, region);
|
||||
// TODO: epsilon?
|
||||
let from = active.region;
|
||||
self.mov(id, from, region);
|
||||
return;
|
||||
}
|
||||
let active = self.remove(id).unwrap();
|
||||
@@ -185,22 +187,21 @@ impl<'a> PainterCtx<'a> {
|
||||
self.active.insert(id, instance);
|
||||
}
|
||||
|
||||
fn mov(&mut self, id: Id, to: UiRegion) {
|
||||
fn mov(&mut self, id: Id, from: UiRegion, to: UiRegion) {
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
// children will not be changed, so this technically should not be needed
|
||||
// probably need unsafe
|
||||
let from = active.region;
|
||||
for h in &active.primitives {
|
||||
let region = self.layers[h.layer].primitives.region_mut(h);
|
||||
*region = region.outside(&from).within(&to);
|
||||
}
|
||||
active.region = to;
|
||||
active.region = active.region.outside(&from).within(&to);
|
||||
for m in self.modules.iter_mut() {
|
||||
m.on_move(active);
|
||||
}
|
||||
let children = active.children.clone();
|
||||
for child in children {
|
||||
self.mov(child, to);
|
||||
self.mov(child, from, to);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,23 +11,20 @@ pub struct UiVec2 {
|
||||
}
|
||||
|
||||
impl UiVec2 {
|
||||
pub const ZERO: Self = Self {
|
||||
rel: Vec2::ZERO,
|
||||
abs: Vec2::ZERO,
|
||||
};
|
||||
|
||||
/// expands this position into a sized region centered at self
|
||||
pub fn expand(&self, size: impl Into<Vec2>) -> UiRegion {
|
||||
let size = size.into();
|
||||
UiRegion {
|
||||
top_left: self.shifted(-size / 2.0),
|
||||
bot_right: self.shifted(size / 2.0),
|
||||
top_left: self.offset(-size / 2.0),
|
||||
bot_right: self.offset(size / 2.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn anchor(anchor: Vec2) -> Self {
|
||||
Self::rel(anchor)
|
||||
}
|
||||
|
||||
pub const fn offset(offset: Vec2) -> Self {
|
||||
Self::abs(offset)
|
||||
}
|
||||
|
||||
pub const fn abs(abs: Vec2) -> Self {
|
||||
Self {
|
||||
rel: Vec2::ZERO,
|
||||
@@ -42,11 +39,12 @@ impl UiVec2 {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn shift(&mut self, offset: impl const Into<Vec2>) {
|
||||
self.abs += offset.into();
|
||||
pub const fn shift(&mut self, offset: impl const Into<UiVec2>) {
|
||||
let offset = offset.into();
|
||||
*self += offset;
|
||||
}
|
||||
|
||||
pub const fn shifted(mut self, offset: Vec2) -> Self {
|
||||
pub const fn offset(mut self, offset: impl const Into<UiVec2>) -> Self {
|
||||
self.shift(offset);
|
||||
self
|
||||
}
|
||||
@@ -118,7 +116,7 @@ impl_op!(UiVec2 Sub sub; rel abs);
|
||||
|
||||
impl const From<Align> for UiVec2 {
|
||||
fn from(align: Align) -> Self {
|
||||
Self::anchor(align.anchor())
|
||||
Self::rel(align.rel())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +126,12 @@ impl Align {
|
||||
}
|
||||
}
|
||||
|
||||
impl const From<Vec2> for UiVec2 {
|
||||
fn from(abs: Vec2) -> Self {
|
||||
Self::abs(abs)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct UiScalar {
|
||||
pub rel: f32,
|
||||
@@ -198,10 +202,10 @@ impl UiRegion {
|
||||
bot_right: Align::BotRight.into(),
|
||||
}
|
||||
}
|
||||
pub fn anchor(anchor: Vec2) -> Self {
|
||||
pub fn rel(anchor: Vec2) -> Self {
|
||||
Self {
|
||||
top_left: UiVec2::anchor(anchor),
|
||||
bot_right: UiVec2::anchor(anchor),
|
||||
top_left: UiVec2::rel(anchor),
|
||||
bot_right: UiVec2::rel(anchor),
|
||||
}
|
||||
}
|
||||
pub fn within(&self, parent: &Self) -> Self {
|
||||
@@ -229,13 +233,13 @@ impl UiRegion {
|
||||
std::mem::swap(&mut self.top_left, &mut self.bot_right);
|
||||
}
|
||||
|
||||
pub fn shift(&mut self, offset: impl Into<Vec2>) {
|
||||
pub fn shift(&mut self, offset: impl Into<UiVec2>) {
|
||||
let offset = offset.into();
|
||||
self.top_left.shift(offset);
|
||||
self.bot_right.shift(offset);
|
||||
}
|
||||
|
||||
pub fn shifted(mut self, offset: impl Into<Vec2>) -> Self {
|
||||
pub fn offset(mut self, offset: impl Into<UiVec2>) -> Self {
|
||||
self.shift(offset);
|
||||
self
|
||||
}
|
||||
@@ -265,9 +269,9 @@ impl UiRegion {
|
||||
|
||||
pub fn from_size_align(size: Vec2, align: Align) -> Self {
|
||||
let mut top_left = UiVec2::from(align);
|
||||
top_left.abs -= size * align.anchor();
|
||||
top_left.abs -= size * align.rel();
|
||||
let mut bot_right = UiVec2::from(align);
|
||||
bot_right.abs += size * (Vec2::ONE - align.anchor());
|
||||
bot_right.abs += size * (Vec2::ONE - align.rel());
|
||||
Self {
|
||||
top_left,
|
||||
bot_right,
|
||||
@@ -276,11 +280,11 @@ impl UiRegion {
|
||||
|
||||
pub fn from_ui_size_align(size: UiVec2, align: Align) -> Self {
|
||||
let mut top_left = UiVec2::from(align);
|
||||
top_left.abs -= size.abs * align.anchor();
|
||||
top_left.rel -= size.rel * align.anchor();
|
||||
top_left.abs -= size.abs * align.rel();
|
||||
top_left.rel -= size.rel * align.rel();
|
||||
let mut bot_right = UiVec2::from(align);
|
||||
bot_right.abs += size.abs * (Vec2::ONE - align.anchor());
|
||||
bot_right.rel += size.rel * (Vec2::ONE - align.anchor());
|
||||
bot_right.abs += size.abs * (Vec2::ONE - align.rel());
|
||||
bot_right.rel += size.rel * (Vec2::ONE - align.rel());
|
||||
Self {
|
||||
top_left,
|
||||
bot_right,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#![feature(const_trait_impl)]
|
||||
#![feature(const_convert)]
|
||||
#![feature(map_try_insert)]
|
||||
#![feature(trait_alias)]
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(const_cmp)]
|
||||
|
||||
@@ -3,7 +3,7 @@ use ui::{
|
||||
layout::Vec2,
|
||||
};
|
||||
use winit::{
|
||||
event::{MouseButton, WindowEvent},
|
||||
event::{MouseButton, MouseScrollDelta, WindowEvent},
|
||||
keyboard::{Key, NamedKey},
|
||||
};
|
||||
|
||||
@@ -32,6 +32,13 @@ impl Input {
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
let delta = match *delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y),
|
||||
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
|
||||
};
|
||||
self.cursor.scroll_delta = delta;
|
||||
}
|
||||
WindowEvent::CursorLeft { .. } => {
|
||||
self.cursor.exists = false;
|
||||
self.modifiers.clear();
|
||||
@@ -56,7 +63,7 @@ impl Input {
|
||||
}
|
||||
|
||||
pub fn end_frame(&mut self) {
|
||||
self.cursor.buttons.end_frame();
|
||||
self.cursor.end_frame();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use app::App;
|
||||
use arboard::Clipboard;
|
||||
use cosmic_text::Family;
|
||||
use render::Renderer;
|
||||
use ui::prelude::*;
|
||||
@@ -22,6 +23,7 @@ pub struct Client {
|
||||
ui: Ui,
|
||||
info: WidgetId<Text>,
|
||||
focus: Option<WidgetId<TextEdit>>,
|
||||
clipboard: Clipboard,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
@@ -127,6 +129,15 @@ impl Client {
|
||||
.add_static(&mut ui);
|
||||
|
||||
let texts = Span::empty(Dir::DOWN).add_static(&mut ui);
|
||||
let msg_area = (
|
||||
Rect::new(Color::SKY),
|
||||
texts
|
||||
.offset(UiVec2::ZERO)
|
||||
.edit_on(Sense::Scroll, |w, data| {
|
||||
w.amt += UiVec2::abs(data.scroll_delta * 50.0);
|
||||
}),
|
||||
)
|
||||
.stack();
|
||||
let add_text = text_edit("add")
|
||||
.text_align(Align::Left)
|
||||
.size(30)
|
||||
@@ -151,7 +162,7 @@ impl Client {
|
||||
})
|
||||
.add(&mut ui);
|
||||
let text_edit_scroll = (
|
||||
(Rect::new(Color::SKY), texts).stack(),
|
||||
msg_area,
|
||||
(
|
||||
Rect::new(Color::WHITE.darker(0.9)),
|
||||
(
|
||||
@@ -217,6 +228,7 @@ impl Client {
|
||||
ui,
|
||||
info,
|
||||
focus: None,
|
||||
clipboard: Clipboard::new().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,14 +261,20 @@ impl Client {
|
||||
if let Some(sel) = &self.focus
|
||||
&& event.state.is_pressed()
|
||||
{
|
||||
match self.ui.text(sel).apply_event(&event, &self.input.modifiers) {
|
||||
let mut text = self.ui.text(sel);
|
||||
match text.apply_event(&event, &self.input.modifiers) {
|
||||
TextInputResult::Unfocus => {
|
||||
self.focus = None;
|
||||
}
|
||||
TextInputResult::Submit => {
|
||||
self.run_event(&sel.clone(), Submit, ());
|
||||
}
|
||||
_ => (),
|
||||
TextInputResult::Paste => {
|
||||
if let Ok(t) = self.clipboard.get_text() {
|
||||
text.insert(&t);
|
||||
}
|
||||
}
|
||||
TextInputResult::Unused | TextInputResult::Used => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user