Web: async improvements (#3805)
- Internal: Fix dropping `Notifier` without sending a result causing `Future`s to never complete. This should never happen anyway, but now we get a panic instead of nothing if we hit a bug. - Internal: Remove a bunch of `unwrap()`s that aren't required when correctly using `MainThreadMarker`. - `Window::canvas()` is now able to return a reference instead of an owned value. Extracted from #3801.
This commit is contained in:
parent
5ec934b1b0
commit
ef580b817d
12 changed files with 238 additions and 219 deletions
|
|
@ -70,6 +70,7 @@ changelog entry.
|
||||||
- Change signature of `EventLoop::run_app`, `EventLoopExtPumpEvents::pump_app_events` and
|
- Change signature of `EventLoop::run_app`, `EventLoopExtPumpEvents::pump_app_events` and
|
||||||
`EventLoopExtRunOnDemand::run_app_on_demand` to accept a `impl ApplicationHandler` directly,
|
`EventLoopExtRunOnDemand::run_app_on_demand` to accept a `impl ApplicationHandler` directly,
|
||||||
instead of requiring a `&mut` reference to it.
|
instead of requiring a `&mut` reference to it.
|
||||||
|
- On Web, `Window::canvas()` now returns a reference.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
//! [`WindowEvent::Touch`]: crate::event::WindowEvent::Touch
|
//! [`WindowEvent::Touch`]: crate::event::WindowEvent::Touch
|
||||||
//! [`Window::set_outer_position()`]: crate::window::Window::set_outer_position
|
//! [`Window::set_outer_position()`]: crate::window::Window::set_outer_position
|
||||||
|
|
||||||
|
use std::cell::Ref;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
@ -67,7 +68,7 @@ pub struct HtmlCanvasElement;
|
||||||
pub trait WindowExtWeb {
|
pub trait WindowExtWeb {
|
||||||
/// Only returns the canvas if called from inside the window context (the
|
/// Only returns the canvas if called from inside the window context (the
|
||||||
/// main thread).
|
/// main thread).
|
||||||
fn canvas(&self) -> Option<HtmlCanvasElement>;
|
fn canvas(&self) -> Option<Ref<'_, HtmlCanvasElement>>;
|
||||||
|
|
||||||
/// Returns [`true`] if calling `event.preventDefault()` is enabled.
|
/// Returns [`true`] if calling `event.preventDefault()` is enabled.
|
||||||
///
|
///
|
||||||
|
|
@ -87,7 +88,7 @@ pub trait WindowExtWeb {
|
||||||
|
|
||||||
impl WindowExtWeb for Window {
|
impl WindowExtWeb for Window {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn canvas(&self) -> Option<HtmlCanvasElement> {
|
fn canvas(&self) -> Option<Ref<'_, HtmlCanvasElement>> {
|
||||||
self.window.canvas()
|
self.window.canvas()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,7 +176,7 @@ pub trait EventLoopExtWeb {
|
||||||
not(all(web_platform, target_feature = "exception-handling")),
|
not(all(web_platform, target_feature = "exception-handling")),
|
||||||
doc = "[`run_app()`]: EventLoop::run_app()"
|
doc = "[`run_app()`]: EventLoop::run_app()"
|
||||||
)]
|
)]
|
||||||
/// [^1]: `run_app()` is _not_ available on WASM when the target supports `exception-handling`.
|
/// [^1]: `run_app()` is _not_ available on Wasm when the target supports `exception-handling`.
|
||||||
fn spawn_app<A: ApplicationHandler + 'static>(self, app: A);
|
fn spawn_app<A: ApplicationHandler + 'static>(self, app: A);
|
||||||
|
|
||||||
/// Sets the strategy for [`ControlFlow::Poll`].
|
/// Sets the strategy for [`ControlFlow::Poll`].
|
||||||
|
|
@ -398,7 +399,7 @@ impl fmt::Display for BadAnimation {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Empty => write!(f, "No cursors supplied"),
|
Self::Empty => write!(f, "No cursors supplied"),
|
||||||
Self::Animation => write!(f, "A supplied cursor is an animtion"),
|
Self::Animation => write!(f, "A supplied cursor is an animation"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,12 @@ pub struct Dispatcher<T: 'static>(Wrapper<T, Arc<Sender<Closure<T>>>, Closure<T>
|
||||||
struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
|
struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
|
||||||
|
|
||||||
impl<T> Dispatcher<T> {
|
impl<T> Dispatcher<T> {
|
||||||
#[track_caller]
|
pub fn new(main_thread: MainThreadMarker, value: T) -> (Self, DispatchRunner<T>) {
|
||||||
pub fn new(main_thread: MainThreadMarker, value: T) -> Option<(Self, DispatchRunner<T>)> {
|
|
||||||
let (sender, receiver) = channel::<Closure<T>>();
|
let (sender, receiver) = channel::<Closure<T>>();
|
||||||
let sender = Arc::new(sender);
|
let sender = Arc::new(sender);
|
||||||
let receiver = Rc::new(receiver);
|
let receiver = Rc::new(receiver);
|
||||||
|
|
||||||
Wrapper::new(
|
let wrapper = Wrapper::new(
|
||||||
main_thread,
|
main_thread,
|
||||||
value,
|
value,
|
||||||
|value, Closure(closure)| {
|
|value, Closure(closure)| {
|
||||||
|
|
@ -29,8 +28,7 @@ impl<T> Dispatcher<T> {
|
||||||
move |value| async move {
|
move |value| async move {
|
||||||
while let Ok(Closure(closure)) = receiver.next().await {
|
while let Ok(Closure(closure)) = receiver.next().await {
|
||||||
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't
|
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't
|
||||||
// do anything funny with it here. See
|
// do anything funny with it here. See `Self::queue()`.
|
||||||
// `Self::queue()`.
|
|
||||||
closure(value.borrow().as_ref().unwrap())
|
closure(value.borrow().as_ref().unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -41,25 +39,25 @@ impl<T> Dispatcher<T> {
|
||||||
// anything funny with it here. See `Self::queue()`.
|
// anything funny with it here. See `Self::queue()`.
|
||||||
sender.send(closure).unwrap()
|
sender.send(closure).unwrap()
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
.map(|wrapper| (Self(wrapper.clone()), DispatchRunner { wrapper, receiver }))
|
(Self(wrapper.clone()), DispatchRunner { wrapper, receiver })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(&self) -> Option<Ref<'_, T>> {
|
pub fn value(&self, main_thread: MainThreadMarker) -> Ref<'_, T> {
|
||||||
self.0.value()
|
self.0.value(main_thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) {
|
pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) {
|
||||||
if let Some(value) = self.0.value() {
|
if let Some(main_thread) = MainThreadMarker::new() {
|
||||||
f(&value)
|
f(&self.0.value(main_thread))
|
||||||
} else {
|
} else {
|
||||||
self.0.send(Closure(Box::new(f)))
|
self.0.send(Closure(Box::new(f)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn queue<R: Send>(&self, f: impl FnOnce(&T) -> R + Send) -> R {
|
pub fn queue<R: Send>(&self, f: impl FnOnce(&T) -> R + Send) -> R {
|
||||||
if let Some(value) = self.0.value() {
|
if let Some(main_thread) = MainThreadMarker::new() {
|
||||||
f(&value)
|
f(&self.0.value(main_thread))
|
||||||
} else {
|
} else {
|
||||||
let pair = Arc::new((Mutex::new(None), Condvar::new()));
|
let pair = Arc::new((Mutex::new(None), Condvar::new()));
|
||||||
let closure = Box::new({
|
let closure = Box::new({
|
||||||
|
|
@ -98,13 +96,13 @@ pub struct DispatchRunner<T: 'static> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DispatchRunner<T> {
|
impl<T> DispatchRunner<T> {
|
||||||
pub fn run(&self) {
|
pub fn run(&self, main_thread: MainThreadMarker) {
|
||||||
while let Some(Closure(closure)) =
|
while let Some(Closure(closure)) =
|
||||||
self.receiver.try_recv().expect("should only be closed when `Dispatcher` is dropped")
|
self.receiver.try_recv().expect("should only be closed when `Dispatcher` is dropped")
|
||||||
{
|
{
|
||||||
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything
|
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything
|
||||||
// funny with it here. See `Self::queue()`.
|
// funny with it here. See `Self::queue()`.
|
||||||
closure(&self.wrapper.value().expect("don't call this outside the main thread"))
|
closure(&self.wrapper.value(main_thread))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,6 @@ impl<T: Clone> Notifier<T> {
|
||||||
if self.0.value.set(value).is_err() {
|
if self.0.value.set(value).is_err() {
|
||||||
unreachable!("value set before")
|
unreachable!("value set before")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.0.queue.close();
|
|
||||||
|
|
||||||
while let Ok(waker) = self.0.queue.pop() {
|
|
||||||
waker.wake()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notified(&self) -> Notified<T> {
|
pub fn notified(&self) -> Notified<T> {
|
||||||
|
|
@ -30,11 +24,21 @@ impl<T: Clone> Notifier<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Drop for Notifier<T> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.queue.close();
|
||||||
|
|
||||||
|
while let Ok(waker) = self.0.queue.pop() {
|
||||||
|
waker.wake()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Notified<T: Clone>(Option<Arc<Inner<T>>>);
|
pub struct Notified<T: Clone>(Option<Arc<Inner<T>>>);
|
||||||
|
|
||||||
impl<T: Clone> Future for Notified<T> {
|
impl<T: Clone> Future for Notified<T> {
|
||||||
type Output = T;
|
type Output = Option<T>;
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
let this = self.0.take().expect("`Receiver` polled after completion");
|
let this = self.0.take().expect("`Receiver` polled after completion");
|
||||||
|
|
@ -54,14 +58,13 @@ impl<T: Clone> Future for Notified<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (Ok(Some(value)) | Err(Some(value))) = Arc::try_unwrap(this)
|
match Arc::try_unwrap(this)
|
||||||
.map(|mut inner| inner.value.take())
|
.map(|mut inner| inner.value.take())
|
||||||
.map_err(|this| this.value.get().cloned())
|
.map_err(|this| this.value.get().cloned())
|
||||||
else {
|
{
|
||||||
unreachable!("found no value despite being ready")
|
Ok(Some(value)) | Err(Some(value)) => Poll::Ready(Some(value)),
|
||||||
};
|
_ => Poll::Ready(None),
|
||||||
|
}
|
||||||
Poll::Ready(value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ struct Handler<T> {
|
||||||
struct Sender(Arc<Inner>);
|
struct Sender(Arc<Inner>);
|
||||||
|
|
||||||
impl<T> WakerSpawner<T> {
|
impl<T> WakerSpawner<T> {
|
||||||
#[track_caller]
|
pub fn new(main_thread: MainThreadMarker, value: T, handler: fn(&T, bool)) -> Self {
|
||||||
pub fn new(main_thread: MainThreadMarker, value: T, handler: fn(&T, bool)) -> Option<Self> {
|
|
||||||
let inner = Arc::new(Inner {
|
let inner = Arc::new(Inner {
|
||||||
awoken: AtomicBool::new(false),
|
awoken: AtomicBool::new(false),
|
||||||
waker: AtomicWaker::new(),
|
waker: AtomicWaker::new(),
|
||||||
|
|
@ -31,7 +30,7 @@ impl<T> WakerSpawner<T> {
|
||||||
|
|
||||||
let sender = Sender(Arc::clone(&inner));
|
let sender = Sender(Arc::clone(&inner));
|
||||||
|
|
||||||
let wrapper = Wrapper::new(
|
Self(Wrapper::new(
|
||||||
main_thread,
|
main_thread,
|
||||||
handler,
|
handler,
|
||||||
|handler, _| {
|
|handler, _| {
|
||||||
|
|
@ -73,9 +72,7 @@ impl<T> WakerSpawner<T> {
|
||||||
inner.0.awoken.store(true, Ordering::Relaxed);
|
inner.0.awoken.store(true, Ordering::Relaxed);
|
||||||
inner.0.waker.wake();
|
inner.0.waker.wake();
|
||||||
},
|
},
|
||||||
)?;
|
))
|
||||||
|
|
||||||
Some(Self(wrapper))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn waker(&self) -> Waker<T> {
|
pub fn waker(&self) -> Waker<T> {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ unsafe impl<V> Send for Value<V> {}
|
||||||
unsafe impl<V> Sync for Value<V> {}
|
unsafe impl<V> Sync for Value<V> {}
|
||||||
|
|
||||||
impl<V, S: Clone + Send, E> Wrapper<V, S, E> {
|
impl<V, S: Clone + Send, E> Wrapper<V, S, E> {
|
||||||
#[track_caller]
|
|
||||||
pub fn new<R: Future<Output = ()>>(
|
pub fn new<R: Future<Output = ()>>(
|
||||||
_: MainThreadMarker,
|
_: MainThreadMarker,
|
||||||
value: V,
|
value: V,
|
||||||
|
|
@ -41,7 +40,7 @@ impl<V, S: Clone + Send, E> Wrapper<V, S, E> {
|
||||||
receiver: impl 'static + FnOnce(Arc<RefCell<Option<V>>>) -> R,
|
receiver: impl 'static + FnOnce(Arc<RefCell<Option<V>>>) -> R,
|
||||||
sender_data: S,
|
sender_data: S,
|
||||||
sender_handler: fn(&S, E),
|
sender_handler: fn(&S, E),
|
||||||
) -> Option<Self> {
|
) -> Self {
|
||||||
let value = Arc::new(RefCell::new(Some(value)));
|
let value = Arc::new(RefCell::new(Some(value)));
|
||||||
|
|
||||||
wasm_bindgen_futures::spawn_local({
|
wasm_bindgen_futures::spawn_local({
|
||||||
|
|
@ -52,12 +51,7 @@ impl<V, S: Clone + Send, E> Wrapper<V, S, E> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(Self {
|
Self { value: Value { value, local: PhantomData }, handler, sender_data, sender_handler }
|
||||||
value: Value { value, local: PhantomData },
|
|
||||||
handler,
|
|
||||||
sender_data,
|
|
||||||
sender_handler,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&self, event: E) {
|
pub fn send(&self, event: E) {
|
||||||
|
|
@ -68,9 +62,8 @@ impl<V, S: Clone + Send, E> Wrapper<V, S, E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(&self) -> Option<Ref<'_, V>> {
|
pub fn value(&self, _: MainThreadMarker) -> Ref<'_, V> {
|
||||||
MainThreadMarker::new()
|
Ref::map(self.value.value.borrow(), |value| value.as_ref().unwrap())
|
||||||
.map(|_| Ref::map(self.value.value.borrow(), |value| value.as_ref().unwrap()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_sender_data<T>(&self, f: impl FnOnce(&S) -> T) -> T {
|
pub fn with_sender_data<T>(&self, f: impl FnOnce(&S) -> T) -> T {
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ impl Future for CustomCursorFuture {
|
||||||
panic!("`CustomCursorFuture` polled after completion")
|
panic!("`CustomCursorFuture` polled after completion")
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = ready!(Pin::new(&mut self.notified).poll(cx));
|
let result = ready!(Pin::new(&mut self.notified).poll(cx)).unwrap();
|
||||||
let state = self.state.take().expect("`CustomCursorFuture` polled after completion");
|
let state = self.state.take().expect("`CustomCursorFuture` polled after completion");
|
||||||
|
|
||||||
Poll::Ready(result.map(|_| CustomCursor { animation: self.animation, state }))
|
Poll::Ready(result.map(|_| CustomCursor { animation: self.animation, state }))
|
||||||
|
|
@ -662,7 +662,7 @@ async fn from_animation(
|
||||||
ImageState::Loading { notifier, .. } => {
|
ImageState::Loading { notifier, .. } => {
|
||||||
let notified = notifier.notified();
|
let notified = notifier.notified();
|
||||||
drop(state);
|
drop(state);
|
||||||
notified.await?;
|
notified.await.unwrap()?;
|
||||||
},
|
},
|
||||||
ImageState::Failed(error) => return Err(error.clone()),
|
ImageState::Failed(error) => return Err(error.clone()),
|
||||||
ImageState::Image(_) => drop(state),
|
ImageState::Image(_) => drop(state),
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
use std::rc::Weak;
|
use super::runner::WeakShared;
|
||||||
|
|
||||||
use super::runner::Execution;
|
|
||||||
use crate::platform_impl::platform::r#async::Waker;
|
use crate::platform_impl::platform::r#async::Waker;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EventLoopProxy {
|
pub struct EventLoopProxy {
|
||||||
runner: Waker<Weak<Execution>>,
|
runner: Waker<WeakShared>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventLoopProxy {
|
impl EventLoopProxy {
|
||||||
pub fn new(runner: Waker<Weak<Execution>>) -> Self {
|
pub fn new(runner: Waker<WeakShared>) -> Self {
|
||||||
Self { runner }
|
Self { runner }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ impl Clone for Shared {
|
||||||
|
|
||||||
type OnEventHandle<T> = RefCell<Option<EventListenerHandle<dyn FnMut(T)>>>;
|
type OnEventHandle<T> = RefCell<Option<EventListenerHandle<dyn FnMut(T)>>>;
|
||||||
|
|
||||||
pub struct Execution {
|
struct Execution {
|
||||||
main_thread: MainThreadMarker,
|
main_thread: MainThreadMarker,
|
||||||
proxy_spawner: WakerSpawner<Weak<Self>>,
|
proxy_spawner: WakerSpawner<WeakShared>,
|
||||||
control_flow: Cell<ControlFlow>,
|
control_flow: Cell<ControlFlow>,
|
||||||
poll_strategy: Cell<PollStrategy>,
|
poll_strategy: Cell<PollStrategy>,
|
||||||
wait_until_strategy: Cell<WaitUntilStrategy>,
|
wait_until_strategy: Cell<WaitUntilStrategy>,
|
||||||
|
|
@ -53,7 +53,7 @@ pub struct Execution {
|
||||||
window: web_sys::Window,
|
window: web_sys::Window,
|
||||||
document: Document,
|
document: Document,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
all_canvases: RefCell<Vec<(WindowId, Weak<RefCell<backend::Canvas>>, DispatchRunner<Inner>)>>,
|
all_canvases: RefCell<Vec<(WindowId, Weak<backend::Canvas>, DispatchRunner<Inner>)>>,
|
||||||
redraw_pending: RefCell<HashSet<WindowId>>,
|
redraw_pending: RefCell<HashSet<WindowId>>,
|
||||||
destroy_pending: RefCell<VecDeque<WindowId>>,
|
destroy_pending: RefCell<VecDeque<WindowId>>,
|
||||||
page_transition_event_handle: RefCell<Option<backend::PageTransitionEventHandle>>,
|
page_transition_event_handle: RefCell<Option<backend::PageTransitionEventHandle>>,
|
||||||
|
|
@ -116,7 +116,7 @@ impl Runner {
|
||||||
EventWrapper::Event(event) => (self.event_handler)(event),
|
EventWrapper::Event(event) => (self.event_handler)(event),
|
||||||
EventWrapper::ScaleChange { canvas, size, scale } => {
|
EventWrapper::ScaleChange { canvas, size, scale } => {
|
||||||
if let Some(canvas) = canvas.upgrade() {
|
if let Some(canvas) = canvas.upgrade() {
|
||||||
canvas.borrow().handle_scale_change(
|
canvas.handle_scale_change(
|
||||||
runner,
|
runner,
|
||||||
|event| (self.event_handler)(event),
|
|event| (self.event_handler)(event),
|
||||||
size,
|
size,
|
||||||
|
|
@ -137,12 +137,12 @@ impl Shared {
|
||||||
let document = window.document().expect("Failed to obtain document");
|
let document = window.document().expect("Failed to obtain document");
|
||||||
|
|
||||||
Shared(Rc::<Execution>::new_cyclic(|weak| {
|
Shared(Rc::<Execution>::new_cyclic(|weak| {
|
||||||
let proxy_spawner = WakerSpawner::new(main_thread, weak.clone(), |runner, local| {
|
let proxy_spawner =
|
||||||
if let Some(runner) = runner.upgrade() {
|
WakerSpawner::new(main_thread, WeakShared(weak.clone()), |runner, local| {
|
||||||
Shared(runner).send_proxy_wake_up(local);
|
if let Some(runner) = runner.upgrade() {
|
||||||
}
|
runner.send_proxy_wake_up(local);
|
||||||
})
|
}
|
||||||
.expect("`EventLoop` has to be created in the main thread");
|
});
|
||||||
|
|
||||||
Execution {
|
Execution {
|
||||||
main_thread,
|
main_thread,
|
||||||
|
|
@ -189,7 +189,7 @@ impl Shared {
|
||||||
pub fn add_canvas(
|
pub fn add_canvas(
|
||||||
&self,
|
&self,
|
||||||
id: WindowId,
|
id: WindowId,
|
||||||
canvas: Weak<RefCell<backend::Canvas>>,
|
canvas: Weak<backend::Canvas>,
|
||||||
runner: DispatchRunner<Inner>,
|
runner: DispatchRunner<Inner>,
|
||||||
) {
|
) {
|
||||||
self.0.all_canvases.borrow_mut().push((id, canvas, runner));
|
self.0.all_canvases.borrow_mut().push((id, canvas, runner));
|
||||||
|
|
@ -391,7 +391,7 @@ impl Shared {
|
||||||
// - not visible and we don't know if it intersects yet
|
// - not visible and we don't know if it intersects yet
|
||||||
// - visible and intersects
|
// - visible and intersects
|
||||||
if let (false, Some(true) | None) | (true, Some(true)) =
|
if let (false, Some(true) | None) | (true, Some(true)) =
|
||||||
(is_visible, canvas.borrow().is_intersecting)
|
(is_visible, canvas.is_intersecting.get())
|
||||||
{
|
{
|
||||||
runner.send_event(Event::WindowEvent {
|
runner.send_event(Event::WindowEvent {
|
||||||
window_id: *id,
|
window_id: *id,
|
||||||
|
|
@ -616,7 +616,7 @@ impl Shared {
|
||||||
// and potentially block other threads in the meantime.
|
// and potentially block other threads in the meantime.
|
||||||
for (_, window, runner) in self.0.all_canvases.borrow().iter() {
|
for (_, window, runner) in self.0.all_canvases.borrow().iter() {
|
||||||
if let Some(window) = window.upgrade() {
|
if let Some(window) = window.upgrade() {
|
||||||
runner.run();
|
runner.run(self.main_thread());
|
||||||
drop(window)
|
drop(window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -705,7 +705,6 @@ impl Shared {
|
||||||
// In case any remaining `Window`s are still not dropped, we will need
|
// In case any remaining `Window`s are still not dropped, we will need
|
||||||
// to explicitly remove the event handlers associated with their canvases.
|
// to explicitly remove the event handlers associated with their canvases.
|
||||||
if let Some(canvas) = canvas.upgrade() {
|
if let Some(canvas) = canvas.upgrade() {
|
||||||
let mut canvas = canvas.borrow_mut();
|
|
||||||
canvas.remove_listeners();
|
canvas.remove_listeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -747,7 +746,7 @@ impl Shared {
|
||||||
DeviceEvents::WhenFocused => {
|
DeviceEvents::WhenFocused => {
|
||||||
self.0.all_canvases.borrow().iter().any(|(_, canvas, _)| {
|
self.0.all_canvases.borrow().iter().any(|(_, canvas, _)| {
|
||||||
if let Some(canvas) = canvas.upgrade() {
|
if let Some(canvas) = canvas.upgrade() {
|
||||||
canvas.borrow().has_focus.get()
|
canvas.has_focus.get()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -793,14 +792,23 @@ impl Shared {
|
||||||
self.0.wait_until_strategy.get()
|
self.0.wait_until_strategy.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn waker(&self) -> Waker<Weak<Execution>> {
|
pub(crate) fn waker(&self) -> Waker<WeakShared> {
|
||||||
self.0.proxy_spawner.waker()
|
self.0.proxy_spawner.waker()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WeakShared(Weak<Execution>);
|
||||||
|
|
||||||
|
impl WeakShared {
|
||||||
|
pub fn upgrade(&self) -> Option<Shared> {
|
||||||
|
self.0.upgrade().map(Shared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) enum EventWrapper {
|
pub(crate) enum EventWrapper {
|
||||||
Event(Event),
|
Event(Event),
|
||||||
ScaleChange { canvas: Weak<RefCell<backend::Canvas>>, size: PhysicalSize<u32>, scale: f64 },
|
ScaleChange { canvas: Weak<backend::Canvas>, size: PhysicalSize<u32>, scale: f64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Event> for EventWrapper {
|
impl From<Event> for EventWrapper {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::Cell;
|
||||||
use std::clone::Clone;
|
use std::clone::Clone;
|
||||||
use std::collections::vec_deque::IntoIter as VecDequeIter;
|
use std::collections::vec_deque::IntoIter as VecDequeIter;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::rc::{Rc, Weak};
|
use std::rc::Rc;
|
||||||
|
|
||||||
use web_sys::Element;
|
use web_sys::Element;
|
||||||
|
|
||||||
use super::super::monitor::MonitorHandle;
|
use super::super::monitor::MonitorHandle;
|
||||||
use super::super::KeyEventExtra;
|
use super::super::KeyEventExtra;
|
||||||
use super::device::DeviceId;
|
use super::device::DeviceId;
|
||||||
use super::runner::{EventWrapper, Execution};
|
use super::runner::{EventWrapper, WeakShared};
|
||||||
use super::window::WindowId;
|
use super::window::WindowId;
|
||||||
use super::{backend, runner, EventLoopProxy};
|
use super::{backend, runner, EventLoopProxy};
|
||||||
use crate::event::{
|
use crate::event::{
|
||||||
|
|
@ -80,9 +80,8 @@ impl ActiveEventLoop {
|
||||||
CustomCursorFuture(CustomCursor::new_async(self, source.inner))
|
CustomCursorFuture(CustomCursor::new_async(self, source.inner))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(&self, canvas: &Rc<RefCell<backend::Canvas>>, id: WindowId) {
|
pub fn register(&self, canvas: &Rc<backend::Canvas>, id: WindowId) {
|
||||||
let canvas_clone = canvas.clone();
|
let canvas_clone = canvas.clone();
|
||||||
let mut canvas = canvas.borrow_mut();
|
|
||||||
#[cfg(any(feature = "rwh_04", feature = "rwh_05"))]
|
#[cfg(any(feature = "rwh_04", feature = "rwh_05"))]
|
||||||
canvas.set_attribute("data-raw-handle", &id.0.to_string());
|
canvas.set_attribute("data-raw-handle", &id.0.to_string());
|
||||||
|
|
||||||
|
|
@ -561,7 +560,6 @@ impl ActiveEventLoop {
|
||||||
let canvas = canvas_clone.clone();
|
let canvas = canvas_clone.clone();
|
||||||
|
|
||||||
move |new_size| {
|
move |new_size| {
|
||||||
let canvas = canvas.borrow();
|
|
||||||
canvas.set_current_size(new_size);
|
canvas.set_current_size(new_size);
|
||||||
if canvas.old_size() != new_size {
|
if canvas.old_size() != new_size {
|
||||||
canvas.set_old_size(new_size);
|
canvas.set_old_size(new_size);
|
||||||
|
|
@ -579,7 +577,7 @@ impl ActiveEventLoop {
|
||||||
canvas.on_intersection(move |is_intersecting| {
|
canvas.on_intersection(move |is_intersecting| {
|
||||||
// only fire if visible while skipping the first event if it's intersecting
|
// only fire if visible while skipping the first event if it's intersecting
|
||||||
if backend::is_visible(runner.document())
|
if backend::is_visible(runner.document())
|
||||||
&& !(is_intersecting && canvas_clone.borrow().is_intersecting.is_none())
|
&& !(is_intersecting && canvas_clone.is_intersecting.get().is_none())
|
||||||
{
|
{
|
||||||
runner.send_event(Event::WindowEvent {
|
runner.send_event(Event::WindowEvent {
|
||||||
window_id: RootWindowId(id),
|
window_id: RootWindowId(id),
|
||||||
|
|
@ -587,7 +585,7 @@ impl ActiveEventLoop {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas_clone.borrow_mut().is_intersecting = Some(is_intersecting);
|
canvas_clone.is_intersecting.set(Some(is_intersecting));
|
||||||
});
|
});
|
||||||
|
|
||||||
let runner = self.runner.clone();
|
let runner = self.runner.clone();
|
||||||
|
|
@ -654,7 +652,7 @@ impl ActiveEventLoop {
|
||||||
self.runner.wait_until_strategy()
|
self.runner.wait_until_strategy()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn waker(&self) -> Waker<Weak<Execution>> {
|
pub(crate) fn waker(&self) -> Waker<WeakShared> {
|
||||||
self.runner.waker()
|
self.runner.waker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::cell::Cell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
@ -29,11 +29,18 @@ use crate::window::{WindowAttributes, WindowId as RootWindowId};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct Canvas {
|
pub struct Canvas {
|
||||||
|
main_thread: MainThreadMarker,
|
||||||
common: Common,
|
common: Common,
|
||||||
id: WindowId,
|
id: WindowId,
|
||||||
pub has_focus: Rc<Cell<bool>>,
|
pub has_focus: Rc<Cell<bool>>,
|
||||||
pub prevent_default: Rc<Cell<bool>>,
|
pub prevent_default: Rc<Cell<bool>>,
|
||||||
pub is_intersecting: Option<bool>,
|
pub is_intersecting: Cell<Option<bool>>,
|
||||||
|
pub cursor: CursorHandler,
|
||||||
|
handlers: RefCell<Handlers>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Handlers {
|
||||||
|
animation_frame_handler: AnimationFrameHandler,
|
||||||
on_touch_start: Option<EventListenerHandle<dyn FnMut(Event)>>,
|
on_touch_start: Option<EventListenerHandle<dyn FnMut(Event)>>,
|
||||||
on_focus: Option<EventListenerHandle<dyn FnMut(FocusEvent)>>,
|
on_focus: Option<EventListenerHandle<dyn FnMut(FocusEvent)>>,
|
||||||
on_blur: Option<EventListenerHandle<dyn FnMut(FocusEvent)>>,
|
on_blur: Option<EventListenerHandle<dyn FnMut(FocusEvent)>>,
|
||||||
|
|
@ -44,10 +51,8 @@ pub struct Canvas {
|
||||||
pointer_handler: PointerHandler,
|
pointer_handler: PointerHandler,
|
||||||
on_resize_scale: Option<ResizeScaleHandle>,
|
on_resize_scale: Option<ResizeScaleHandle>,
|
||||||
on_intersect: Option<IntersectionObserverHandle>,
|
on_intersect: Option<IntersectionObserverHandle>,
|
||||||
animation_frame_handler: AnimationFrameHandler,
|
|
||||||
on_touch_end: Option<EventListenerHandle<dyn FnMut(Event)>>,
|
on_touch_end: Option<EventListenerHandle<dyn FnMut(Event)>>,
|
||||||
on_context_menu: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
|
on_context_menu: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
|
||||||
pub cursor: CursorHandler,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Common {
|
pub struct Common {
|
||||||
|
|
@ -74,14 +79,11 @@ impl Canvas {
|
||||||
id: WindowId,
|
id: WindowId,
|
||||||
window: web_sys::Window,
|
window: web_sys::Window,
|
||||||
document: Document,
|
document: Document,
|
||||||
attr: &mut WindowAttributes,
|
attr: WindowAttributes,
|
||||||
) -> Result<Self, RootOE> {
|
) -> Result<Self, RootOE> {
|
||||||
let canvas = match attr.platform_specific.canvas.take().map(|canvas| {
|
let canvas = match attr.platform_specific.canvas.map(Arc::try_unwrap) {
|
||||||
Arc::try_unwrap(canvas)
|
Some(Ok(canvas)) => canvas.into_inner(main_thread),
|
||||||
.map(|canvas| canvas.into_inner(main_thread))
|
Some(Err(canvas)) => canvas.get(main_thread).clone(),
|
||||||
.unwrap_or_else(|canvas| canvas.get(main_thread).clone())
|
|
||||||
}) {
|
|
||||||
Some(canvas) => canvas,
|
|
||||||
None => document
|
None => document
|
||||||
.create_element("canvas")
|
.create_element("canvas")
|
||||||
.map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))?
|
.map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))?
|
||||||
|
|
@ -149,25 +151,28 @@ impl Canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Canvas {
|
Ok(Canvas {
|
||||||
|
main_thread,
|
||||||
common,
|
common,
|
||||||
id,
|
id,
|
||||||
has_focus: Rc::new(Cell::new(false)),
|
has_focus: Rc::new(Cell::new(false)),
|
||||||
prevent_default: Rc::new(Cell::new(attr.platform_specific.prevent_default)),
|
prevent_default: Rc::new(Cell::new(attr.platform_specific.prevent_default)),
|
||||||
is_intersecting: None,
|
is_intersecting: Cell::new(None),
|
||||||
on_touch_start: None,
|
|
||||||
on_blur: None,
|
|
||||||
on_focus: None,
|
|
||||||
on_keyboard_release: None,
|
|
||||||
on_keyboard_press: None,
|
|
||||||
on_mouse_wheel: None,
|
|
||||||
on_dark_mode: None,
|
|
||||||
pointer_handler: PointerHandler::new(),
|
|
||||||
on_resize_scale: None,
|
|
||||||
on_intersect: None,
|
|
||||||
animation_frame_handler: AnimationFrameHandler::new(window),
|
|
||||||
on_touch_end: None,
|
|
||||||
on_context_menu: None,
|
|
||||||
cursor,
|
cursor,
|
||||||
|
handlers: RefCell::new(Handlers {
|
||||||
|
animation_frame_handler: AnimationFrameHandler::new(window),
|
||||||
|
on_touch_start: None,
|
||||||
|
on_blur: None,
|
||||||
|
on_focus: None,
|
||||||
|
on_keyboard_release: None,
|
||||||
|
on_keyboard_press: None,
|
||||||
|
on_mouse_wheel: None,
|
||||||
|
on_dark_mode: None,
|
||||||
|
pointer_handler: PointerHandler::new(),
|
||||||
|
on_resize_scale: None,
|
||||||
|
on_intersect: None,
|
||||||
|
on_touch_end: None,
|
||||||
|
on_context_menu: None,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,39 +246,42 @@ impl Canvas {
|
||||||
&self.common.style
|
&self.common.style
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_touch_start(&mut self) {
|
pub fn on_touch_start(&self) {
|
||||||
let prevent_default = Rc::clone(&self.prevent_default);
|
let prevent_default = Rc::clone(&self.prevent_default);
|
||||||
self.on_touch_start = Some(self.common.add_event("touchstart", move |event: Event| {
|
self.handlers.borrow_mut().on_touch_start =
|
||||||
if prevent_default.get() {
|
Some(self.common.add_event("touchstart", move |event: Event| {
|
||||||
event.prevent_default();
|
if prevent_default.get() {
|
||||||
}
|
event.prevent_default();
|
||||||
}));
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_blur<F>(&mut self, mut handler: F)
|
pub fn on_blur<F>(&self, mut handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(),
|
F: 'static + FnMut(),
|
||||||
{
|
{
|
||||||
self.on_blur = Some(self.common.add_event("blur", move |_: FocusEvent| {
|
self.handlers.borrow_mut().on_blur =
|
||||||
handler();
|
Some(self.common.add_event("blur", move |_: FocusEvent| {
|
||||||
}));
|
handler();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_focus<F>(&mut self, mut handler: F)
|
pub fn on_focus<F>(&self, mut handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(),
|
F: 'static + FnMut(),
|
||||||
{
|
{
|
||||||
self.on_focus = Some(self.common.add_event("focus", move |_: FocusEvent| {
|
self.handlers.borrow_mut().on_focus =
|
||||||
handler();
|
Some(self.common.add_event("focus", move |_: FocusEvent| {
|
||||||
}));
|
handler();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_keyboard_release<F>(&mut self, mut handler: F)
|
pub fn on_keyboard_release<F>(&self, mut handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(PhysicalKey, Key, Option<SmolStr>, KeyLocation, bool, ModifiersState),
|
F: 'static + FnMut(PhysicalKey, Key, Option<SmolStr>, KeyLocation, bool, ModifiersState),
|
||||||
{
|
{
|
||||||
let prevent_default = Rc::clone(&self.prevent_default);
|
let prevent_default = Rc::clone(&self.prevent_default);
|
||||||
self.on_keyboard_release =
|
self.handlers.borrow_mut().on_keyboard_release =
|
||||||
Some(self.common.add_event("keyup", move |event: KeyboardEvent| {
|
Some(self.common.add_event("keyup", move |event: KeyboardEvent| {
|
||||||
if prevent_default.get() {
|
if prevent_default.get() {
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
|
@ -291,12 +299,12 @@ impl Canvas {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_keyboard_press<F>(&mut self, mut handler: F)
|
pub fn on_keyboard_press<F>(&self, mut handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(PhysicalKey, Key, Option<SmolStr>, KeyLocation, bool, ModifiersState),
|
F: 'static + FnMut(PhysicalKey, Key, Option<SmolStr>, KeyLocation, bool, ModifiersState),
|
||||||
{
|
{
|
||||||
let prevent_default = Rc::clone(&self.prevent_default);
|
let prevent_default = Rc::clone(&self.prevent_default);
|
||||||
self.on_keyboard_press =
|
self.handlers.borrow_mut().on_keyboard_press =
|
||||||
Some(self.common.add_event("keydown", move |event: KeyboardEvent| {
|
Some(self.common.add_event("keydown", move |event: KeyboardEvent| {
|
||||||
if prevent_default.get() {
|
if prevent_default.get() {
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
|
@ -314,34 +322,38 @@ impl Canvas {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_cursor_leave<F>(&mut self, handler: F)
|
pub fn on_cursor_leave<F>(&self, handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(ModifiersState, Option<i32>),
|
F: 'static + FnMut(ModifiersState, Option<i32>),
|
||||||
{
|
{
|
||||||
self.pointer_handler.on_cursor_leave(&self.common, handler)
|
self.handlers.borrow_mut().pointer_handler.on_cursor_leave(&self.common, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_cursor_enter<F>(&mut self, handler: F)
|
pub fn on_cursor_enter<F>(&self, handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(ModifiersState, Option<i32>),
|
F: 'static + FnMut(ModifiersState, Option<i32>),
|
||||||
{
|
{
|
||||||
self.pointer_handler.on_cursor_enter(&self.common, handler)
|
self.handlers.borrow_mut().pointer_handler.on_cursor_enter(&self.common, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_mouse_release<M, T>(&mut self, mouse_handler: M, touch_handler: T)
|
pub fn on_mouse_release<M, T>(&self, mouse_handler: M, touch_handler: T)
|
||||||
where
|
where
|
||||||
M: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, MouseButton),
|
M: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, MouseButton),
|
||||||
T: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, Force),
|
T: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, Force),
|
||||||
{
|
{
|
||||||
self.pointer_handler.on_mouse_release(&self.common, mouse_handler, touch_handler)
|
self.handlers.borrow_mut().pointer_handler.on_mouse_release(
|
||||||
|
&self.common,
|
||||||
|
mouse_handler,
|
||||||
|
touch_handler,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_mouse_press<M, T>(&mut self, mouse_handler: M, touch_handler: T)
|
pub fn on_mouse_press<M, T>(&self, mouse_handler: M, touch_handler: T)
|
||||||
where
|
where
|
||||||
M: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, MouseButton),
|
M: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, MouseButton),
|
||||||
T: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, Force),
|
T: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, Force),
|
||||||
{
|
{
|
||||||
self.pointer_handler.on_mouse_press(
|
self.handlers.borrow_mut().pointer_handler.on_mouse_press(
|
||||||
&self.common,
|
&self.common,
|
||||||
mouse_handler,
|
mouse_handler,
|
||||||
touch_handler,
|
touch_handler,
|
||||||
|
|
@ -349,14 +361,14 @@ impl Canvas {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_cursor_move<M, T, B>(&mut self, mouse_handler: M, touch_handler: T, button_handler: B)
|
pub fn on_cursor_move<M, T, B>(&self, mouse_handler: M, touch_handler: T, button_handler: B)
|
||||||
where
|
where
|
||||||
M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator<Item = PhysicalPosition<f64>>),
|
M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator<Item = PhysicalPosition<f64>>),
|
||||||
T: 'static
|
T: 'static
|
||||||
+ FnMut(ModifiersState, i32, &mut dyn Iterator<Item = (PhysicalPosition<f64>, Force)>),
|
+ FnMut(ModifiersState, i32, &mut dyn Iterator<Item = (PhysicalPosition<f64>, Force)>),
|
||||||
B: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, ButtonsState, MouseButton),
|
B: 'static + FnMut(ModifiersState, i32, PhysicalPosition<f64>, ButtonsState, MouseButton),
|
||||||
{
|
{
|
||||||
self.pointer_handler.on_cursor_move(
|
self.handlers.borrow_mut().pointer_handler.on_cursor_move(
|
||||||
&self.common,
|
&self.common,
|
||||||
mouse_handler,
|
mouse_handler,
|
||||||
touch_handler,
|
touch_handler,
|
||||||
|
|
@ -365,48 +377,49 @@ impl Canvas {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_touch_cancel<F>(&mut self, handler: F)
|
pub fn on_touch_cancel<F>(&self, handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(i32, PhysicalPosition<f64>, Force),
|
F: 'static + FnMut(i32, PhysicalPosition<f64>, Force),
|
||||||
{
|
{
|
||||||
self.pointer_handler.on_touch_cancel(&self.common, handler)
|
self.handlers.borrow_mut().pointer_handler.on_touch_cancel(&self.common, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_mouse_wheel<F>(&mut self, mut handler: F)
|
pub fn on_mouse_wheel<F>(&self, mut handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState),
|
F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState),
|
||||||
{
|
{
|
||||||
let window = self.common.window.clone();
|
let window = self.common.window.clone();
|
||||||
let prevent_default = Rc::clone(&self.prevent_default);
|
let prevent_default = Rc::clone(&self.prevent_default);
|
||||||
self.on_mouse_wheel = Some(self.common.add_event("wheel", move |event: WheelEvent| {
|
self.handlers.borrow_mut().on_mouse_wheel =
|
||||||
if prevent_default.get() {
|
Some(self.common.add_event("wheel", move |event: WheelEvent| {
|
||||||
event.prevent_default();
|
if prevent_default.get() {
|
||||||
}
|
event.prevent_default();
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(delta) = event::mouse_scroll_delta(&window, &event) {
|
if let Some(delta) = event::mouse_scroll_delta(&window, &event) {
|
||||||
let modifiers = event::mouse_modifiers(&event);
|
let modifiers = event::mouse_modifiers(&event);
|
||||||
handler(0, delta, modifiers);
|
handler(0, delta, modifiers);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_dark_mode<F>(&mut self, mut handler: F)
|
pub fn on_dark_mode<F>(&self, mut handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(bool),
|
F: 'static + FnMut(bool),
|
||||||
{
|
{
|
||||||
self.on_dark_mode = Some(MediaQueryListHandle::new(
|
self.handlers.borrow_mut().on_dark_mode = Some(MediaQueryListHandle::new(
|
||||||
&self.common.window,
|
&self.common.window,
|
||||||
"(prefers-color-scheme: dark)",
|
"(prefers-color-scheme: dark)",
|
||||||
move |mql| handler(mql.matches()),
|
move |mql| handler(mql.matches()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn on_resize_scale<S, R>(&mut self, scale_handler: S, size_handler: R)
|
pub(crate) fn on_resize_scale<S, R>(&self, scale_handler: S, size_handler: R)
|
||||||
where
|
where
|
||||||
S: 'static + Fn(PhysicalSize<u32>, f64),
|
S: 'static + Fn(PhysicalSize<u32>, f64),
|
||||||
R: 'static + Fn(PhysicalSize<u32>),
|
R: 'static + Fn(PhysicalSize<u32>),
|
||||||
{
|
{
|
||||||
self.on_resize_scale = Some(ResizeScaleHandle::new(
|
self.handlers.borrow_mut().on_resize_scale = Some(ResizeScaleHandle::new(
|
||||||
self.window().clone(),
|
self.window().clone(),
|
||||||
self.document().clone(),
|
self.document().clone(),
|
||||||
self.raw().clone(),
|
self.raw().clone(),
|
||||||
|
|
@ -416,23 +429,24 @@ impl Canvas {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn on_intersection<F>(&mut self, handler: F)
|
pub(crate) fn on_intersection<F>(&self, handler: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(bool),
|
F: 'static + FnMut(bool),
|
||||||
{
|
{
|
||||||
self.on_intersect = Some(IntersectionObserverHandle::new(self.raw(), handler));
|
self.handlers.borrow_mut().on_intersect =
|
||||||
|
Some(IntersectionObserverHandle::new(self.raw(), handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn on_animation_frame<F>(&mut self, f: F)
|
pub(crate) fn on_animation_frame<F>(&self, f: F)
|
||||||
where
|
where
|
||||||
F: 'static + FnMut(),
|
F: 'static + FnMut(),
|
||||||
{
|
{
|
||||||
self.animation_frame_handler.on_animation_frame(f)
|
self.handlers.borrow_mut().animation_frame_handler.on_animation_frame(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn on_context_menu(&mut self) {
|
pub(crate) fn on_context_menu(&self) {
|
||||||
let prevent_default = Rc::clone(&self.prevent_default);
|
let prevent_default = Rc::clone(&self.prevent_default);
|
||||||
self.on_context_menu =
|
self.handlers.borrow_mut().on_context_menu =
|
||||||
Some(self.common.add_event("contextmenu", move |event: PointerEvent| {
|
Some(self.common.add_event("contextmenu", move |event: PointerEvent| {
|
||||||
if prevent_default.get() {
|
if prevent_default.get() {
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
|
@ -453,7 +467,7 @@ impl Canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_animation_frame(&self) {
|
pub fn request_animation_frame(&self) {
|
||||||
self.animation_frame_handler.request();
|
self.handlers.borrow().animation_frame_handler.request();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_scale_change(
|
pub(crate) fn handle_scale_change(
|
||||||
|
|
@ -486,7 +500,9 @@ impl Canvas {
|
||||||
super::set_canvas_size(self.document(), self.raw(), self.style(), new_size);
|
super::set_canvas_size(self.document(), self.raw(), self.style(), new_size);
|
||||||
|
|
||||||
// Set the size might not trigger the event because the calculation is inaccurate.
|
// Set the size might not trigger the event because the calculation is inaccurate.
|
||||||
self.on_resize_scale
|
self.handlers
|
||||||
|
.borrow()
|
||||||
|
.on_resize_scale
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("expected Window to still be active")
|
.expect("expected Window to still be active")
|
||||||
.notify_resize();
|
.notify_resize();
|
||||||
|
|
@ -500,20 +516,21 @@ impl Canvas {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_listeners(&mut self) {
|
pub fn remove_listeners(&self) {
|
||||||
self.on_touch_start = None;
|
let mut handlers = self.handlers.borrow_mut();
|
||||||
self.on_focus = None;
|
handlers.on_touch_start.take();
|
||||||
self.on_blur = None;
|
handlers.on_focus.take();
|
||||||
self.on_keyboard_release = None;
|
handlers.on_blur.take();
|
||||||
self.on_keyboard_press = None;
|
handlers.on_keyboard_release.take();
|
||||||
self.on_mouse_wheel = None;
|
handlers.on_keyboard_press.take();
|
||||||
self.on_dark_mode = None;
|
handlers.on_mouse_wheel.take();
|
||||||
self.pointer_handler.remove_listeners();
|
handlers.on_dark_mode.take();
|
||||||
self.on_resize_scale = None;
|
handlers.pointer_handler.remove_listeners();
|
||||||
self.on_intersect = None;
|
handlers.on_resize_scale = None;
|
||||||
self.animation_frame_handler.cancel();
|
handlers.on_intersect = None;
|
||||||
self.on_touch_end = None;
|
handlers.animation_frame_handler.cancel();
|
||||||
self.on_context_menu = None;
|
handlers.on_touch_end = None;
|
||||||
|
handlers.on_context_menu = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::cell::RefCell;
|
use std::cell::Ref;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -24,15 +24,12 @@ pub struct Window {
|
||||||
pub struct Inner {
|
pub struct Inner {
|
||||||
id: WindowId,
|
id: WindowId,
|
||||||
pub window: web_sys::Window,
|
pub window: web_sys::Window,
|
||||||
canvas: Rc<RefCell<backend::Canvas>>,
|
canvas: Rc<backend::Canvas>,
|
||||||
destroy_fn: Option<Box<dyn FnOnce()>>,
|
destroy_fn: Option<Box<dyn FnOnce()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(target: &ActiveEventLoop, attr: WindowAttributes) -> Result<Self, RootOE> {
|
||||||
target: &ActiveEventLoop,
|
|
||||||
mut attr: WindowAttributes,
|
|
||||||
) -> Result<Self, RootOE> {
|
|
||||||
let id = target.generate_id();
|
let id = target.generate_id();
|
||||||
|
|
||||||
let window = target.runner.window();
|
let window = target.runner.window();
|
||||||
|
|
@ -42,9 +39,9 @@ impl Window {
|
||||||
id,
|
id,
|
||||||
window.clone(),
|
window.clone(),
|
||||||
document.clone(),
|
document.clone(),
|
||||||
&mut attr,
|
attr,
|
||||||
)?;
|
)?;
|
||||||
let canvas = Rc::new(RefCell::new(canvas));
|
let canvas = Rc::new(canvas);
|
||||||
|
|
||||||
target.register(&canvas, id);
|
target.register(&canvas, id);
|
||||||
|
|
||||||
|
|
@ -53,14 +50,8 @@ impl Window {
|
||||||
|
|
||||||
let inner = Inner { id, window: window.clone(), canvas, destroy_fn: Some(destroy_fn) };
|
let inner = Inner { id, window: window.clone(), canvas, destroy_fn: Some(destroy_fn) };
|
||||||
|
|
||||||
inner.set_title(&attr.title);
|
|
||||||
inner.set_maximized(attr.maximized);
|
|
||||||
inner.set_visible(attr.visible);
|
|
||||||
inner.set_window_icon(attr.window_icon);
|
|
||||||
inner.set_cursor(attr.cursor);
|
|
||||||
|
|
||||||
let canvas = Rc::downgrade(&inner.canvas);
|
let canvas = Rc::downgrade(&inner.canvas);
|
||||||
let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner).unwrap();
|
let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner);
|
||||||
target.runner.add_canvas(RootWI(id), canvas, runner);
|
target.runner.add_canvas(RootWI(id), canvas, runner);
|
||||||
|
|
||||||
Ok(Window { inner: dispatcher })
|
Ok(Window { inner: dispatcher })
|
||||||
|
|
@ -74,27 +65,27 @@ impl Window {
|
||||||
self.inner.queue(f)
|
self.inner.queue(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn canvas(&self) -> Option<HtmlCanvasElement> {
|
pub fn canvas(&self) -> Option<Ref<'_, HtmlCanvasElement>> {
|
||||||
self.inner.value().map(|inner| inner.canvas.borrow().raw().clone())
|
MainThreadMarker::new()
|
||||||
|
.map(|main_thread| Ref::map(self.inner.value(main_thread), |inner| inner.canvas.raw()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn prevent_default(&self) -> bool {
|
pub(crate) fn prevent_default(&self) -> bool {
|
||||||
self.inner.queue(|inner| inner.canvas.borrow().prevent_default.get())
|
self.inner.queue(|inner| inner.canvas.prevent_default.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_prevent_default(&self, prevent_default: bool) {
|
pub(crate) fn set_prevent_default(&self, prevent_default: bool) {
|
||||||
self.inner.dispatch(move |inner| inner.canvas.borrow().prevent_default.set(prevent_default))
|
self.inner.dispatch(move |inner| inner.canvas.prevent_default.set(prevent_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rwh_06")]
|
#[cfg(feature = "rwh_06")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
|
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
|
||||||
self.inner
|
MainThreadMarker::new()
|
||||||
.value()
|
.map(|main_thread| {
|
||||||
.map(|inner| {
|
let inner = self.inner.value(main_thread);
|
||||||
let canvas = inner.canvas.borrow();
|
|
||||||
// SAFETY: This will only work if the reference to `HtmlCanvasElement` stays valid.
|
// SAFETY: This will only work if the reference to `HtmlCanvasElement` stays valid.
|
||||||
let canvas: &wasm_bindgen::JsValue = canvas.raw();
|
let canvas: &wasm_bindgen::JsValue = inner.canvas.raw();
|
||||||
let window_handle =
|
let window_handle =
|
||||||
rwh_06::WebCanvasWindowHandle::new(std::ptr::NonNull::from(canvas).cast());
|
rwh_06::WebCanvasWindowHandle::new(std::ptr::NonNull::from(canvas).cast());
|
||||||
rwh_06::RawWindowHandle::WebCanvas(window_handle)
|
rwh_06::RawWindowHandle::WebCanvas(window_handle)
|
||||||
|
|
@ -113,7 +104,7 @@ impl Window {
|
||||||
|
|
||||||
impl Inner {
|
impl Inner {
|
||||||
pub fn set_title(&self, title: &str) {
|
pub fn set_title(&self, title: &str) {
|
||||||
self.canvas.borrow().set_attribute("alt", title)
|
self.canvas.set_attribute("alt", title)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_transparent(&self, _transparent: bool) {}
|
pub fn set_transparent(&self, _transparent: bool) {}
|
||||||
|
|
@ -130,13 +121,13 @@ impl Inner {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_redraw(&self) {
|
pub fn request_redraw(&self) {
|
||||||
self.canvas.borrow().request_animation_frame();
|
self.canvas.request_animation_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pre_present_notify(&self) {}
|
pub fn pre_present_notify(&self) {}
|
||||||
|
|
||||||
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
||||||
Ok(self.canvas.borrow().position().to_physical(self.scale_factor()))
|
Ok(self.canvas.position().to_physical(self.scale_factor()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
||||||
|
|
@ -145,15 +136,19 @@ impl Inner {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_outer_position(&self, position: Position) {
|
pub fn set_outer_position(&self, position: Position) {
|
||||||
let canvas = self.canvas.borrow();
|
|
||||||
let position = position.to_logical::<f64>(self.scale_factor());
|
let position = position.to_logical::<f64>(self.scale_factor());
|
||||||
|
|
||||||
backend::set_canvas_position(canvas.document(), canvas.raw(), canvas.style(), position)
|
backend::set_canvas_position(
|
||||||
|
self.canvas.document(),
|
||||||
|
self.canvas.raw(),
|
||||||
|
self.canvas.style(),
|
||||||
|
position,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn inner_size(&self) -> PhysicalSize<u32> {
|
pub fn inner_size(&self) -> PhysicalSize<u32> {
|
||||||
self.canvas.borrow().inner_size()
|
self.canvas.inner_size()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -165,23 +160,35 @@ impl Inner {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn request_inner_size(&self, size: Size) -> Option<PhysicalSize<u32>> {
|
pub fn request_inner_size(&self, size: Size) -> Option<PhysicalSize<u32>> {
|
||||||
let size = size.to_logical(self.scale_factor());
|
let size = size.to_logical(self.scale_factor());
|
||||||
let canvas = self.canvas.borrow();
|
backend::set_canvas_size(
|
||||||
backend::set_canvas_size(canvas.document(), canvas.raw(), canvas.style(), size);
|
self.canvas.document(),
|
||||||
|
self.canvas.raw(),
|
||||||
|
self.canvas.style(),
|
||||||
|
size,
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_min_inner_size(&self, dimensions: Option<Size>) {
|
pub fn set_min_inner_size(&self, dimensions: Option<Size>) {
|
||||||
let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor()));
|
let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor()));
|
||||||
let canvas = self.canvas.borrow();
|
backend::set_canvas_min_size(
|
||||||
backend::set_canvas_min_size(canvas.document(), canvas.raw(), canvas.style(), dimensions)
|
self.canvas.document(),
|
||||||
|
self.canvas.raw(),
|
||||||
|
self.canvas.style(),
|
||||||
|
dimensions,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_max_inner_size(&self, dimensions: Option<Size>) {
|
pub fn set_max_inner_size(&self, dimensions: Option<Size>) {
|
||||||
let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor()));
|
let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor()));
|
||||||
let canvas = self.canvas.borrow();
|
backend::set_canvas_max_size(
|
||||||
backend::set_canvas_max_size(canvas.document(), canvas.raw(), canvas.style(), dimensions)
|
self.canvas.document(),
|
||||||
|
self.canvas.raw(),
|
||||||
|
self.canvas.style(),
|
||||||
|
dimensions,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -218,7 +225,7 @@ impl Inner {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_cursor(&self, cursor: Cursor) {
|
pub fn set_cursor(&self, cursor: Cursor) {
|
||||||
self.canvas.borrow_mut().cursor.set_cursor(cursor)
|
self.canvas.cursor.set_cursor(cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -236,12 +243,12 @@ impl Inner {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
self.canvas.borrow().set_cursor_lock(lock).map_err(ExternalError::Os)
|
self.canvas.set_cursor_lock(lock).map_err(ExternalError::Os)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_cursor_visible(&self, visible: bool) {
|
pub fn set_cursor_visible(&self, visible: bool) {
|
||||||
self.canvas.borrow_mut().cursor.set_cursor_visible(visible)
|
self.canvas.cursor.set_cursor_visible(visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -286,7 +293,7 @@ impl Inner {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn fullscreen(&self) -> Option<Fullscreen> {
|
pub(crate) fn fullscreen(&self) -> Option<Fullscreen> {
|
||||||
if self.canvas.borrow().is_fullscreen() {
|
if self.canvas.is_fullscreen() {
|
||||||
Some(Fullscreen::Borderless(None))
|
Some(Fullscreen::Borderless(None))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -295,12 +302,10 @@ impl Inner {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
|
pub(crate) fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
|
||||||
let canvas = &self.canvas.borrow();
|
|
||||||
|
|
||||||
if fullscreen.is_some() {
|
if fullscreen.is_some() {
|
||||||
canvas.request_fullscreen();
|
self.canvas.request_fullscreen();
|
||||||
} else {
|
} else {
|
||||||
canvas.exit_fullscreen()
|
self.canvas.exit_fullscreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,7 +345,7 @@ impl Inner {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn focus_window(&self) {
|
pub fn focus_window(&self) {
|
||||||
let _ = self.canvas.borrow().raw().focus();
|
let _ = self.canvas.raw().focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -408,7 +413,7 @@ impl Inner {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn has_focus(&self) -> bool {
|
pub fn has_focus(&self) -> bool {
|
||||||
self.canvas.borrow().has_focus.get()
|
self.canvas.has_focus.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title(&self) -> String {
|
pub fn title(&self) -> String {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue