Review pass 3
This commit is contained in:
@@ -9,6 +9,7 @@ use std::net::{
|
|||||||
};
|
};
|
||||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock, mpsc};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -16,10 +17,14 @@ use crate::op::completion::completion_for_current_thread;
|
|||||||
use crate::op::net::{AcceptedSocket, NetOp, ReceivedDatagram};
|
use crate::op::net::{AcceptedSocket, NetOp, ReceivedDatagram};
|
||||||
|
|
||||||
const DEFAULT_LISTENER_BACKLOG: i32 = 1024;
|
const DEFAULT_LISTENER_BACKLOG: i32 = 1024;
|
||||||
|
const DNS_QUEUE_CAPACITY: usize = 256;
|
||||||
|
|
||||||
type RecvFuture = Pin<Box<dyn Future<Output = io::Result<Vec<u8>>> + 'static>>;
|
type RecvFuture = Pin<Box<dyn Future<Output = io::Result<Vec<u8>>> + 'static>>;
|
||||||
type SendFuture = Pin<Box<dyn Future<Output = io::Result<usize>> + 'static>>;
|
type SendFuture = Pin<Box<dyn Future<Output = io::Result<usize>> + 'static>>;
|
||||||
type ShutdownFuture = Pin<Box<dyn Future<Output = io::Result<()>> + 'static>>;
|
type ShutdownFuture = Pin<Box<dyn Future<Output = io::Result<()>> + 'static>>;
|
||||||
|
type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
|
||||||
|
|
||||||
|
static DNS_POOL: OnceLock<io::Result<BlockingPool>> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ExecutionPath {
|
pub enum ExecutionPath {
|
||||||
@@ -391,16 +396,73 @@ async fn offload<T: Send + 'static>(
|
|||||||
work: impl FnOnce() -> io::Result<T> + Send + 'static,
|
work: impl FnOnce() -> io::Result<T> + Send + 'static,
|
||||||
) -> io::Result<T> {
|
) -> io::Result<T> {
|
||||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||||
let handle_for_thread = handle.clone();
|
if let Err(error) = dns_pool().and_then(|pool| {
|
||||||
if let Err(error) = thread::Builder::new()
|
pool.spawn(Box::new({
|
||||||
.name("ruin-runtime-net-offload".into())
|
let handle = handle.clone();
|
||||||
.spawn(move || handle_for_thread.complete(work()))
|
move || handle.complete(work())
|
||||||
{
|
}))
|
||||||
handle.complete(Err(io::Error::other(error)));
|
}) {
|
||||||
|
handle.complete(Err(error));
|
||||||
}
|
}
|
||||||
future.await
|
future.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BlockingPool {
|
||||||
|
sender: mpsc::SyncSender<BlockingTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockingPool {
|
||||||
|
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
||||||
|
self.sender.try_send(task).map_err(|error| match error {
|
||||||
|
mpsc::TrySendError::Full(_) => io::Error::new(
|
||||||
|
io::ErrorKind::WouldBlock,
|
||||||
|
"DNS resolver blocking worker queue is full",
|
||||||
|
),
|
||||||
|
mpsc::TrySendError::Disconnected(_) => io::Error::new(
|
||||||
|
io::ErrorKind::BrokenPipe,
|
||||||
|
"DNS resolver blocking worker pool has stopped",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dns_pool() -> io::Result<&'static BlockingPool> {
|
||||||
|
match DNS_POOL.get_or_init(create_dns_pool) {
|
||||||
|
Ok(pool) => Ok(pool),
|
||||||
|
Err(error) => Err(io::Error::new(error.kind(), error.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dns_pool() -> io::Result<BlockingPool> {
|
||||||
|
let (sender, receiver) = mpsc::sync_channel::<BlockingTask>(DNS_QUEUE_CAPACITY);
|
||||||
|
let receiver = Arc::new(Mutex::new(receiver));
|
||||||
|
let worker_count = std::thread::available_parallelism()
|
||||||
|
.map(usize::from)
|
||||||
|
.unwrap_or(2)
|
||||||
|
.clamp(2, 4);
|
||||||
|
|
||||||
|
for index in 0..worker_count {
|
||||||
|
let receiver = Arc::clone(&receiver);
|
||||||
|
thread::Builder::new()
|
||||||
|
.name(format!("ruin-runtime-dns-{index}"))
|
||||||
|
.spawn(move || {
|
||||||
|
loop {
|
||||||
|
let task = {
|
||||||
|
let receiver = receiver.lock().expect("DNS worker queue mutex poisoned");
|
||||||
|
receiver.recv()
|
||||||
|
};
|
||||||
|
match task {
|
||||||
|
Ok(task) => task(),
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(BlockingPool { sender })
|
||||||
|
}
|
||||||
|
|
||||||
async fn io_timeout<T>(
|
async fn io_timeout<T>(
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
future: impl Future<Output = io::Result<T>>,
|
future: impl Future<Output = io::Result<T>>,
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ pub enum WindowLifecycle {
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct WindowConfigured {
|
pub struct WindowConfigured {
|
||||||
|
/// Current logical content size and platform presentation scale.
|
||||||
|
///
|
||||||
|
/// Backends should emit a new configuration when either the logical size or
|
||||||
|
/// scale changes. This lets hosts relayout for size changes while renderers
|
||||||
|
/// can update drawable/text scale when a window moves between displays.
|
||||||
pub actual_inner_size: UiSize,
|
pub actual_inner_size: UiSize,
|
||||||
pub scale_factor: f32,
|
pub scale_factor: f32,
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::collections::BTreeMap;
|
|||||||
use std::ffi::{CStr, CString, c_void};
|
use std::ffi::{CStr, CString, c_void};
|
||||||
use std::ptr::NonNull;
|
use std::ptr::NonNull;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex, Once, OnceLock};
|
use std::sync::{Arc, Mutex, MutexGuard, Once, OnceLock};
|
||||||
|
|
||||||
use objc2::rc::Retained;
|
use objc2::rc::Retained;
|
||||||
use objc2::runtime::{AnyClass, AnyObject, Bool, ClassBuilder, Sel};
|
use objc2::runtime::{AnyClass, AnyObject, Bool, ClassBuilder, Sel};
|
||||||
@@ -65,12 +65,17 @@ struct RunLoopSourceRegistration {
|
|||||||
_source: CFRetained<CFRunLoopSource>,
|
_source: CFRetained<CFRunLoopSource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone)]
|
||||||
struct RunLoopWakeHandle {
|
struct RunLoopWakeHandle {
|
||||||
run_loop: usize,
|
run_loop: CFRetained<CFRunLoop>,
|
||||||
source: usize,
|
source: CFRetained<CFRunLoopSource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CFRunLoopSourceSignal and CFRunLoopWakeUp are designed for cross-thread wakeups.
|
||||||
|
// The handle owns retained references and never exposes mutable access to the CF objects.
|
||||||
|
unsafe impl Send for RunLoopWakeHandle {}
|
||||||
|
unsafe impl Sync for RunLoopWakeHandle {}
|
||||||
|
|
||||||
struct MacosWindow {
|
struct MacosWindow {
|
||||||
spec: WindowSpec,
|
spec: WindowSpec,
|
||||||
state: MacosWindowState,
|
state: MacosWindowState,
|
||||||
@@ -321,35 +326,41 @@ fn run_loop_wake_handle() -> &'static Mutex<Option<RunLoopWakeHandle>> {
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
struct ResizePipeline {
|
struct ResizePipeline {
|
||||||
active_resize: Option<UiSize>,
|
active_resize: Option<WindowConfigured>,
|
||||||
next_resize: Option<UiSize>,
|
next_resize: Option<WindowConfigured>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResizePipeline {
|
impl ResizePipeline {
|
||||||
fn note_resize(&mut self, viewport: UiSize) -> Option<UiSize> {
|
fn note_resize(&mut self, configuration: WindowConfigured) -> Option<WindowConfigured> {
|
||||||
if self.active_resize.is_some() {
|
if self.active_resize.is_some() {
|
||||||
self.next_resize = Some(viewport);
|
self.next_resize = Some(configuration);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.active_resize = Some(viewport);
|
self.active_resize = Some(configuration);
|
||||||
Some(viewport)
|
Some(configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_initial_request(&mut self, viewport: UiSize) -> Option<UiSize> {
|
fn ensure_initial_request(
|
||||||
|
&mut self,
|
||||||
|
configuration: WindowConfigured,
|
||||||
|
) -> Option<WindowConfigured> {
|
||||||
if self.active_resize.is_some() || self.next_resize.is_some() {
|
if self.active_resize.is_some() || self.next_resize.is_some() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.active_resize = Some(viewport);
|
self.active_resize = Some(configuration);
|
||||||
Some(viewport)
|
Some(configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn note_presented_scene(&mut self, scene_viewport: UiSize) -> Option<UiSize> {
|
fn note_presented_scene(&mut self, scene_viewport: UiSize) -> Option<WindowConfigured> {
|
||||||
if self.active_resize != Some(scene_viewport) {
|
let Some(presented_configuration) = self.active_resize else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if presented_configuration.actual_inner_size != scene_viewport {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.active_resize = None;
|
self.active_resize = None;
|
||||||
if self.next_resize == Some(scene_viewport) {
|
if self.next_resize == Some(presented_configuration) {
|
||||||
self.next_resize = None;
|
self.next_resize = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,6 +409,7 @@ unsafe impl Sync for AppKitSurfaceTarget {}
|
|||||||
|
|
||||||
impl HasDisplayHandle for AppKitSurfaceTarget {
|
impl HasDisplayHandle for AppKitSurfaceTarget {
|
||||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||||
|
assert_main_thread();
|
||||||
let raw = RawDisplayHandle::AppKit(AppKitDisplayHandle::new());
|
let raw = RawDisplayHandle::AppKit(AppKitDisplayHandle::new());
|
||||||
Ok(unsafe { DisplayHandle::borrow_raw(raw) })
|
Ok(unsafe { DisplayHandle::borrow_raw(raw) })
|
||||||
}
|
}
|
||||||
@@ -405,6 +417,7 @@ impl HasDisplayHandle for AppKitSurfaceTarget {
|
|||||||
|
|
||||||
impl HasWindowHandle for AppKitSurfaceTarget {
|
impl HasWindowHandle for AppKitSurfaceTarget {
|
||||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||||
|
assert_main_thread();
|
||||||
let ptr = NonNull::new(self.view.cast::<c_void>()).ok_or(HandleError::Unavailable)?;
|
let ptr = NonNull::new(self.view.cast::<c_void>()).ok_or(HandleError::Unavailable)?;
|
||||||
let raw = RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr));
|
let raw = RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr));
|
||||||
Ok(unsafe { WindowHandle::borrow_raw(raw) })
|
Ok(unsafe { WindowHandle::borrow_raw(raw) })
|
||||||
@@ -467,8 +480,8 @@ fn install_platform_run_loop_source() -> Option<RunLoopSourceRegistration> {
|
|||||||
run_loop.add_source(Some(&source), common_modes);
|
run_loop.add_source(Some(&source), common_modes);
|
||||||
|
|
||||||
let handle = RunLoopWakeHandle {
|
let handle = RunLoopWakeHandle {
|
||||||
run_loop: CFRetained::as_ptr(&run_loop).as_ptr().cast::<c_void>() as usize,
|
run_loop: run_loop.clone(),
|
||||||
source: CFRetained::as_ptr(&source).as_ptr().cast::<c_void>() as usize,
|
source: source.clone(),
|
||||||
};
|
};
|
||||||
if let Ok(mut slot) = run_loop_wake_handle().lock() {
|
if let Ok(mut slot) = run_loop_wake_handle().lock() {
|
||||||
*slot = Some(handle);
|
*slot = Some(handle);
|
||||||
@@ -489,13 +502,15 @@ extern "C-unwind" fn platform_run_loop_source_perform(_info: *mut c_void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn signal_platform_run_loop_source() {
|
fn signal_platform_run_loop_source() {
|
||||||
let Some(handle) = run_loop_wake_handle().lock().ok().and_then(|slot| *slot) else {
|
let Some(handle) = run_loop_wake_handle()
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|slot| slot.clone())
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let source = unsafe { &*(handle.source as *const CFRunLoopSource) };
|
handle.source.signal();
|
||||||
let run_loop = unsafe { &*(handle.run_loop as *const CFRunLoop) };
|
handle.run_loop.wake_up();
|
||||||
source.signal();
|
|
||||||
run_loop.wake_up();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_main_thread() {
|
fn assert_main_thread() {
|
||||||
@@ -574,91 +589,98 @@ fn delegate_state() -> &'static Mutex<DelegateState> {
|
|||||||
DELEGATE_STATE.get_or_init(|| Mutex::new(DelegateState::default()))
|
DELEGATE_STATE.get_or_init(|| Mutex::new(DelegateState::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lock_delegate_state() -> MutexGuard<'static, DelegateState> {
|
||||||
|
delegate_state()
|
||||||
|
.lock()
|
||||||
|
.expect("macOS delegate event state mutex poisoned")
|
||||||
|
}
|
||||||
|
|
||||||
fn queue_delegate_close_request(window: *mut AnyObject) {
|
fn queue_delegate_close_request(window: *mut AnyObject) {
|
||||||
if window.is_null() {
|
if window.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
lock_delegate_state()
|
||||||
state.pending_close_requests.push(window as usize);
|
.pending_close_requests
|
||||||
}
|
.push(window as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queue_delegate_window_closed(window: *mut AnyObject) {
|
fn queue_delegate_window_closed(window: *mut AnyObject) {
|
||||||
if window.is_null() {
|
if window.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
lock_delegate_state()
|
||||||
state.pending_closed_windows.push(window as usize);
|
.pending_closed_windows
|
||||||
}
|
.push(window as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queue_delegate_resize(window: *mut AnyObject, size: UiSize, scale_factor: f32) {
|
fn queue_delegate_resize(window: *mut AnyObject, size: UiSize, scale_factor: f32) {
|
||||||
if window.is_null() {
|
if window.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
lock_delegate_state()
|
||||||
state.pending_resize_events.push(DelegateResizeEvent {
|
.pending_resize_events
|
||||||
|
.push(DelegateResizeEvent {
|
||||||
window_ptr: window as usize,
|
window_ptr: window as usize,
|
||||||
size,
|
size,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_delegate_configuration_from_window(window: *mut AnyObject) {
|
||||||
|
if window.is_null() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
let view: *mut AnyObject = unsafe { objc2::msg_send![window, contentView] };
|
||||||
|
if view.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let frame: CGRect = unsafe { objc2::msg_send![view, frame] };
|
||||||
|
let scale_factor: f64 = unsafe { objc2::msg_send![window, backingScaleFactor] };
|
||||||
|
queue_delegate_resize(
|
||||||
|
window,
|
||||||
|
UiSize::new(frame.size.width as f32, frame.size.height as f32),
|
||||||
|
(scale_factor as f32).max(1.0),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queue_delegate_pointer_event(window: *mut AnyObject, event: PointerEvent) {
|
fn queue_delegate_pointer_event(window: *mut AnyObject, event: PointerEvent) {
|
||||||
if window.is_null() {
|
if window.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
lock_delegate_state()
|
||||||
state
|
.pending_input_events
|
||||||
.pending_input_events
|
.push(DelegateInputEvent::Pointer {
|
||||||
.push(DelegateInputEvent::Pointer {
|
window_ptr: window as usize,
|
||||||
window_ptr: window as usize,
|
event,
|
||||||
event,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queue_delegate_keyboard_event(window: *mut AnyObject, event: KeyboardEvent) {
|
fn queue_delegate_keyboard_event(window: *mut AnyObject, event: KeyboardEvent) {
|
||||||
if window.is_null() {
|
if window.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
lock_delegate_state()
|
||||||
state
|
.pending_input_events
|
||||||
.pending_input_events
|
.push(DelegateInputEvent::Keyboard {
|
||||||
.push(DelegateInputEvent::Keyboard {
|
window_ptr: window as usize,
|
||||||
window_ptr: window as usize,
|
event,
|
||||||
event,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_delegate_close_requests() -> Vec<usize> {
|
fn take_delegate_close_requests() -> Vec<usize> {
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
std::mem::take(&mut lock_delegate_state().pending_close_requests)
|
||||||
return std::mem::take(&mut state.pending_close_requests);
|
|
||||||
}
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_delegate_closed_windows() -> Vec<usize> {
|
fn take_delegate_closed_windows() -> Vec<usize> {
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
std::mem::take(&mut lock_delegate_state().pending_closed_windows)
|
||||||
return std::mem::take(&mut state.pending_closed_windows);
|
|
||||||
}
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_delegate_resize_events() -> Vec<DelegateResizeEvent> {
|
fn take_delegate_resize_events() -> Vec<DelegateResizeEvent> {
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
std::mem::take(&mut lock_delegate_state().pending_resize_events)
|
||||||
return std::mem::take(&mut state.pending_resize_events);
|
|
||||||
}
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_delegate_input_events() -> Vec<DelegateInputEvent> {
|
fn take_delegate_input_events() -> Vec<DelegateInputEvent> {
|
||||||
if let Ok(mut state) = delegate_state().lock() {
|
std::mem::take(&mut lock_delegate_state().pending_input_events)
|
||||||
return std::mem::take(&mut state.pending_input_events);
|
|
||||||
}
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_delegate_class() -> &'static AnyClass {
|
fn window_delegate_class() -> &'static AnyClass {
|
||||||
@@ -696,28 +718,26 @@ fn window_delegate_class() -> &'static AnyClass {
|
|||||||
notification: *mut AnyObject,
|
notification: *mut AnyObject,
|
||||||
) {
|
) {
|
||||||
let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] };
|
let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] };
|
||||||
if window.is_null() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let view: *mut AnyObject = unsafe { objc2::msg_send![window, contentView] };
|
|
||||||
if view.is_null() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let frame: CGRect = unsafe { objc2::msg_send![view, frame] };
|
|
||||||
let scale_factor: f64 = unsafe { objc2::msg_send![window, backingScaleFactor] };
|
|
||||||
trace!(
|
trace!(
|
||||||
target: "ruin_ui_platform_macos::resize",
|
target: "ruin_ui_platform_macos::resize",
|
||||||
window_ptr = window as usize,
|
window_ptr = window as usize,
|
||||||
width = frame.size.width,
|
|
||||||
height = frame.size.height,
|
|
||||||
scale_factor,
|
|
||||||
"windowDidResize:"
|
"windowDidResize:"
|
||||||
);
|
);
|
||||||
queue_delegate_resize(
|
queue_delegate_configuration_from_window(window);
|
||||||
window,
|
signal_platform_run_loop_source();
|
||||||
UiSize::new(frame.size.width as f32, frame.size.height as f32),
|
}
|
||||||
(scale_factor as f32).max(1.0),
|
extern "C-unwind" fn did_change_screen(
|
||||||
|
_this: &AnyObject,
|
||||||
|
_cmd: Sel,
|
||||||
|
notification: *mut AnyObject,
|
||||||
|
) {
|
||||||
|
let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] };
|
||||||
|
trace!(
|
||||||
|
target: "ruin_ui_platform_macos::resize",
|
||||||
|
window_ptr = window as usize,
|
||||||
|
"windowDidChangeScreen:"
|
||||||
);
|
);
|
||||||
|
queue_delegate_configuration_from_window(window);
|
||||||
signal_platform_run_loop_source();
|
signal_platform_run_loop_source();
|
||||||
}
|
}
|
||||||
extern "C-unwind" fn did_change_occlusion_state(
|
extern "C-unwind" fn did_change_occlusion_state(
|
||||||
@@ -782,6 +802,8 @@ fn window_delegate_class() -> &'static AnyClass {
|
|||||||
builder.add_method::<AnyObject, _>(sel!(windowWillClose:), method);
|
builder.add_method::<AnyObject, _>(sel!(windowWillClose:), method);
|
||||||
let method: extern "C-unwind" fn(_, _, _) = did_resize;
|
let method: extern "C-unwind" fn(_, _, _) = did_resize;
|
||||||
builder.add_method::<AnyObject, _>(sel!(windowDidResize:), method);
|
builder.add_method::<AnyObject, _>(sel!(windowDidResize:), method);
|
||||||
|
let method: extern "C-unwind" fn(_, _, _) = did_change_screen;
|
||||||
|
builder.add_method::<AnyObject, _>(sel!(windowDidChangeScreen:), method);
|
||||||
let method: extern "C-unwind" fn(_, _, _) = did_change_occlusion_state;
|
let method: extern "C-unwind" fn(_, _, _) = did_change_occlusion_state;
|
||||||
builder.add_method::<AnyObject, _>(sel!(windowDidChangeOcclusionState:), method);
|
builder.add_method::<AnyObject, _>(sel!(windowDidChangeOcclusionState:), method);
|
||||||
let method: extern "C-unwind" fn(_, _, _) = did_become_key;
|
let method: extern "C-unwind" fn(_, _, _) = did_become_key;
|
||||||
@@ -817,6 +839,17 @@ fn content_view_class() -> &'static AnyClass {
|
|||||||
Bool::YES
|
Bool::YES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C-unwind" fn did_change_backing_properties(this: &AnyObject, _cmd: Sel) {
|
||||||
|
let window: *mut AnyObject = unsafe { objc2::msg_send![this, window] };
|
||||||
|
trace!(
|
||||||
|
target: "ruin_ui_platform_macos::resize",
|
||||||
|
window_ptr = window as usize,
|
||||||
|
"viewDidChangeBackingProperties"
|
||||||
|
);
|
||||||
|
queue_delegate_configuration_from_window(window);
|
||||||
|
signal_platform_run_loop_source();
|
||||||
|
}
|
||||||
|
|
||||||
extern "C-unwind" fn mouse_moved(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
extern "C-unwind" fn mouse_moved(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
||||||
queue_pointer_from_view_event(this, event, PointerEventKind::Move);
|
queue_pointer_from_view_event(this, event, PointerEventKind::Move);
|
||||||
signal_platform_run_loop_source();
|
signal_platform_run_loop_source();
|
||||||
@@ -923,6 +956,8 @@ fn content_view_class() -> &'static AnyClass {
|
|||||||
builder.add_method::<AnyObject, _>(sel!(acceptsFirstResponder), method);
|
builder.add_method::<AnyObject, _>(sel!(acceptsFirstResponder), method);
|
||||||
let method: extern "C-unwind" fn(_, _) -> _ = is_flipped;
|
let method: extern "C-unwind" fn(_, _) -> _ = is_flipped;
|
||||||
builder.add_method::<AnyObject, _>(sel!(isFlipped), method);
|
builder.add_method::<AnyObject, _>(sel!(isFlipped), method);
|
||||||
|
let method: extern "C-unwind" fn(_, _) = did_change_backing_properties;
|
||||||
|
builder.add_method::<AnyObject, _>(sel!(viewDidChangeBackingProperties), method);
|
||||||
let method: extern "C-unwind" fn(_, _, _) = mouse_moved;
|
let method: extern "C-unwind" fn(_, _, _) = mouse_moved;
|
||||||
builder.add_method::<AnyObject, _>(sel!(mouseMoved:), method);
|
builder.add_method::<AnyObject, _>(sel!(mouseMoved:), method);
|
||||||
let method: extern "C-unwind" fn(_, _, _) = mouse_moved;
|
let method: extern "C-unwind" fn(_, _, _) = mouse_moved;
|
||||||
@@ -1167,7 +1202,7 @@ fn handle_create_window(state: &Rc<RefCell<MacosState>>, window_id: WindowId, sp
|
|||||||
window.configuration.visible = visible;
|
window.configuration.visible = visible;
|
||||||
let _ = window
|
let _ = window
|
||||||
.resize_pipeline
|
.resize_pipeline
|
||||||
.ensure_initial_request(window.configuration.actual_inner_size);
|
.ensure_initial_request(window.configuration);
|
||||||
sync_visibility(window);
|
sync_visibility(window);
|
||||||
apply_cursor_icon(window.cursor_icon);
|
apply_cursor_icon(window.cursor_icon);
|
||||||
emit_opened = true;
|
emit_opened = true;
|
||||||
@@ -1265,13 +1300,15 @@ fn handle_update_window(
|
|||||||
resize_native_window(native, size);
|
resize_native_window(native, size);
|
||||||
}
|
}
|
||||||
repaint_retained_scene_for_resize(window_id, window);
|
repaint_retained_scene_for_resize(window_id, window);
|
||||||
if let Some(active_resize) = window.resize_pipeline.note_resize(size) {
|
if let Some(active_resize) =
|
||||||
|
window.resize_pipeline.note_resize(window.configuration)
|
||||||
|
{
|
||||||
trace!(
|
trace!(
|
||||||
target: "ruin_ui_platform_macos::resize",
|
target: "ruin_ui_platform_macos::resize",
|
||||||
?window_id,
|
?window_id,
|
||||||
width = active_resize.width,
|
width = active_resize.actual_inner_size.width,
|
||||||
height = active_resize.height,
|
height = active_resize.actual_inner_size.height,
|
||||||
scale_factor = window.configuration.scale_factor,
|
scale_factor = active_resize.scale_factor,
|
||||||
"emitting Configured from window update resize"
|
"emitting Configured from window update resize"
|
||||||
);
|
);
|
||||||
emit_configured = Some(window.configuration);
|
emit_configured = Some(window.configuration);
|
||||||
@@ -1279,10 +1316,10 @@ fn handle_update_window(
|
|||||||
trace!(
|
trace!(
|
||||||
target: "ruin_ui_platform_macos::resize",
|
target: "ruin_ui_platform_macos::resize",
|
||||||
?window_id,
|
?window_id,
|
||||||
active_width = window.resize_pipeline.active_resize.map(|s| s.width),
|
active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width),
|
||||||
active_height = window.resize_pipeline.active_resize.map(|s| s.height),
|
active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height),
|
||||||
next_width = window.resize_pipeline.next_resize.map(|s| s.width),
|
next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width),
|
||||||
next_height = window.resize_pipeline.next_resize.map(|s| s.height),
|
next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height),
|
||||||
"staged next resize from window update while another layout is in flight"
|
"staged next resize from window update while another layout is in flight"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1371,10 +1408,10 @@ fn try_present_scene(
|
|||||||
scene_height = scene_viewport.height,
|
scene_height = scene_viewport.height,
|
||||||
current_width = current_viewport.width,
|
current_width = current_viewport.width,
|
||||||
current_height = current_viewport.height,
|
current_height = current_viewport.height,
|
||||||
active_width = window.resize_pipeline.active_resize.map(|s| s.width),
|
active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width),
|
||||||
active_height = window.resize_pipeline.active_resize.map(|s| s.height),
|
active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height),
|
||||||
next_width = window.resize_pipeline.next_resize.map(|s| s.width),
|
next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width),
|
||||||
next_height = window.resize_pipeline.next_resize.map(|s| s.height),
|
next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height),
|
||||||
"handle_replace_scene start"
|
"handle_replace_scene start"
|
||||||
);
|
);
|
||||||
let render_succeeded = render_scene_to_current_drawable(window_id, window, scene);
|
let render_succeeded = render_scene_to_current_drawable(window_id, window, scene);
|
||||||
@@ -1391,11 +1428,12 @@ fn try_present_scene(
|
|||||||
?window_id,
|
?window_id,
|
||||||
scene_width = scene_viewport.width,
|
scene_width = scene_viewport.width,
|
||||||
scene_height = scene_viewport.height,
|
scene_height = scene_viewport.height,
|
||||||
next_width = next_resize.width,
|
next_width = next_resize.actual_inner_size.width,
|
||||||
next_height = next_resize.height,
|
next_height = next_resize.actual_inner_size.height,
|
||||||
|
next_scale = next_resize.scale_factor,
|
||||||
"presented active resize scene; promoting staged next resize"
|
"presented active resize scene; promoting staged next resize"
|
||||||
);
|
);
|
||||||
emit_resize_request(window_id, window, next_resize, followup_events);
|
emit_resize_request(window_id, next_resize, followup_events);
|
||||||
}
|
}
|
||||||
Some((scene_version, item_count, window.configuration.visible))
|
Some((scene_version, item_count, window.configuration.visible))
|
||||||
}
|
}
|
||||||
@@ -1495,8 +1533,8 @@ fn render_scene_to_current_drawable(
|
|||||||
?window_id,
|
?window_id,
|
||||||
scene_width = scene_viewport.width,
|
scene_width = scene_viewport.width,
|
||||||
scene_height = scene_viewport.height,
|
scene_height = scene_viewport.height,
|
||||||
active_width = window.resize_pipeline.active_resize.map(|s| s.width),
|
active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width),
|
||||||
active_height = window.resize_pipeline.active_resize.map(|s| s.height),
|
active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height),
|
||||||
current_width = current_viewport.width,
|
current_width = current_viewport.width,
|
||||||
current_height = current_viewport.height,
|
current_height = current_viewport.height,
|
||||||
render_succeeded,
|
render_succeeded,
|
||||||
@@ -1754,42 +1792,47 @@ fn note_window_resize(
|
|||||||
viewport: UiSize,
|
viewport: UiSize,
|
||||||
pending: &mut Vec<PlatformEvent>,
|
pending: &mut Vec<PlatformEvent>,
|
||||||
) {
|
) {
|
||||||
if let Some(active_resize) = window.resize_pipeline.note_resize(viewport) {
|
debug_assert_eq!(window.configuration.actual_inner_size, viewport);
|
||||||
emit_resize_request(window_id, window, active_resize, pending);
|
if let Some(active_resize) = window.resize_pipeline.note_resize(window.configuration) {
|
||||||
|
emit_resize_request(window_id, active_resize, pending);
|
||||||
} else {
|
} else {
|
||||||
trace!(
|
trace!(
|
||||||
target: "ruin_ui_platform_macos::resize",
|
target: "ruin_ui_platform_macos::resize",
|
||||||
?window_id,
|
?window_id,
|
||||||
active_width = window.resize_pipeline.active_resize.map(|s| s.width),
|
active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width),
|
||||||
active_height = window.resize_pipeline.active_resize.map(|s| s.height),
|
active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height),
|
||||||
next_width = window.resize_pipeline.next_resize.map(|s| s.width),
|
active_scale = window.resize_pipeline.active_resize.map(|s| s.scale_factor),
|
||||||
next_height = window.resize_pipeline.next_resize.map(|s| s.height),
|
next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width),
|
||||||
"staged next resize while another layout is in flight"
|
next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height),
|
||||||
|
next_scale = window.resize_pipeline.next_resize.map(|s| s.scale_factor),
|
||||||
|
"staged next configuration while another layout is in flight"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_resize_request(
|
fn emit_resize_request(
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
window: &mut MacosWindow,
|
configuration: WindowConfigured,
|
||||||
viewport: UiSize,
|
|
||||||
pending: &mut Vec<PlatformEvent>,
|
pending: &mut Vec<PlatformEvent>,
|
||||||
) {
|
) {
|
||||||
pending.push(PlatformEvent::Configured {
|
pending.push(PlatformEvent::Configured {
|
||||||
window_id,
|
window_id,
|
||||||
configuration: window.configuration,
|
configuration,
|
||||||
});
|
});
|
||||||
trace!(
|
trace!(
|
||||||
target: "ruin_ui_platform_macos::resize",
|
target: "ruin_ui_platform_macos::resize",
|
||||||
?window_id,
|
?window_id,
|
||||||
width = viewport.width,
|
width = configuration.actual_inner_size.width,
|
||||||
height = viewport.height,
|
height = configuration.actual_inner_size.height,
|
||||||
scale_factor = window.configuration.scale_factor,
|
scale_factor = configuration.scale_factor,
|
||||||
"emitting Configured from delegate resize"
|
"emitting Configured from platform configuration change"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_presented_resize(window: &mut MacosWindow, presented: UiSize) -> Option<UiSize> {
|
fn finish_presented_resize(
|
||||||
|
window: &mut MacosWindow,
|
||||||
|
presented: UiSize,
|
||||||
|
) -> Option<WindowConfigured> {
|
||||||
window.resize_pipeline.note_presented_scene(presented)
|
window.resize_pipeline.note_presented_scene(presented)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1926,16 +1969,19 @@ fn collect_window_state_events(
|
|||||||
&& let Some(scene) = window.latest_scene.clone()
|
&& let Some(scene) = window.latest_scene.clone()
|
||||||
&& (became_visible
|
&& (became_visible
|
||||||
|| !window.state.has_presented_frame
|
|| !window.state.has_presented_frame
|
||||||
|| window.resize_pipeline.active_resize == Some(scene.logical_size))
|
|| window
|
||||||
|
.resize_pipeline
|
||||||
|
.active_resize
|
||||||
|
.is_some_and(|configuration| configuration.actual_inner_size == scene.logical_size))
|
||||||
{
|
{
|
||||||
let _ = try_present_scene(window_id, window, &scene, pending);
|
let _ = try_present_scene(window_id, window, &scene, pending);
|
||||||
} else if is_visible
|
} else if is_visible
|
||||||
&& !window.state.has_presented_frame
|
&& !window.state.has_presented_frame
|
||||||
&& let Some(initial_viewport) = window
|
&& let Some(initial_viewport) = window
|
||||||
.resize_pipeline
|
.resize_pipeline
|
||||||
.ensure_initial_request(window.configuration.actual_inner_size)
|
.ensure_initial_request(window.configuration)
|
||||||
{
|
{
|
||||||
emit_resize_request(window_id, window, initial_viewport, pending);
|
emit_resize_request(window_id, initial_viewport, pending);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2067,38 +2113,51 @@ mod tests {
|
|||||||
use super::{
|
use super::{
|
||||||
MacosWindowState, PresentationFlushPolicy, ResizePipeline, presentation_flush_policy,
|
MacosWindowState, PresentationFlushPolicy, ResizePipeline, presentation_flush_policy,
|
||||||
};
|
};
|
||||||
use ruin_ui::{UiSize, WindowLifecycle};
|
use ruin_ui::{UiSize, WindowConfigured, WindowLifecycle};
|
||||||
|
|
||||||
|
fn configuration(width: f32, height: f32, scale_factor: f32) -> WindowConfigured {
|
||||||
|
WindowConfigured {
|
||||||
|
actual_inner_size: UiSize::new(width, height),
|
||||||
|
scale_factor,
|
||||||
|
visible: true,
|
||||||
|
maximized: false,
|
||||||
|
fullscreen: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resize_pipeline_stages_latest_resize_while_active_request_is_in_flight() {
|
fn resize_pipeline_stages_latest_resize_while_active_request_is_in_flight() {
|
||||||
let mut pipeline = ResizePipeline::default();
|
let mut pipeline = ResizePipeline::default();
|
||||||
|
let first = configuration(640.0, 480.0, 1.0);
|
||||||
|
let second = configuration(800.0, 600.0, 1.0);
|
||||||
|
let third = configuration(1024.0, 768.0, 1.0);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(pipeline.note_resize(first), Some(first));
|
||||||
pipeline.note_resize(UiSize::new(640.0, 480.0)),
|
assert_eq!(pipeline.active_resize, Some(first));
|
||||||
Some(UiSize::new(640.0, 480.0))
|
|
||||||
);
|
|
||||||
assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0)));
|
|
||||||
assert_eq!(pipeline.next_resize, None);
|
assert_eq!(pipeline.next_resize, None);
|
||||||
|
|
||||||
assert_eq!(pipeline.note_resize(UiSize::new(800.0, 600.0)), None);
|
assert_eq!(pipeline.note_resize(second), None);
|
||||||
assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0)));
|
assert_eq!(pipeline.active_resize, Some(first));
|
||||||
assert_eq!(pipeline.next_resize, Some(UiSize::new(800.0, 600.0)));
|
assert_eq!(pipeline.next_resize, Some(second));
|
||||||
|
|
||||||
assert_eq!(pipeline.note_resize(UiSize::new(1024.0, 768.0)), None);
|
assert_eq!(pipeline.note_resize(third), None);
|
||||||
assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0)));
|
assert_eq!(pipeline.active_resize, Some(first));
|
||||||
assert_eq!(pipeline.next_resize, Some(UiSize::new(1024.0, 768.0)));
|
assert_eq!(pipeline.next_resize, Some(third));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resize_pipeline_promotes_next_resize_after_presenting_active_scene() {
|
fn resize_pipeline_promotes_next_resize_after_presenting_active_scene() {
|
||||||
let mut pipeline = ResizePipeline::default();
|
let mut pipeline = ResizePipeline::default();
|
||||||
let first = UiSize::new(640.0, 480.0);
|
let first = configuration(640.0, 480.0, 1.0);
|
||||||
let second = UiSize::new(1024.0, 768.0);
|
let second = configuration(1024.0, 768.0, 1.0);
|
||||||
|
|
||||||
assert_eq!(pipeline.note_resize(first), Some(first));
|
assert_eq!(pipeline.note_resize(first), Some(first));
|
||||||
assert_eq!(pipeline.note_resize(second), None);
|
assert_eq!(pipeline.note_resize(second), None);
|
||||||
|
|
||||||
assert_eq!(pipeline.note_presented_scene(first), Some(second));
|
assert_eq!(
|
||||||
|
pipeline.note_presented_scene(first.actual_inner_size),
|
||||||
|
Some(second)
|
||||||
|
);
|
||||||
assert_eq!(pipeline.active_resize, Some(second));
|
assert_eq!(pipeline.active_resize, Some(second));
|
||||||
assert_eq!(pipeline.next_resize, None);
|
assert_eq!(pipeline.next_resize, None);
|
||||||
}
|
}
|
||||||
@@ -2106,21 +2165,41 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resize_pipeline_drops_duplicate_staged_resize_when_present_already_satisfies_it() {
|
fn resize_pipeline_drops_duplicate_staged_resize_when_present_already_satisfies_it() {
|
||||||
let mut pipeline = ResizePipeline::default();
|
let mut pipeline = ResizePipeline::default();
|
||||||
let viewport = UiSize::new(800.0, 600.0);
|
let viewport = configuration(800.0, 600.0, 1.0);
|
||||||
|
|
||||||
assert_eq!(pipeline.note_resize(viewport), Some(viewport));
|
assert_eq!(pipeline.note_resize(viewport), Some(viewport));
|
||||||
assert_eq!(pipeline.note_resize(viewport), None);
|
assert_eq!(pipeline.note_resize(viewport), None);
|
||||||
|
|
||||||
assert_eq!(pipeline.note_presented_scene(viewport), None);
|
assert_eq!(
|
||||||
|
pipeline.note_presented_scene(viewport.actual_inner_size),
|
||||||
|
None
|
||||||
|
);
|
||||||
assert_eq!(pipeline.active_resize, None);
|
assert_eq!(pipeline.active_resize, None);
|
||||||
assert_eq!(pipeline.next_resize, None);
|
assert_eq!(pipeline.next_resize, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_pipeline_preserves_same_size_scale_changes() {
|
||||||
|
let mut pipeline = ResizePipeline::default();
|
||||||
|
let one_x = configuration(800.0, 600.0, 1.0);
|
||||||
|
let two_x = configuration(800.0, 600.0, 2.0);
|
||||||
|
|
||||||
|
assert_eq!(pipeline.note_resize(one_x), Some(one_x));
|
||||||
|
assert_eq!(pipeline.note_resize(two_x), None);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pipeline.note_presented_scene(one_x.actual_inner_size),
|
||||||
|
Some(two_x)
|
||||||
|
);
|
||||||
|
assert_eq!(pipeline.active_resize, Some(two_x));
|
||||||
|
assert_eq!(pipeline.next_resize, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resize_pipeline_ignores_presents_for_non_active_scene_sizes() {
|
fn resize_pipeline_ignores_presents_for_non_active_scene_sizes() {
|
||||||
let mut pipeline = ResizePipeline::default();
|
let mut pipeline = ResizePipeline::default();
|
||||||
let active = UiSize::new(640.0, 480.0);
|
let active = configuration(640.0, 480.0, 1.0);
|
||||||
let next = UiSize::new(900.0, 700.0);
|
let next = configuration(900.0, 700.0, 1.0);
|
||||||
|
|
||||||
assert_eq!(pipeline.note_resize(active), Some(active));
|
assert_eq!(pipeline.note_resize(active), Some(active));
|
||||||
assert_eq!(pipeline.note_resize(next), None);
|
assert_eq!(pipeline.note_resize(next), None);
|
||||||
|
|||||||
Reference in New Issue
Block a user