eframe
: selectively expose parts of the API based on compile target (#1867)
A lot of the `eframe` API is native-only or web-only. With this PR, only the parts that are implemented for each platform is exposed. This means you'll need to add `#[cfg(target_arch = "wasm32")]` around code that uses the web-parts of the eframe API, and add `#[cfg(not(target_arch = "wasm32"))]` around the parts that are for native/desktop.
This commit is contained in:
parent
51052c08e9
commit
8c09804abd
7 changed files with 76 additions and 69 deletions
|
@ -14,6 +14,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
|
||||||
* Use `Arc` for `glow::Context` instead of `Rc` ([#1640](https://github.com/emilk/egui/pull/1640)).
|
* Use `Arc` for `glow::Context` instead of `Rc` ([#1640](https://github.com/emilk/egui/pull/1640)).
|
||||||
* Fixed bug where the result returned from `App::on_exit_event` would sometimes be ignored ([#1696](https://github.com/emilk/egui/pull/1696)).
|
* Fixed bug where the result returned from `App::on_exit_event` would sometimes be ignored ([#1696](https://github.com/emilk/egui/pull/1696)).
|
||||||
* Added `NativeOptions::follow_system_theme` and `NativeOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
|
* Added `NativeOptions::follow_system_theme` and `NativeOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
|
||||||
|
* Selectively expose parts of the API based on target arch (`wasm32` or not) ([#1867](https://github.com/emilk/egui/pull/1867)).
|
||||||
|
|
||||||
#### Desktop/Native:
|
#### Desktop/Native:
|
||||||
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
|
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
|
||||||
|
|
|
@ -476,21 +476,17 @@ pub struct IconData {
|
||||||
/// allocate textures, and change settings (e.g. window size).
|
/// allocate textures, and change settings (e.g. window size).
|
||||||
pub struct Frame {
|
pub struct Frame {
|
||||||
/// Information about the integration.
|
/// Information about the integration.
|
||||||
#[doc(hidden)]
|
pub(crate) info: IntegrationInfo,
|
||||||
pub info: IntegrationInfo,
|
|
||||||
|
|
||||||
/// Where the app can issue commands back to the integration.
|
/// Where the app can issue commands back to the integration.
|
||||||
#[doc(hidden)]
|
pub(crate) output: backend::AppOutput,
|
||||||
pub output: backend::AppOutput,
|
|
||||||
|
|
||||||
/// A place where you can store custom data in a way that persists when you restart the app.
|
/// A place where you can store custom data in a way that persists when you restart the app.
|
||||||
#[doc(hidden)]
|
pub(crate) storage: Option<Box<dyn Storage>>,
|
||||||
pub storage: Option<Box<dyn Storage>>,
|
|
||||||
|
|
||||||
/// A reference to the underlying [`glow`] (OpenGL) context.
|
/// A reference to the underlying [`glow`] (OpenGL) context.
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
#[doc(hidden)]
|
pub(crate) gl: Option<std::sync::Arc<glow::Context>>,
|
||||||
pub gl: Option<std::sync::Arc<glow::Context>>,
|
|
||||||
|
|
||||||
/// Can be used to manage GPU resources for custom rendering with WGPU using
|
/// Can be used to manage GPU resources for custom rendering with WGPU using
|
||||||
/// [`egui::PaintCallback`]s.
|
/// [`egui::PaintCallback`]s.
|
||||||
|
@ -500,8 +496,11 @@ pub struct Frame {
|
||||||
|
|
||||||
impl Frame {
|
impl Frame {
|
||||||
/// True if you are in a web environment.
|
/// True if you are in a web environment.
|
||||||
|
///
|
||||||
|
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
||||||
|
#[allow(clippy::unused_self)]
|
||||||
pub fn is_web(&self) -> bool {
|
pub fn is_web(&self) -> bool {
|
||||||
self.info.web_info.is_some()
|
cfg!(target_arch = "wasm32")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about the integration.
|
/// Information about the integration.
|
||||||
|
@ -538,16 +537,19 @@ impl Frame {
|
||||||
|
|
||||||
/// Signal the app to stop/exit/quit the app (only works for native apps, not web apps).
|
/// Signal the app to stop/exit/quit the app (only works for native apps, not web apps).
|
||||||
/// The framework will not quit immediately, but at the end of the this frame.
|
/// The framework will not quit immediately, but at the end of the this frame.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn quit(&mut self) {
|
pub fn quit(&mut self) {
|
||||||
self.output.quit = true;
|
self.output.quit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the desired inner size of the window (in egui points).
|
/// Set the desired inner size of the window (in egui points).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_window_size(&mut self, size: egui::Vec2) {
|
pub fn set_window_size(&mut self, size: egui::Vec2) {
|
||||||
self.output.window_size = Some(size);
|
self.output.window_size = Some(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the desired title of the window.
|
/// Set the desired title of the window.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_window_title(&mut self, title: &str) {
|
pub fn set_window_title(&mut self, title: &str) {
|
||||||
self.output.window_title = Some(title.to_owned());
|
self.output.window_title = Some(title.to_owned());
|
||||||
}
|
}
|
||||||
|
@ -555,16 +557,19 @@ impl Frame {
|
||||||
/// Set whether to show window decorations (i.e. a frame around you app).
|
/// Set whether to show window decorations (i.e. a frame around you app).
|
||||||
///
|
///
|
||||||
/// If false it will be difficult to move and resize the app.
|
/// If false it will be difficult to move and resize the app.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_decorations(&mut self, decorated: bool) {
|
pub fn set_decorations(&mut self, decorated: bool) {
|
||||||
self.output.decorated = Some(decorated);
|
self.output.decorated = Some(decorated);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turn borderless fullscreen on/off (native only).
|
/// Turn borderless fullscreen on/off (native only).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_fullscreen(&mut self, fullscreen: bool) {
|
pub fn set_fullscreen(&mut self, fullscreen: bool) {
|
||||||
self.output.fullscreen = Some(fullscreen);
|
self.output.fullscreen = Some(fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// set the position of the outer window
|
/// set the position of the outer window.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_window_pos(&mut self, pos: egui::Pos2) {
|
pub fn set_window_pos(&mut self, pos: egui::Pos2) {
|
||||||
self.output.window_pos = Some(pos);
|
self.output.window_pos = Some(pos);
|
||||||
}
|
}
|
||||||
|
@ -573,35 +578,41 @@ impl Frame {
|
||||||
/// movement of the cursor while the primary mouse button is down.
|
/// movement of the cursor while the primary mouse button is down.
|
||||||
///
|
///
|
||||||
/// Does not work on the web.
|
/// Does not work on the web.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn drag_window(&mut self) {
|
pub fn drag_window(&mut self) {
|
||||||
self.output.drag_window = true;
|
self.output.drag_window = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the visibility of the window.
|
/// Set the visibility of the window.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_visible(&mut self, visible: bool) {
|
pub fn set_visible(&mut self, visible: bool) {
|
||||||
self.output.visible = Some(visible);
|
self.output.visible = Some(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// for integrations only: call once per frame
|
/// for integrations only: call once per frame
|
||||||
#[doc(hidden)]
|
pub(crate) fn take_app_output(&mut self) -> backend::AppOutput {
|
||||||
pub fn take_app_output(&mut self) -> backend::AppOutput {
|
|
||||||
std::mem::take(&mut self.output)
|
std::mem::take(&mut self.output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about the web environment (if applicable).
|
/// Information about the web environment (if applicable).
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub struct WebInfo {
|
pub struct WebInfo {
|
||||||
/// Information about the URL.
|
/// Information about the URL.
|
||||||
pub location: Location,
|
pub location: Location,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about the application's main window, if available.
|
/// Information about the application's main window, if available.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct WindowInfo {
|
pub struct WindowInfo {
|
||||||
/// Coordinates of the window's outer top left corner, relative to the top left corner of the first display.
|
/// Coordinates of the window's outer top left corner, relative to the top left corner of the first display.
|
||||||
|
///
|
||||||
/// Unit: egui points (logical pixels).
|
/// Unit: egui points (logical pixels).
|
||||||
pub position: egui::Pos2,
|
///
|
||||||
|
/// `None` = unknown.
|
||||||
|
pub position: Option<egui::Pos2>,
|
||||||
|
|
||||||
/// Are we in fullscreen mode?
|
/// Are we in fullscreen mode?
|
||||||
pub fullscreen: bool,
|
pub fullscreen: bool,
|
||||||
|
@ -613,6 +624,7 @@ pub struct WindowInfo {
|
||||||
/// Information about the URL.
|
/// Information about the URL.
|
||||||
///
|
///
|
||||||
/// Everything has been percent decoded (`%20` -> ` ` etc).
|
/// Everything has been percent decoded (`%20` -> ` ` etc).
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Location {
|
pub struct Location {
|
||||||
/// The full URL (`location.href`) without the hash.
|
/// The full URL (`location.href`) without the hash.
|
||||||
|
@ -667,8 +679,9 @@ pub struct Location {
|
||||||
/// Information about the integration passed to the use app each frame.
|
/// Information about the integration passed to the use app each frame.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct IntegrationInfo {
|
pub struct IntegrationInfo {
|
||||||
/// If the app is running in a Web context, this returns information about the environment.
|
/// Information about the surrounding web environment.
|
||||||
pub web_info: Option<WebInfo>,
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub web_info: WebInfo,
|
||||||
|
|
||||||
/// Does the OS use dark or light mode?
|
/// Does the OS use dark or light mode?
|
||||||
///
|
///
|
||||||
|
@ -682,8 +695,9 @@ pub struct IntegrationInfo {
|
||||||
/// The OS native pixels-per-point
|
/// The OS native pixels-per-point
|
||||||
pub native_pixels_per_point: Option<f32>,
|
pub native_pixels_per_point: Option<f32>,
|
||||||
|
|
||||||
/// Window-specific geometry information, if provided by the platform.
|
/// The position and size of the native window.
|
||||||
pub window_info: Option<WindowInfo>,
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub window_info: WindowInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -736,35 +750,42 @@ pub const APP_KEY: &str = "app";
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// You only need to look here if you are writing a backend for `epi`.
|
/// You only need to look here if you are writing a backend for `epi`.
|
||||||
#[doc(hidden)]
|
pub(crate) mod backend {
|
||||||
pub mod backend {
|
|
||||||
/// Action that can be taken by the user app.
|
/// Action that can be taken by the user app.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct AppOutput {
|
pub struct AppOutput {
|
||||||
/// Set to `true` to stop the app.
|
/// Set to `true` to stop the app.
|
||||||
/// This does nothing for web apps.
|
/// This does nothing for web apps.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub quit: bool,
|
pub quit: bool,
|
||||||
|
|
||||||
/// Set to some size to resize the outer window (e.g. glium window) to this size.
|
/// Set to some size to resize the outer window (e.g. glium window) to this size.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub window_size: Option<egui::Vec2>,
|
pub window_size: Option<egui::Vec2>,
|
||||||
|
|
||||||
/// Set to some string to rename the outer window (e.g. glium window) to this title.
|
/// Set to some string to rename the outer window (e.g. glium window) to this title.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub window_title: Option<String>,
|
pub window_title: Option<String>,
|
||||||
|
|
||||||
/// Set to some bool to change window decorations.
|
/// Set to some bool to change window decorations.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub decorated: Option<bool>,
|
pub decorated: Option<bool>,
|
||||||
|
|
||||||
/// Set to some bool to change window fullscreen.
|
/// Set to some bool to change window fullscreen.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))] // TODO: implement fullscreen on web
|
||||||
pub fullscreen: Option<bool>,
|
pub fullscreen: Option<bool>,
|
||||||
|
|
||||||
/// Set to true to drag window while primary mouse button is down.
|
/// Set to true to drag window while primary mouse button is down.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub drag_window: bool,
|
pub drag_window: bool,
|
||||||
|
|
||||||
/// Set to some position to move the outer window (e.g. glium window) to this position
|
/// Set to some position to move the outer window (e.g. glium window) to this position
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub window_pos: Option<egui::Pos2>,
|
pub window_pos: Option<egui::Pos2>,
|
||||||
|
|
||||||
/// Set to some bool to change window visibility.
|
/// Set to some bool to change window visibility.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub visible: Option<bool>,
|
pub visible: Option<bool>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,26 +9,24 @@ pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_window_info(
|
pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -> WindowInfo {
|
||||||
window: &winit::window::Window,
|
let position = window
|
||||||
pixels_per_point: f32,
|
.outer_position()
|
||||||
) -> Option<WindowInfo> {
|
.ok()
|
||||||
match window.outer_position() {
|
.map(|pos| pos.to_logical::<f32>(pixels_per_point.into()))
|
||||||
Ok(pos) => {
|
.map(|pos| egui::Pos2 { x: pos.x, y: pos.y });
|
||||||
let pos = pos.to_logical::<f32>(pixels_per_point.into());
|
|
||||||
let size = window
|
let size = window
|
||||||
.inner_size()
|
.inner_size()
|
||||||
.to_logical::<f32>(pixels_per_point.into());
|
.to_logical::<f32>(pixels_per_point.into());
|
||||||
Some(WindowInfo {
|
|
||||||
position: egui::Pos2 { x: pos.x, y: pos.y },
|
WindowInfo {
|
||||||
fullscreen: window.fullscreen().is_some(),
|
position,
|
||||||
size: egui::Vec2 {
|
fullscreen: window.fullscreen().is_some(),
|
||||||
x: size.width,
|
size: egui::Vec2 {
|
||||||
y: size.height,
|
x: size.width,
|
||||||
},
|
y: size.height,
|
||||||
})
|
},
|
||||||
}
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +204,6 @@ impl EpiIntegration {
|
||||||
|
|
||||||
let frame = epi::Frame {
|
let frame = epi::Frame {
|
||||||
info: epi::IntegrationInfo {
|
info: epi::IntegrationInfo {
|
||||||
web_info: None,
|
|
||||||
system_theme,
|
system_theme,
|
||||||
cpu_usage: None,
|
cpu_usage: None,
|
||||||
native_pixels_per_point: Some(native_pixels_per_point(window)),
|
native_pixels_per_point: Some(native_pixels_per_point(window)),
|
||||||
|
|
|
@ -170,13 +170,12 @@ impl AppRunner {
|
||||||
};
|
};
|
||||||
|
|
||||||
let info = epi::IntegrationInfo {
|
let info = epi::IntegrationInfo {
|
||||||
web_info: Some(epi::WebInfo {
|
web_info: epi::WebInfo {
|
||||||
location: web_location(),
|
location: web_location(),
|
||||||
}),
|
},
|
||||||
system_theme,
|
system_theme,
|
||||||
cpu_usage: None,
|
cpu_usage: None,
|
||||||
native_pixels_per_point: Some(native_pixels_per_point()),
|
native_pixels_per_point: Some(native_pixels_per_point()),
|
||||||
window_info: None,
|
|
||||||
};
|
};
|
||||||
let storage = LocalStorage::default();
|
let storage = LocalStorage::default();
|
||||||
|
|
||||||
|
@ -293,16 +292,7 @@ impl AppRunner {
|
||||||
|
|
||||||
{
|
{
|
||||||
let app_output = self.frame.take_app_output();
|
let app_output = self.frame.take_app_output();
|
||||||
let epi::backend::AppOutput {
|
let epi::backend::AppOutput {} = app_output;
|
||||||
quit: _, // Can't quit a web page
|
|
||||||
window_size: _, // Can't resize a web page
|
|
||||||
window_title: _, // TODO(emilk): change title of window
|
|
||||||
decorated: _, // Can't toggle decorations
|
|
||||||
fullscreen: _, // TODO(emilk): fullscreen web window
|
|
||||||
drag_window: _, // Can't be dragged
|
|
||||||
window_pos: _, // Can't set position of a web page
|
|
||||||
visible: _, // Can't hide a web page
|
|
||||||
} = app_output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
|
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
|
||||||
|
|
|
@ -181,9 +181,7 @@ pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<
|
||||||
"hashchange",
|
"hashchange",
|
||||||
|_: web_sys::Event, mut runner_lock| {
|
|_: web_sys::Event, mut runner_lock| {
|
||||||
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
|
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
|
||||||
if let Some(web_info) = &mut runner_lock.frame.info.web_info {
|
runner_lock.frame.info.web_info.location.hash = location_hash();
|
||||||
web_info.location.hash = location_hash();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,8 @@ impl BackendPanel {
|
||||||
ui.ctx().options().screen_reader = screen_reader;
|
ui.ctx().options().screen_reader = screen_reader;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !frame.is_web() {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Quit").clicked() {
|
if ui.button("Quit").clicked() {
|
||||||
frame.quit();
|
frame.quit();
|
||||||
|
@ -159,11 +160,10 @@ impl BackendPanel {
|
||||||
ui.label(".");
|
ui.label(".");
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(web_info) = &frame.info().web_info {
|
#[cfg(target_arch = "wasm32")]
|
||||||
ui.collapsing("Web info (location)", |ui| {
|
ui.collapsing("Web info (location)", |ui| {
|
||||||
ui.monospace(format!("{:#?}", web_info.location));
|
ui.monospace(format!("{:#?}", frame.info().web_info.location));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// For instance: `eframe` web sets `pixels_per_point` every frame to force
|
// For instance: `eframe` web sets `pixels_per_point` every frame to force
|
||||||
// egui to use the same scale as the web zoom factor.
|
// egui to use the same scale as the web zoom factor.
|
||||||
|
@ -174,10 +174,11 @@ impl BackendPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !frame.is_web() {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if let Some(window_info) = &frame.info().window_info {
|
{
|
||||||
let mut fullscreen = window_info.fullscreen;
|
let mut fullscreen = frame.info().window_info.fullscreen;
|
||||||
ui.checkbox(&mut fullscreen, "🗖 Fullscreen")
|
ui.checkbox(&mut fullscreen, "🗖 Fullscreen")
|
||||||
.on_hover_text("Fullscreen the window");
|
.on_hover_text("Fullscreen the window");
|
||||||
frame.set_fullscreen(fullscreen);
|
frame.set_fullscreen(fullscreen);
|
||||||
|
|
|
@ -168,10 +168,9 @@ impl eframe::App for WrapApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
if let Some(web_info) = frame.info().web_info.as_ref() {
|
#[cfg(target_arch = "wasm32")]
|
||||||
if let Some(anchor) = web_info.location.hash.strip_prefix('#') {
|
if let Some(anchor) = frame.info().web_info.location.hash.strip_prefix('#') {
|
||||||
self.state.selected_anchor = anchor.to_owned();
|
self.state.selected_anchor = anchor.to_owned();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.state.selected_anchor.is_empty() {
|
if self.state.selected_anchor.is_empty() {
|
||||||
|
|
Loading…
Reference in a new issue