1
0
Fork 0
forked from wry/wry

seat: implement input methods

This commit is contained in:
Julian Orth 2024-04-14 20:22:57 +02:00
parent 5e2cdef388
commit daf52299db
44 changed files with 2165 additions and 75 deletions

View file

@ -15,7 +15,7 @@ use {
test_xdg_activation::TestXdgActivation, test_xdg_base::TestXdgWmBase,
},
test_transport::TestTransport,
test_utils::test_window::TestWindow,
test_utils::{test_surface_ext::TestSurfaceExt, test_window::TestWindow},
testrun::TestRun,
},
theme::Color,
@ -124,21 +124,24 @@ impl TestClient {
Ok(())
}
pub async fn create_window(&self) -> Result<Rc<TestWindow>, TestError> {
pub async fn create_surface_ext(&self) -> Result<TestSurfaceExt, TestError> {
let surface = self.comp.create_surface().await?;
let viewport = self.viewporter.get_viewport(&surface)?;
let xdg = self.xdg.create_xdg_surface(surface.id).await?;
let tl = xdg.create_toplevel().await?;
surface.commit()?;
self.sync().await;
Ok(Rc::new(TestWindow {
Ok(TestSurfaceExt {
surface,
spbm: self.spbm.clone(),
viewport,
xdg,
tl,
color: Cell::new(Color::SOLID_BLACK),
}))
})
}
pub async fn create_window(&self) -> Result<Rc<TestWindow>, TestError> {
let surface = self.create_surface_ext().await?;
let xdg = self.xdg.create_xdg_surface(surface.surface.id).await?;
let tl = xdg.create_toplevel().await?;
surface.surface.commit()?;
self.sync().await;
Ok(Rc::new(TestWindow { surface, xdg, tl }))
}
}

View file

@ -20,6 +20,10 @@ pub mod test_dmabuf;
pub mod test_dmabuf_feedback;
pub mod test_ext_foreign_toplevel_handle;
pub mod test_ext_foreign_toplevel_list;
pub mod test_input_method;
pub mod test_input_method_keyboard_grab;
pub mod test_input_method_manager;
pub mod test_input_popup_surface;
pub mod test_jay_compositor;
pub mod test_keyboard;
pub mod test_pointer;
@ -37,6 +41,8 @@ pub mod test_surface;
pub mod test_syncobj_manager;
pub mod test_syncobj_surface;
pub mod test_syncobj_timeline;
pub mod test_text_input;
pub mod test_text_input_manager;
pub mod test_toplevel_drag;
pub mod test_toplevel_drag_manager;
pub mod test_viewport;

View file

@ -0,0 +1,119 @@
use {
crate::{
it::{
test_error::{TestError, TestResult},
test_ifs::{
test_input_method_keyboard_grab::TestInputMethodKeyboardGrab,
test_input_popup_surface::TestInputPopupSurface, test_surface::TestSurface,
},
test_object::TestObject,
test_transport::TestTransport,
test_utils::test_expected_event::TEEH,
testrun::ParseFull,
},
utils::{buffd::MsgParser, numcell::NumCell},
wire::{zwp_input_method_v2::*, ZwpInputMethodV2Id},
},
std::{cell::Cell, rc::Rc},
};
pub struct TestInputMethod {
pub id: ZwpInputMethodV2Id,
pub tran: Rc<TestTransport>,
pub destroyed: Cell<bool>,
pub activate: TEEH<bool>,
pub done: TEEH<()>,
pub done_received: NumCell<u32>,
}
impl TestInputMethod {
pub fn commit_string(&self, s: &str) -> TestResult {
self.tran.send(CommitString {
self_id: self.id,
text: s,
})
}
pub fn commit(&self) -> TestResult {
self.tran.send(Commit {
self_id: self.id,
serial: self.done_received.get(),
})
}
#[allow(dead_code)]
pub fn grab(&self) -> TestResult<Rc<TestInputMethodKeyboardGrab>> {
let obj = Rc::new(TestInputMethodKeyboardGrab {
id: self.tran.id(),
tran: self.tran.clone(),
destroyed: Cell::new(false),
keymap: Rc::new(Default::default()),
key: Rc::new(Default::default()),
modifiers: Rc::new(Default::default()),
repeat_info: Rc::new(Default::default()),
});
self.tran.add_obj(obj.clone())?;
self.tran.send(GrabKeyboard {
self_id: self.id,
keyboard: obj.id,
})?;
Ok(obj)
}
pub fn get_popup(&self, surface: &TestSurface) -> TestResult<Rc<TestInputPopupSurface>> {
let obj = Rc::new(TestInputPopupSurface {
id: self.tran.id(),
tran: self.tran.clone(),
destroyed: Cell::new(false),
});
self.tran.add_obj(obj.clone())?;
self.tran.send(GetInputPopupSurface {
self_id: self.id,
id: obj.id,
surface: surface.id,
})?;
Ok(obj)
}
pub fn destroy(&self) -> Result<(), TestError> {
if !self.destroyed.replace(true) {
self.tran.send(Destroy { self_id: self.id })?;
}
Ok(())
}
fn handle_activate(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let _ev = Activate::parse_full(parser)?;
self.activate.push(true);
Ok(())
}
fn handle_deactivate(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let _ev = Deactivate::parse_full(parser)?;
self.activate.push(false);
Ok(())
}
fn handle_done(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let _ev = Done::parse_full(parser)?;
self.done.push(());
self.done_received.fetch_add(1);
Ok(())
}
}
impl Drop for TestInputMethod {
fn drop(&mut self) {
let _ = self.destroy();
}
}
test_object! {
TestInputMethod, ZwpInputMethodV2;
ACTIVATE => handle_activate,
DEACTIVATE => handle_deactivate,
DONE => handle_done,
}
impl TestObject for TestInputMethod {}

View file

@ -0,0 +1,71 @@
use {
crate::{
it::{
test_error::TestError, test_object::TestObject, test_transport::TestTransport,
test_utils::test_expected_event::TEEH, testrun::ParseFull,
},
utils::buffd::MsgParser,
wire::{zwp_input_method_keyboard_grab_v2::*, ZwpInputMethodKeyboardGrabV2Id},
},
std::{cell::Cell, rc::Rc},
};
pub struct TestInputMethodKeyboardGrab {
pub id: ZwpInputMethodKeyboardGrabV2Id,
pub tran: Rc<TestTransport>,
pub destroyed: Cell<bool>,
pub keymap: TEEH<Keymap>,
pub key: TEEH<Key>,
pub modifiers: TEEH<Modifiers>,
pub repeat_info: TEEH<RepeatInfo>,
}
impl TestInputMethodKeyboardGrab {
pub fn destroy(&self) -> Result<(), TestError> {
if !self.destroyed.replace(true) {
self.tran.send(Release { self_id: self.id })?;
}
Ok(())
}
fn handle_keymap(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let ev = Keymap::parse_full(parser)?;
self.keymap.push(ev);
Ok(())
}
fn handle_key(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let ev = Key::parse_full(parser)?;
self.key.push(ev);
Ok(())
}
fn handle_modifiers(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let ev = Modifiers::parse_full(parser)?;
self.modifiers.push(ev);
Ok(())
}
fn handle_repeat_info(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let ev = RepeatInfo::parse_full(parser)?;
self.repeat_info.push(ev);
Ok(())
}
}
impl Drop for TestInputMethodKeyboardGrab {
fn drop(&mut self) {
let _ = self.destroy();
}
}
test_object! {
TestInputMethodKeyboardGrab, ZwpInputMethodKeyboardGrabV2;
KEYMAP => handle_keymap,
KEY => handle_key,
MODIFIERS => handle_modifiers,
REPEAT_INFO => handle_repeat_info,
}
impl TestObject for TestInputMethodKeyboardGrab {}

View file

@ -0,0 +1,50 @@
use {
crate::{
it::{
test_error::TestResult,
test_ifs::{test_input_method::TestInputMethod, test_seat::TestSeat},
test_object::TestObject,
test_transport::TestTransport,
},
wire::{zwp_input_method_manager_v2::GetInputMethod, ZwpInputMethodManagerV2Id},
},
std::{cell::Cell, rc::Rc},
};
pub struct TestInputMethodManager {
pub id: ZwpInputMethodManagerV2Id,
pub tran: Rc<TestTransport>,
}
impl TestInputMethodManager {
pub fn new(tran: &Rc<TestTransport>) -> Self {
Self {
id: tran.id(),
tran: tran.clone(),
}
}
pub fn get_input_method(&self, seat: &TestSeat) -> TestResult<Rc<TestInputMethod>> {
let obj = Rc::new(TestInputMethod {
id: self.tran.id(),
tran: self.tran.clone(),
destroyed: Cell::new(false),
activate: Rc::new(Default::default()),
done: Rc::new(Default::default()),
done_received: Default::default(),
});
self.tran.add_obj(obj.clone())?;
self.tran.send(GetInputMethod {
self_id: self.id,
seat: seat.id,
input_method: obj.id,
})?;
Ok(obj)
}
}
test_object! {
TestInputMethodManager, ZwpInputMethodManagerV2;
}
impl TestObject for TestInputMethodManager {}

View file

@ -0,0 +1,34 @@
use {
crate::{
it::{test_error::TestError, test_object::TestObject, test_transport::TestTransport},
wire::{zwp_input_popup_surface_v2::*, ZwpInputPopupSurfaceV2Id},
},
std::{cell::Cell, rc::Rc},
};
pub struct TestInputPopupSurface {
pub id: ZwpInputPopupSurfaceV2Id,
pub tran: Rc<TestTransport>,
pub destroyed: Cell<bool>,
}
impl TestInputPopupSurface {
pub fn destroy(&self) -> Result<(), TestError> {
if !self.destroyed.replace(true) {
self.tran.send(Destroy { self_id: self.id })?;
}
Ok(())
}
}
impl Drop for TestInputPopupSurface {
fn drop(&mut self) {
let _ = self.destroy();
}
}
test_object! {
TestInputPopupSurface, ZwpInputPopupSurfaceV2;
}
impl TestObject for TestInputPopupSurface {}

View file

@ -11,9 +11,11 @@ use {
test_data_control_manager::TestDataControlManager,
test_data_device_manager::TestDataDeviceManager, test_dmabuf::TestDmabuf,
test_ext_foreign_toplevel_list::TestExtForeignToplevelList,
test_input_method_manager::TestInputMethodManager,
test_jay_compositor::TestJayCompositor, test_shm::TestShm,
test_single_pixel_buffer_manager::TestSinglePixelBufferManager,
test_subcompositor::TestSubcompositor, test_syncobj_manager::TestSyncobjManager,
test_text_input_manager::TestTextInputManager,
test_toplevel_drag_manager::TestToplevelDragManager,
test_viewporter::TestViewporter,
test_virtual_keyboard_manager::TestVirtualKeyboardManager,
@ -54,6 +56,8 @@ pub struct TestRegistrySingletons {
pub xdg_toplevel_drag_manager_v1: u32,
pub wp_alpha_modifier_v1: u32,
pub zwp_virtual_keyboard_manager_v1: u32,
pub zwp_input_method_manager_v2: u32,
pub zwp_text_input_manager_v3: u32,
}
pub struct TestRegistry {
@ -79,6 +83,8 @@ pub struct TestRegistry {
pub drag_manager: CloneCell<Option<Rc<TestToplevelDragManager>>>,
pub alpha_modifier: CloneCell<Option<Rc<TestAlphaModifier>>>,
pub virtual_keyboard_manager: CloneCell<Option<Rc<TestVirtualKeyboardManager>>>,
pub input_method_manager: CloneCell<Option<Rc<TestInputMethodManager>>>,
pub text_input_manager: CloneCell<Option<Rc<TestTextInputManager>>>,
pub seats: CopyHashMap<GlobalName, Rc<WlSeatGlobal>>,
}
@ -148,6 +154,8 @@ impl TestRegistry {
xdg_toplevel_drag_manager_v1,
wp_alpha_modifier_v1,
zwp_virtual_keyboard_manager_v1,
zwp_input_method_manager_v2,
zwp_text_input_manager_v3,
};
self.singletons.set(Some(singletons.clone()));
Ok(singletons)
@ -249,6 +257,20 @@ impl TestRegistry {
1,
TestVirtualKeyboardManager
);
create_singleton!(
get_input_method_manager,
input_method_manager,
zwp_input_method_manager_v2,
1,
TestInputMethodManager
);
create_singleton!(
get_text_input_manager,
text_input_manager,
zwp_text_input_manager_v3,
1,
TestTextInputManager
);
pub fn bind<O: TestObject>(
&self,

View file

@ -0,0 +1,97 @@
use {
crate::{
it::{
test_error::{TestError, TestResult},
test_object::TestObject,
test_transport::TestTransport,
test_utils::test_expected_event::TEEH,
testrun::ParseFull,
},
utils::buffd::MsgParser,
wire::{zwp_text_input_v3::*, ZwpTextInputV3Id},
},
std::{cell::Cell, rc::Rc},
};
pub struct TestTextInput {
pub id: ZwpTextInputV3Id,
pub tran: Rc<TestTransport>,
pub destroyed: Cell<bool>,
pub enter: TEEH<Enter>,
pub leave: TEEH<Leave>,
pub commit_string: TEEH<String>,
pub done: TEEH<Done>,
}
impl TestTextInput {
pub fn destroy(&self) -> Result<(), TestError> {
if !self.destroyed.replace(true) {
self.tran.send(Destroy { self_id: self.id })?;
}
Ok(())
}
pub fn enable(&self) -> TestResult {
self.tran.send(Enable { self_id: self.id })
}
pub fn disable(&self) -> TestResult {
self.tran.send(Disable { self_id: self.id })
}
pub fn set_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32) -> TestResult {
self.tran.send(SetCursorRectangle {
self_id: self.id,
x,
y,
width,
height,
})
}
pub fn commit(&self) -> TestResult {
self.tran.send(Commit { self_id: self.id })
}
fn handle_enter(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let ev = Enter::parse_full(parser)?;
self.enter.push(ev);
Ok(())
}
fn handle_leave(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let ev = Leave::parse_full(parser)?;
self.leave.push(ev);
Ok(())
}
fn handle_commit_string(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let ev = CommitString::parse_full(parser)?;
self.commit_string
.push(ev.text.unwrap_or_default().to_string());
Ok(())
}
fn handle_done(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> {
let ev = Done::parse_full(parser)?;
self.done.push(ev);
Ok(())
}
}
impl Drop for TestTextInput {
fn drop(&mut self) {
let _ = self.destroy();
}
}
test_object! {
TestTextInput, ZwpTextInputV3;
ENTER => handle_enter,
LEAVE => handle_leave,
COMMIT_STRING => handle_commit_string,
DONE => handle_done,
}
impl TestObject for TestTextInput {}

View file

@ -0,0 +1,51 @@
use {
crate::{
it::{
test_error::TestResult,
test_ifs::{test_seat::TestSeat, test_text_input::TestTextInput},
test_object::TestObject,
test_transport::TestTransport,
},
wire::{zwp_text_input_manager_v3::*, ZwpTextInputManagerV3Id},
},
std::{cell::Cell, rc::Rc},
};
pub struct TestTextInputManager {
pub id: ZwpTextInputManagerV3Id,
pub tran: Rc<TestTransport>,
}
impl TestTextInputManager {
pub fn new(tran: &Rc<TestTransport>) -> Self {
Self {
id: tran.id(),
tran: tran.clone(),
}
}
pub fn get_text_input(&self, seat: &TestSeat) -> TestResult<Rc<TestTextInput>> {
let obj = Rc::new(TestTextInput {
id: self.tran.id(),
tran: self.tran.clone(),
destroyed: Cell::new(false),
enter: Rc::new(Default::default()),
leave: Rc::new(Default::default()),
commit_string: Rc::new(Default::default()),
done: Rc::new(Default::default()),
});
self.tran.add_obj(obj.clone())?;
self.tran.send(GetTextInput {
self_id: self.id,
id: obj.id,
seat: seat.id,
})?;
Ok(obj)
}
}
test_object! {
TestTextInputManager, ZwpTextInputManagerV3;
}
impl TestObject for TestTextInputManager {}

View file

@ -70,6 +70,8 @@ impl TestTransport {
drag_manager: Default::default(),
alpha_modifier: Default::default(),
virtual_keyboard_manager: Default::default(),
input_method_manager: Default::default(),
text_input_manager: Default::default(),
seats: Default::default(),
});
self.send(wl_display::GetRegistry {

View file

@ -3,6 +3,7 @@ pub mod test_expected_event;
pub mod test_object_ext;
pub mod test_ouput_node_ext;
pub mod test_rect_ext;
pub mod test_surface_ext;
pub mod test_toplevel_node_ext;
pub mod test_window;
pub mod test_workspace_node_ext;

View file

@ -0,0 +1,45 @@
use {
crate::{
it::{
test_error::TestError,
test_ifs::{
test_single_pixel_buffer_manager::TestSinglePixelBufferManager,
test_surface::TestSurface, test_viewport::TestViewport,
},
},
theme::Color,
},
std::{cell::Cell, ops::Deref, rc::Rc},
};
pub struct TestSurfaceExt {
pub surface: Rc<TestSurface>,
pub spbm: Rc<TestSinglePixelBufferManager>,
pub viewport: Rc<TestViewport>,
pub color: Cell<Color>,
}
impl Deref for TestSurfaceExt {
type Target = TestSurface;
fn deref(&self) -> &Self::Target {
&self.surface
}
}
impl TestSurfaceExt {
pub async fn map(&self, width: i32, height: i32) -> Result<(), TestError> {
let buffer = self.spbm.create_buffer(self.color.get())?;
self.surface.attach(buffer.id)?;
self.viewport.set_source(0, 0, 1, 1)?;
self.viewport.set_destination(width, height)?;
self.surface.commit()?;
self.surface.tran.sync().await;
Ok(())
}
#[allow(dead_code)]
pub fn set_color(&self, r: u8, g: u8, b: u8, a: u8) {
self.color.set(Color::from_rgba_straight(r, g, b, a));
}
}

View file

@ -1,37 +1,24 @@
use {
crate::{
it::{
test_error::{TestError, TestResult},
test_ifs::{
test_single_pixel_buffer_manager::TestSinglePixelBufferManager,
test_surface::TestSurface, test_viewport::TestViewport,
test_xdg_surface::TestXdgSurface, test_xdg_toplevel::TestXdgToplevel,
},
},
theme::Color,
crate::it::{
test_error::{TestError, TestResult},
test_ifs::{test_xdg_surface::TestXdgSurface, test_xdg_toplevel::TestXdgToplevel},
test_utils::test_surface_ext::TestSurfaceExt,
},
std::{cell::Cell, rc::Rc},
std::rc::Rc,
};
pub struct TestWindow {
pub surface: Rc<TestSurface>,
pub spbm: Rc<TestSinglePixelBufferManager>,
pub viewport: Rc<TestViewport>,
pub surface: TestSurfaceExt,
pub xdg: Rc<TestXdgSurface>,
pub tl: Rc<TestXdgToplevel>,
pub color: Cell<Color>,
}
impl TestWindow {
pub async fn map(&self) -> Result<(), TestError> {
let buffer = self.spbm.create_buffer(self.color.get())?;
self.surface.attach(buffer.id)?;
self.viewport.set_source(0, 0, 1, 1)?;
self.viewport
.set_destination(self.tl.core.width.get(), self.tl.core.height.get())?;
self.xdg.ack_configure(self.xdg.last_serial.get())?;
self.surface.commit()?;
self.surface.tran.sync().await;
self.surface
.map(self.tl.core.width.get(), self.tl.core.height.get())
.await?;
Ok(())
}
@ -40,8 +27,7 @@ impl TestWindow {
self.map().await
}
#[allow(dead_code)]
pub fn set_color(&self, r: u8, g: u8, b: u8, a: u8) {
self.color.set(Color::from_rgba_straight(r, g, b, a));
self.surface.set_color(r, g, b, a);
}
}

View file

@ -72,6 +72,7 @@ mod t0037_toplevel_drag;
mod t0038_subsurface_parent_state;
mod t0039_alpha_modifier;
mod t0040_virtual_keyboard;
mod t0041_input_method;
pub trait TestCase: Sync {
fn name(&self) -> &'static str;
@ -131,5 +132,6 @@ pub fn tests() -> Vec<&'static dyn TestCase> {
t0038_subsurface_parent_state,
t0039_alpha_modifier,
t0040_virtual_keyboard,
t0041_input_method,
}
}

View file

@ -0,0 +1,136 @@
use {
crate::{
it::{
test_client::{DefaultSeat, TestClient},
test_error::TestResult,
test_ifs::{
test_input_method::TestInputMethod,
test_input_popup_surface::TestInputPopupSurface, test_text_input::TestTextInput,
},
test_utils::{
test_expected_event::TestExpectedEvent, test_surface_ext::TestSurfaceExt,
test_window::TestWindow,
},
testrun::TestRun,
},
wire::zwp_text_input_v3,
},
std::rc::Rc,
};
testcase!();
async fn test(run: Rc<TestRun>) -> TestResult {
let _ds = run.create_default_setup().await?;
let consumer = create_consumer(&run).await?;
let supplier = create_supplier(&run).await?;
consumer.client.compare_screenshot("1", false).await?;
supplier.client.sync().await;
tassert!(supplier.activate.next().is_err());
consumer.text.enable()?;
consumer.text.set_cursor_rectangle(100, 100, 100, 100)?;
consumer.text.commit()?;
consumer.client.sync().await;
supplier.client.sync().await;
tassert!(matches!(supplier.activate.next(), Ok(true)));
tassert!(supplier.done.next().is_ok());
consumer.client.compare_screenshot("1", false).await?;
supplier.surface.commit()?;
supplier.client.sync().await;
consumer.client.compare_screenshot("2", false).await?;
supplier.im.commit_string("hello world")?;
supplier.im.commit()?;
supplier.client.sync().await;
consumer.client.sync().await;
tassert_eq!(
consumer.commit_string.next().expect("commit string"),
"hello world"
);
tassert!(consumer.done.next().is_ok());
consumer.text.disable()?;
consumer.text.commit()?;
consumer.client.sync().await;
consumer.client.compare_screenshot("3", false).await?;
Ok(())
}
struct Consumer {
client: Rc<TestClient>,
_seat: DefaultSeat,
_window: Rc<TestWindow>,
text: Rc<TestTextInput>,
_enter: TestExpectedEvent<zwp_text_input_v3::Enter>,
_leave: TestExpectedEvent<zwp_text_input_v3::Leave>,
commit_string: TestExpectedEvent<String>,
done: TestExpectedEvent<zwp_text_input_v3::Done>,
}
async fn create_consumer(run: &Rc<TestRun>) -> TestResult<Consumer> {
let client = run.create_client().await?;
let seat = client.get_default_seat().await?;
let text = client
.registry
.get_text_input_manager()
.await?
.get_text_input(&seat.seat)?;
let window = client.create_window().await?;
window.map2().await?;
client.sync().await;
Ok(Consumer {
_enter: text.enter.expect()?,
_leave: text.leave.expect()?,
commit_string: text.commit_string.expect()?,
done: text.done.expect()?,
client,
_seat: seat,
_window: window,
text,
})
}
struct Supplier {
client: Rc<TestClient>,
_seat: DefaultSeat,
im: Rc<TestInputMethod>,
surface: TestSurfaceExt,
_popup: Rc<TestInputPopupSurface>,
activate: TestExpectedEvent<bool>,
done: TestExpectedEvent<()>,
}
async fn create_supplier(run: &Rc<TestRun>) -> TestResult<Supplier> {
let client = run.create_client().await?;
let seat = client.get_default_seat().await?;
let im = client
.registry
.get_input_method_manager()
.await?
.get_input_method(&seat.seat)?;
let surface = client.create_surface_ext().await?;
surface.set_color(255, 0, 0, 255);
surface.map(100, 100).await?;
let popup = im.get_popup(&surface)?;
client.sync().await;
Ok(Supplier {
activate: im.activate.expect()?,
done: im.done.expect()?,
client,
_seat: seat,
im,
surface,
_popup: popup,
})
}

Binary file not shown.

Binary file not shown.

Binary file not shown.