android progress
This commit is contained in:
1445
Cargo.lock
generated
1445
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
90
Cargo.toml
90
Cargo.toml
@@ -7,39 +7,101 @@ rust-version = "1.60"
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["lib", "cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = "0.4"
|
||||||
egui = "0.19.0"
|
egui = "0.19.0"
|
||||||
eframe = { version = "0.19.0", features = ["persistence"] }
|
|
||||||
serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence
|
|
||||||
#nostr_rust = { git = "git://jb55.com/nostr_rust", rev = "ccf7e521fe3bb9ca8f86516aef2c1f71db0213ed" }
|
|
||||||
ehttp = "0.2.0"
|
|
||||||
image = { version = "0.24", features = ["jpeg", "png", "webp"] }
|
|
||||||
egui_extras = { version = "0.19.0", features = ["image", "svg"] }
|
egui_extras = { version = "0.19.0", features = ["image", "svg"] }
|
||||||
|
egui_wgpu_backend = "0.20.0"
|
||||||
|
egui-winit = "0.19.0"
|
||||||
|
egui_winit_platform = {git = "https://github.com/inferrna/egui_winit_platform.git"}
|
||||||
|
ehttp = "0.2.0"
|
||||||
|
epi = "0.17.0"
|
||||||
|
hex = "0.4.3"
|
||||||
|
image = { version = "0.24", features = ["jpeg", "png", "webp"] }
|
||||||
|
log = "0.4.17"
|
||||||
|
#nostr_rust = { git = "git://jb55.com/nostr_rust", rev = "ccf7e521fe3bb9ca8f86516aef2c1f71db0213ed" }
|
||||||
poll-promise = "0.2.0"
|
poll-promise = "0.2.0"
|
||||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
pollster = "0.2"
|
||||||
serde_derive = "1"
|
serde_derive = "1"
|
||||||
|
serde_json = { version = "1", default-features = false, features = ["std"] }
|
||||||
# native:
|
serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
sha2 = "0.10.6"
|
||||||
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
wgpu = "0.14.0"
|
||||||
|
#winit = "0.27.1"
|
||||||
|
|
||||||
|
[target.'cfg(debug_assertions)'.dependencies]
|
||||||
|
simple_logger = "*"
|
||||||
|
android_logger = "0.11.1"
|
||||||
|
|
||||||
|
# This dependency will only be included when targeting Android
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
|
winit = { git="https://github.com/rust-windowing/winit.git" }
|
||||||
|
|
||||||
|
# This dependency will only be included when targeting Android
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
winit = { git="https://github.com/rust-windowing/winit.git", default-features = false, features = ["android-native-activity"] }
|
||||||
|
|
||||||
# web:
|
# web:
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
console_error_panic_hook = "0.1.6"
|
console_error_panic_hook = "0.1.6"
|
||||||
tracing-wasm = "0.2"
|
tracing-wasm = "0.2"
|
||||||
|
|
||||||
|
# This dependency will only be included when targeting Android
|
||||||
|
#[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
#winit = { git="https://github.com/rust-windowing/winit.git", default-features = false, features = ["android-native-activity"] }
|
||||||
|
|
||||||
[profile.release]
|
[package.metadata.android]
|
||||||
opt-level = 2 # fast and small wasm
|
package = "com.damus"
|
||||||
|
apk_name = "damus"
|
||||||
|
#assets = "assets"
|
||||||
|
|
||||||
|
[[package.metadata.android.uses_feature]]
|
||||||
|
name = "android.hardware.vulkan.level"
|
||||||
|
required = true
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[package.metadata.android.uses_permission]]
|
||||||
|
name = "android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
max_sdk_version = 18
|
||||||
|
|
||||||
|
# See https://developer.android.com/guide/topics/manifest/application-element
|
||||||
|
[package.metadata.android.application]
|
||||||
|
label = "Damus"
|
||||||
|
|
||||||
|
# See https://developer.android.com/guide/topics/manifest/application-element#debug
|
||||||
|
#
|
||||||
|
# Defaults to false.
|
||||||
|
debuggable = false
|
||||||
|
|
||||||
|
# See https://developer.android.com/guide/topics/manifest/application-element#theme
|
||||||
|
#
|
||||||
|
# Example shows setting the theme of an application to fullscreen.
|
||||||
|
#theme = "@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"
|
||||||
|
|
||||||
|
# Virtual path your application's icon for any mipmap level.
|
||||||
|
# If not specified, an icon will not be included in the APK.
|
||||||
|
#icon = "@mipmap/ic_launcher"
|
||||||
|
|
||||||
|
# See https://developer.android.com/guide/topics/manifest/application-element#label
|
||||||
|
#
|
||||||
|
# Defaults to the compiled artifact's name.
|
||||||
|
|
||||||
|
|
||||||
[patch.crates-io]
|
#[profile.release]
|
||||||
|
#opt-level = 2 # fast and small wasm
|
||||||
|
|
||||||
|
|
||||||
|
#[patch.crates-io]
|
||||||
# If you want to use the bleeding edge version of egui and eframe:
|
# If you want to use the bleeding edge version of egui and eframe:
|
||||||
# egui = { git = "https://github.com/emilk/egui", branch = "master" }
|
# egui = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||||
# eframe = { git = "https://github.com/emilk/egui", branch = "master" }
|
# eframe = { git = "https://github.com/emilk/egui", branch = "master" }
|
||||||
|
|
||||||
# If you fork https://github.com/emilk/egui you can test with:
|
# If you fork https://github.com/emilk/egui you can test with:
|
||||||
# egui = { path = "../egui/crates/egui" }
|
#egui = { path = "../egui/crates/egui" }
|
||||||
# eframe = { path = "../egui/crates/eframe" }
|
#eframe = { path = "../egui/crates/eframe" }
|
||||||
|
#egui_extras = { path = "../egui/crates/egui_extras", features = ["image", "svg"] }
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[build]
|
[build]
|
||||||
filehash = true
|
filehash = true
|
||||||
public_url = "/webv3/"
|
#public_url = "/webv3/"
|
||||||
|
|||||||
212
src/app.rs
212
src/app.rs
@@ -1,39 +1,53 @@
|
|||||||
//use egui::{Align, Layout, RichText, WidgetText};
|
//use egui::TextureFilter;
|
||||||
use egui_extras::RetainedImage;
|
use egui_extras::RetainedImage;
|
||||||
|
|
||||||
//use nostr_rust::events::Event;
|
//use nostr_rust::events::Event;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
//use std::borrow::{Borrow, Cow};
|
//use std::borrow::{Borrow, Cow};
|
||||||
|
use egui::Context;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::Event;
|
use crate::Event;
|
||||||
|
|
||||||
type ImageCache = HashMap<String, Promise<ehttp::Result<RetainedImage>>>;
|
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
|
||||||
|
enum UrlKey<'a> {
|
||||||
|
Orig(&'a str),
|
||||||
|
Failed(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageCache<'a> = HashMap<UrlKey<'a>, Promise<ehttp::Result<RetainedImage>>>;
|
||||||
|
|
||||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
#[serde(default)] // if we add new fields, give them default values when deserializing old state
|
#[serde(default)] // if we add new fields, give them default values when
|
||||||
pub struct Damus {
|
// deserializing old state
|
||||||
|
pub struct Damus<'a> {
|
||||||
// Example stuff:
|
// Example stuff:
|
||||||
label: String,
|
label: String,
|
||||||
|
|
||||||
|
composing: bool,
|
||||||
|
|
||||||
n_panels: u32,
|
n_panels: u32,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
events: Vec<Event>,
|
events: Vec<Event>,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
img_cache: ImageCache,
|
img_cache: ImageCache<'a>,
|
||||||
|
|
||||||
// this how you opt-out of serialization of a member
|
// this how you opt-out of serialization of a member
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
value: f32,
|
value: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Damus {
|
impl Default for Damus<'_> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
// Example stuff:
|
// Example stuff:
|
||||||
label: "Hello World!".to_owned(),
|
label: "Hello World!".to_owned(),
|
||||||
|
composing: false,
|
||||||
events: vec![],
|
events: vec![],
|
||||||
img_cache: HashMap::new(),
|
img_cache: HashMap::new(),
|
||||||
value: 2.7,
|
value: 2.7,
|
||||||
@@ -42,17 +56,34 @@ impl Default for Damus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Damus {
|
pub fn is_mobile(ctx: &egui::Context) -> bool {
|
||||||
|
let screen_size = ctx.input().screen_rect().size();
|
||||||
|
screen_size.x < 550.0
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Damus<'_> {
|
||||||
|
pub fn ui(&mut self, ctx: &Context) {
|
||||||
|
if is_mobile(ctx) {
|
||||||
|
render_damus_mobile(ctx, self)
|
||||||
|
} else {
|
||||||
|
render_damus_desktop(ctx, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_test_events(&mut self) {
|
||||||
|
add_test_events(self);
|
||||||
|
}
|
||||||
|
|
||||||
/// Called once before the first frame.
|
/// Called once before the first frame.
|
||||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
pub fn new() -> Self {
|
||||||
// This is also where you can customized the look at feel of egui using
|
// This is also where you can customized the look at feel of egui using
|
||||||
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
|
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
|
||||||
|
|
||||||
// Load previous app state (if any).
|
// Load previous app state (if any).
|
||||||
// Note that you must enable the `persistence` feature for this to work.
|
// Note that you must enable the `persistence` feature for this to work.
|
||||||
if let Some(storage) = cc.storage {
|
//if let Some(storage) = cc.storage {
|
||||||
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
//return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
||||||
}
|
//}
|
||||||
|
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
@@ -80,30 +111,60 @@ fn fetch_img(ctx: &egui::Context, url: &str) -> Promise<ehttp::Result<RetainedIm
|
|||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
ehttp::fetch(request, move |response| {
|
ehttp::fetch(request, move |response| {
|
||||||
let image = response.and_then(parse_response);
|
let image = response.and_then(parse_response);
|
||||||
sender.send(image); // send the results back to the UI thread. ctx.request_repaint();
|
sender.send(image); // send the results back to the UI thread.
|
||||||
|
ctx.request_repaint();
|
||||||
});
|
});
|
||||||
promise
|
promise
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_pfp(ui: &mut egui::Ui, img_cache: &mut ImageCache, url: String) {
|
fn robohash(hash: &str) -> String {
|
||||||
let m_cached_promise = img_cache.get_mut(&url);
|
return format!("https://robohash.org/{}", hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_pfp<'a>(ui: &mut egui::Ui, img_cache: &mut ImageCache<'a>, pk: &str, url: &'a str) {
|
||||||
|
let urlkey = UrlKey::Orig(url);
|
||||||
|
let m_cached_promise = img_cache.get(&urlkey);
|
||||||
if m_cached_promise.is_none() {
|
if m_cached_promise.is_none() {
|
||||||
img_cache.insert(url.clone(), fetch_img(ui.ctx(), &url));
|
debug!("urlkey: {:?}", &urlkey);
|
||||||
|
img_cache.insert(UrlKey::Orig(url), fetch_img(ui.ctx(), &url));
|
||||||
}
|
}
|
||||||
|
|
||||||
match img_cache[&url].ready() {
|
let pfp_size = 50.0;
|
||||||
|
|
||||||
|
match img_cache[&urlkey].ready() {
|
||||||
None => {
|
None => {
|
||||||
ui.spinner(); // still loading
|
ui.spinner(); // still loading
|
||||||
}
|
}
|
||||||
Some(Err(err)) => {
|
Some(Err(_err)) => {
|
||||||
ui.colored_label(ui.visuals().error_fg_color, err); // something went wrong
|
let failed_key = UrlKey::Failed(&url);
|
||||||
|
let m_failed_promise = img_cache.get_mut(&failed_key);
|
||||||
|
if m_failed_promise.is_none() {
|
||||||
|
debug!("failed key: {:?}", &failed_key);
|
||||||
|
img_cache.insert(UrlKey::Failed(url), fetch_img(ui.ctx(), &robohash(pk)));
|
||||||
|
}
|
||||||
|
|
||||||
|
match img_cache[&failed_key].ready() {
|
||||||
|
None => {
|
||||||
|
ui.spinner(); // still loading
|
||||||
|
}
|
||||||
|
Some(Err(_err)) => {
|
||||||
|
ui.label("❌");
|
||||||
|
}
|
||||||
|
Some(Ok(img)) => {
|
||||||
|
pfp_image(ui, img, pfp_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(Ok(image)) => {
|
Some(Ok(img)) => {
|
||||||
image.show_max_size(ui, egui::vec2(64.0, 64.0));
|
pfp_image(ui, img, pfp_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pfp_image(ui: &mut egui::Ui, img: &RetainedImage, size: f32) -> egui::Response {
|
||||||
|
img.show_max_size(ui, egui::vec2(size, size))
|
||||||
|
}
|
||||||
|
|
||||||
fn render_username(ui: &mut egui::Ui, pk: &str) {
|
fn render_username(ui: &mut egui::Ui, pk: &str) {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
@@ -113,7 +174,7 @@ fn render_username(ui: &mut egui::Ui, pk: &str) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_event(ui: &mut egui::Ui, img_cache: &mut ImageCache, ev: &Event) {
|
fn render_event(ui: &mut egui::Ui, img_cache: &mut ImageCache<'_>, ev: &Event) {
|
||||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
|
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
|
||||||
let damus_pic = "https://damus.io/img/damus.svg".into();
|
let damus_pic = "https://damus.io/img/damus.svg".into();
|
||||||
let jb55_pic = "https://damus.io/img/red-me.jpg".into();
|
let jb55_pic = "https://damus.io/img/red-me.jpg".into();
|
||||||
@@ -124,7 +185,7 @@ fn render_event(ui: &mut egui::Ui, img_cache: &mut ImageCache, ev: &Event) {
|
|||||||
damus_pic
|
damus_pic
|
||||||
};
|
};
|
||||||
|
|
||||||
render_pfp(ui, img_cache, pic);
|
render_pfp(ui, img_cache, &ev.pub_key, pic);
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
||||||
render_username(ui, &ev.pub_key);
|
render_username(ui, &ev.pub_key);
|
||||||
@@ -134,7 +195,7 @@ fn render_event(ui: &mut egui::Ui, img_cache: &mut ImageCache, ev: &Event) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timeline_view(ui: &mut egui::Ui, app: &mut Damus) {
|
fn timeline_view(ui: &mut egui::Ui, app: &mut Damus<'_>) {
|
||||||
ui.heading("Timeline");
|
ui.heading("Timeline");
|
||||||
|
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
@@ -147,7 +208,7 @@ fn timeline_view(ui: &mut egui::Ui, app: &mut Damus) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_damus(ctx: &egui::Context, _frame: &mut eframe::Frame, app: &mut Damus) {
|
fn render_panel(ctx: &egui::Context, app: &mut Damus<'_>) {
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.visuals_mut().button_frame = false;
|
ui.visuals_mut().button_frame = false;
|
||||||
@@ -172,6 +233,17 @@ fn render_damus(ctx: &egui::Context, _frame: &mut eframe::Frame, app: &mut Damus
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus<'_>) {
|
||||||
|
let panel_width = ctx.input().screen_rect.width();
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
timeline_panel(ui, app, panel_width, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus<'_>) {
|
||||||
|
render_panel(ctx, app);
|
||||||
|
|
||||||
let screen_size = ctx.input().screen_rect.width();
|
let screen_size = ctx.input().screen_rect.width();
|
||||||
let calc_panel_width = (screen_size / app.n_panels as f32) - 30.0;
|
let calc_panel_width = (screen_size / app.n_panels as f32) - 30.0;
|
||||||
@@ -194,7 +266,7 @@ fn render_damus(ctx: &egui::Context, _frame: &mut eframe::Frame, app: &mut Damus
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timeline_panel(ui: &mut egui::Ui, app: &mut Damus, panel_width: f32, ind: u32) {
|
fn timeline_panel(ui: &mut egui::Ui, app: &mut Damus<'_>, panel_width: f32, ind: u32) {
|
||||||
egui::SidePanel::left(format!("l{}", ind))
|
egui::SidePanel::left(format!("l{}", ind))
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.max_width(panel_width)
|
.max_width(panel_width)
|
||||||
@@ -204,56 +276,58 @@ fn timeline_panel(ui: &mut egui::Ui, app: &mut Damus, panel_width: f32, ind: u32
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for Damus {
|
fn add_test_events(damus: &mut Damus<'_>) {
|
||||||
/// Called by the frame work to save state before shutdown.
|
// Examples of how to create different panels and windows.
|
||||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
// Pick whichever suits you.
|
||||||
eframe::set_value(storage, eframe::APP_KEY, self);
|
// Tip: a good default choice is to just keep the `CentralPanel`.
|
||||||
}
|
// For inspiration and more examples, go to https://emilk.github.io/egui
|
||||||
|
|
||||||
/// Called each time the UI needs repainting, which may be many times per second.
|
let test_event = Event {
|
||||||
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
id: "6938e3cd841f3111dbdbd909f87fd52c3d1f1e4a07fd121d1243196e532811cb".to_string(),
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
pub_key: "f0a6ff7f70b872de6d82c8daec692a433fd23b6a49f25923c6f034df715cdeec".to_string(),
|
||||||
// Examples of how to create different panels and windows.
|
created_at: 1667781968,
|
||||||
// Pick whichever suits you.
|
kind: 1,
|
||||||
// Tip: a good default choice is to just keep the `CentralPanel`.
|
tags: vec![],
|
||||||
// For inspiration and more examples, go to https://emilk.github.io/egui
|
content: LOREM_IPSUM.into(),
|
||||||
|
sig: "af02c971015995f79e07fa98aaf98adeeb6a56d0005e451ee4e78844cff712a6bc0f2109f72a878975f162dcefde4173b65ebd4c3d3ab3b520a9dcac6acf092d".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
let test_event = Event {
|
let test_event2 = Event {
|
||||||
id: "6938e3cd841f3111dbdbd909f87fd52c3d1f1e4a07fd121d1243196e532811cb".to_string(),
|
id: "6938e3cd841f3111dbdbd909f87fd52c3d1f1e4a07fd121d1243196e532811cb".to_string(),
|
||||||
pub_key: "f0a6ff7f70b872de6d82c8daec692a433fd23b6a49f25923c6f034df715cdeec".to_string(),
|
pub_key: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245".to_string(),
|
||||||
created_at: 1667781968,
|
created_at: 1667781968,
|
||||||
kind: 1,
|
kind: 1,
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
content: LOREM_IPSUM.into(),
|
content: LOREM_IPSUM_LONG.into(),
|
||||||
sig: "af02c971015995f79e07fa98aaf98adeeb6a56d0005e451ee4e78844cff712a6bc0f2109f72a878975f162dcefde4173b65ebd4c3d3ab3b520a9dcac6acf092d".to_string(),
|
sig: "af02c971015995f79e07fa98aaf98adeeb6a56d0005e451ee4e78844cff712a6bc0f2109f72a878975f162dcefde4173b65ebd4c3d3ab3b520a9dcac6acf092d".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let test_event2 = Event {
|
if damus.events.len() == 0 {
|
||||||
id: "6938e3cd841f3111dbdbd909f87fd52c3d1f1e4a07fd121d1243196e532811cb".to_string(),
|
damus.events.push(test_event.clone());
|
||||||
pub_key: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245".to_string(),
|
damus.events.push(test_event2.clone());
|
||||||
created_at: 1667781968,
|
damus.events.push(test_event.clone());
|
||||||
kind: 1,
|
damus.events.push(test_event2.clone());
|
||||||
tags: vec![],
|
damus.events.push(test_event.clone());
|
||||||
content: LOREM_IPSUM_LONG.into(),
|
damus.events.push(test_event2.clone());
|
||||||
sig: "af02c971015995f79e07fa98aaf98adeeb6a56d0005e451ee4e78844cff712a6bc0f2109f72a878975f162dcefde4173b65ebd4c3d3ab3b520a9dcac6acf092d".to_string(),
|
damus.events.push(test_event.clone());
|
||||||
};
|
damus.events.push(test_event2.clone());
|
||||||
|
damus.events.push(test_event.clone());
|
||||||
if self.events.len() == 0 {
|
|
||||||
self.events.push(test_event.clone());
|
|
||||||
self.events.push(test_event2.clone());
|
|
||||||
self.events.push(test_event.clone());
|
|
||||||
self.events.push(test_event2.clone());
|
|
||||||
self.events.push(test_event.clone());
|
|
||||||
self.events.push(test_event2.clone());
|
|
||||||
self.events.push(test_event.clone());
|
|
||||||
self.events.push(test_event2.clone());
|
|
||||||
self.events.push(test_event.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
render_damus(ctx, _frame, self);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//impl eframe::App for Damus<'_> {
|
||||||
|
// /// Called by the frame work to save state before shutdown.
|
||||||
|
// fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
|
// eframe::set_value(storage, eframe::APP_KEY, self);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /// Called each time the UI needs repainting, which may be many times per second.
|
||||||
|
// /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||||
|
// fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
// update_damus(ctx)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
|
pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
|
||||||
|
|
||||||
pub const LOREM_IPSUM_LONG: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
pub const LOREM_IPSUM_LONG: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
|||||||
383
src/main.rs
383
src/main.rs
@@ -1,34 +1,361 @@
|
|||||||
#![warn(clippy::all, rust_2018_idioms)]
|
use ::egui::FontDefinitions;
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
use chrono::Timelike;
|
||||||
|
use damus::Damus;
|
||||||
|
use egui_wgpu_backend::{RenderPass, ScreenDescriptor};
|
||||||
|
use egui_winit_platform::{Platform, PlatformDescriptor};
|
||||||
|
use log::{error, warn};
|
||||||
|
use std::iter;
|
||||||
|
use std::time::Instant;
|
||||||
|
use wgpu::CompositeAlphaMode;
|
||||||
|
use winit::event::Event::*;
|
||||||
|
use winit::event_loop::ControlFlow;
|
||||||
|
use winit::event_loop::EventLoop;
|
||||||
|
|
||||||
// When compiling natively:
|
#[cfg(target_os = "android")]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
use winit::{
|
||||||
fn main() {
|
event::StartCause, platform::android::EventLoopBuilderExtAndroid,
|
||||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
platform::run_return::EventLoopExtRunReturn,
|
||||||
tracing_subscriber::fmt::init();
|
};
|
||||||
|
|
||||||
let native_options = eframe::NativeOptions::default();
|
/// A custom event type for the winit app.
|
||||||
eframe::run_native(
|
#[derive(Debug, Clone, Copy)]
|
||||||
"Damus Desktop",
|
pub enum Event {
|
||||||
native_options,
|
RequestRedraw,
|
||||||
Box::new(|cc| Box::new(damus::Damus::new(cc))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// when compiling to web using trunk.
|
/// This is the repaint signal type that egui needs for requesting a repaint from another thread.
|
||||||
#[cfg(target_arch = "wasm32")]
|
/// It sends the custom RequestRedraw event to the winit event loop.
|
||||||
fn main() {
|
struct ExampleRepaintSignal(std::sync::Mutex<winit::event_loop::EventLoopProxy<Event>>);
|
||||||
// Make sure panics are logged using `console.error`.
|
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
|
|
||||||
// Redirect tracing to console.log and friends:
|
impl epi::backend::RepaintSignal for ExampleRepaintSignal {
|
||||||
tracing_wasm::set_as_global_default();
|
fn request_repaint(&self) {
|
||||||
|
self.0
|
||||||
let web_options = eframe::WebOptions::default();
|
.lock()
|
||||||
eframe::start_web(
|
.unwrap_or_else(|e| {
|
||||||
"the_canvas_id", // hardcode it
|
panic!(
|
||||||
web_options,
|
"Failed to lock guard at {} line {} with error\n{}",
|
||||||
Box::new(|cc| Box::new(damus::Damus::new(cc))),
|
file!(),
|
||||||
)
|
line!(),
|
||||||
.expect("failed to start eframe");
|
e
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.send_event(Event::RequestRedraw)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[no_mangle]
|
||||||
|
fn android_main(app: winit::platform::android::activity::AndroidApp) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
std::env::set_var("RUST_BACKTRACE", "full");
|
||||||
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default().with_min_level(log::Level::Trace),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let event_loop = winit::event_loop::EventLoopBuilder::<Event>::with_user_event()
|
||||||
|
.with_android_app(app)
|
||||||
|
.build();
|
||||||
|
run_evloop(event_loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
simple_logger::init().unwrap();
|
||||||
|
let event_loop = winit::event_loop::EventLoopBuilder::<Event>::with_user_event().build();
|
||||||
|
run_evloop(event_loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_evloop(mut event_loop: EventLoop<Event>) {
|
||||||
|
//'Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events.', ..../winit-c2fdb27092aba5a7/418cc44/src/platform_impl/android/mod.rs:1028:13
|
||||||
|
warn!("Winit build window at {} line {}", file!(), line!());
|
||||||
|
let window = winit::window::WindowBuilder::new()
|
||||||
|
.with_decorations(!cfg!(android)) /* !cfg!(android) */
|
||||||
|
.with_resizable(!cfg!(android))
|
||||||
|
.with_transparent(false)
|
||||||
|
.with_title("egui-wgpu_winit example")
|
||||||
|
.build(&event_loop)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to init window at {} line {} with error\n{:?}",
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
warn!("WGPU new instance at {} line {}", file!(), line!());
|
||||||
|
let mut instance = wgpu::Instance::new(wgpu::Backends::PRIMARY);
|
||||||
|
|
||||||
|
let mut size = window.inner_size();
|
||||||
|
let outer_size = window.outer_size();
|
||||||
|
|
||||||
|
warn!("outer_size = {:?}", outer_size);
|
||||||
|
warn!("size = {:?}", size);
|
||||||
|
|
||||||
|
warn!("Create platform at {} line {}", file!(), line!());
|
||||||
|
// We use the egui_winit_platform crate as the platform.
|
||||||
|
let mut platform = Platform::new(PlatformDescriptor {
|
||||||
|
physical_width: size.width as u32,
|
||||||
|
physical_height: size.height as u32,
|
||||||
|
scale_factor: window.scale_factor(),
|
||||||
|
font_definitions: FontDefinitions::default(),
|
||||||
|
style: Default::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
let mut platform = {
|
||||||
|
//Just find the actual screen size on android
|
||||||
|
event_loop.run_return(|main_event, tgt, control_flow| {
|
||||||
|
control_flow.set_poll();
|
||||||
|
warn!(
|
||||||
|
"Got event: {:?} at {} line {}",
|
||||||
|
&main_event,
|
||||||
|
file!(),
|
||||||
|
line!()
|
||||||
|
);
|
||||||
|
match main_event {
|
||||||
|
NewEvents(e) => match e {
|
||||||
|
StartCause::ResumeTimeReached { .. } => {}
|
||||||
|
StartCause::WaitCancelled { .. } => {}
|
||||||
|
StartCause::Poll => {}
|
||||||
|
StartCause::Init => {}
|
||||||
|
},
|
||||||
|
WindowEvent {
|
||||||
|
window_id,
|
||||||
|
ref event,
|
||||||
|
} => {
|
||||||
|
if let winit::event::WindowEvent::Resized(r) = event {
|
||||||
|
size = *r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceEvent { .. } => {}
|
||||||
|
UserEvent(_) => {}
|
||||||
|
Suspended => {
|
||||||
|
control_flow.set_poll();
|
||||||
|
}
|
||||||
|
Resumed => {
|
||||||
|
if let Some(primary_mon) = tgt.primary_monitor() {
|
||||||
|
size = primary_mon.size();
|
||||||
|
window.set_inner_size(size);
|
||||||
|
warn!(
|
||||||
|
"Set to new size: {:?} at {} line {}",
|
||||||
|
&size,
|
||||||
|
file!(),
|
||||||
|
line!()
|
||||||
|
);
|
||||||
|
} else if let Some(other_mon) = tgt.available_monitors().next() {
|
||||||
|
size = other_mon.size();
|
||||||
|
window.set_inner_size(size);
|
||||||
|
warn!(
|
||||||
|
"Set to new size: {:?} at {} line {}",
|
||||||
|
&size,
|
||||||
|
file!(),
|
||||||
|
line!()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
control_flow.set_exit();
|
||||||
|
}
|
||||||
|
MainEventsCleared => {}
|
||||||
|
RedrawRequested(rdr) => {}
|
||||||
|
RedrawEventsCleared => {}
|
||||||
|
LoopDestroyed => {}
|
||||||
|
};
|
||||||
|
platform.handle_event(&main_event);
|
||||||
|
});
|
||||||
|
|
||||||
|
warn!("Recreate platform at {} line {}", file!(), line!());
|
||||||
|
// We use the egui_winit_platform crate as the platform.
|
||||||
|
Platform::new(PlatformDescriptor {
|
||||||
|
physical_width: size.width as u32,
|
||||||
|
physical_height: size.height as u32,
|
||||||
|
scale_factor: window.scale_factor(),
|
||||||
|
font_definitions: FontDefinitions::default(),
|
||||||
|
style: Default::default(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
warn!("WGPU new surface at {} line {}", file!(), line!());
|
||||||
|
let mut surface = unsafe { instance.create_surface(&window) };
|
||||||
|
|
||||||
|
warn!("instance request_adapter at {} line {}", file!(), line!());
|
||||||
|
// WGPU 0.11+ support force fallback (if HW implementation not supported), set it to true or false (optional).
|
||||||
|
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
}))
|
||||||
|
.unwrap_or_else(|| panic!("Failed get adapter at {} line {}", file!(), line!()));
|
||||||
|
|
||||||
|
warn!("adapter request_device at {} line {}", file!(), line!());
|
||||||
|
let (device, queue) = pollster::block_on(adapter.request_device(
|
||||||
|
&wgpu::DeviceDescriptor {
|
||||||
|
features: wgpu::Features::default(),
|
||||||
|
limits: wgpu::Limits::default(),
|
||||||
|
label: None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to request device at {} line {} with error\n{:?}",
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let surface_format = surface.get_supported_formats(&adapter)[0];
|
||||||
|
let mut surface_config = wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format: surface_format,
|
||||||
|
width: size.width as u32,
|
||||||
|
height: size.height as u32,
|
||||||
|
present_mode: wgpu::PresentMode::AutoNoVsync,
|
||||||
|
alpha_mode: CompositeAlphaMode::Auto,
|
||||||
|
};
|
||||||
|
|
||||||
|
warn!("surface configure at {} line {}", file!(), line!());
|
||||||
|
surface.configure(&device, &surface_config);
|
||||||
|
|
||||||
|
warn!("RenderPass new at {} line {}", file!(), line!());
|
||||||
|
// We use the egui_wgpu_backend crate as the render backend.
|
||||||
|
let mut egui_rpass = RenderPass::new(&device, surface_format, 1);
|
||||||
|
|
||||||
|
warn!("DemoWindows default at {} line {}", file!(), line!());
|
||||||
|
// Display the demo application that ships with egui.
|
||||||
|
let mut app = Damus::new();
|
||||||
|
app.add_test_events();
|
||||||
|
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
|
let mut in_bad_state = false;
|
||||||
|
|
||||||
|
warn!("Enter the loop");
|
||||||
|
event_loop.run(move |event, _, control_flow| {
|
||||||
|
// Pass the winit events to the platform integration.
|
||||||
|
warn!("Got event: {:?} at {} line {}", &event, file!(), line!());
|
||||||
|
platform.handle_event(&event);
|
||||||
|
match event {
|
||||||
|
RedrawRequested(..) => {
|
||||||
|
platform.update_time(start_time.elapsed().as_secs_f64());
|
||||||
|
|
||||||
|
let output_frame = match surface.get_current_texture() {
|
||||||
|
Ok(frame) => frame,
|
||||||
|
Err(wgpu::SurfaceError::Outdated) => {
|
||||||
|
// This error occurs when the app is minimized on Windows.
|
||||||
|
// Silently return here to prevent spamming the console with:
|
||||||
|
error!("The underlying surface has changed, and therefore the swap chain must be updated");
|
||||||
|
in_bad_state = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
|
// This error occurs when the app is minimized on Windows.
|
||||||
|
// Silently return here to prevent spamming the console with:
|
||||||
|
error!("LOST surface, drop frame. Originally: \"The swap chain has been lost and needs to be recreated\"");
|
||||||
|
in_bad_state = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Dropped frame with error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let output_view = output_frame
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
// Begin to draw the UI frame.
|
||||||
|
platform.begin_frame();
|
||||||
|
|
||||||
|
// Draw the demo application.
|
||||||
|
app.ui(&platform.context());
|
||||||
|
|
||||||
|
// End the UI frame. We could now handle the output and draw the UI with the backend.
|
||||||
|
let full_output = platform.end_frame(Some(&window));
|
||||||
|
let paint_jobs = platform.context().tessellate(full_output.shapes);
|
||||||
|
|
||||||
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("encoder"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload all resources for the GPU.
|
||||||
|
let screen_descriptor = ScreenDescriptor {
|
||||||
|
physical_width: surface_config.width,
|
||||||
|
physical_height: surface_config.height,
|
||||||
|
scale_factor: window.scale_factor() as f32
|
||||||
|
};
|
||||||
|
let tdelta: egui::TexturesDelta = full_output.textures_delta;
|
||||||
|
egui_rpass
|
||||||
|
.add_textures(&device, &queue, &tdelta)
|
||||||
|
.expect("add texture ok");
|
||||||
|
egui_rpass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor);
|
||||||
|
|
||||||
|
// Record all render passes.
|
||||||
|
egui_rpass
|
||||||
|
.execute(
|
||||||
|
&mut encoder,
|
||||||
|
&output_view,
|
||||||
|
&paint_jobs,
|
||||||
|
&screen_descriptor,
|
||||||
|
Some(wgpu::Color::BLACK),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|e| panic!("Failed to render pass at {} line {} with error\n{:?}", file!(), line!(), e));
|
||||||
|
// Submit the commands.
|
||||||
|
queue.submit(iter::once(encoder.finish()));
|
||||||
|
|
||||||
|
// Redraw egui
|
||||||
|
output_frame.present();
|
||||||
|
|
||||||
|
egui_rpass
|
||||||
|
.remove_textures(tdelta)
|
||||||
|
.expect("remove texture ok");
|
||||||
|
|
||||||
|
// Support reactive on windows only, but not on linux.
|
||||||
|
// if _output.needs_repaint {
|
||||||
|
// *control_flow = ControlFlow::Poll;
|
||||||
|
// } else {
|
||||||
|
// *control_flow = ControlFlow::Wait;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
MainEventsCleared | UserEvent(Event::RequestRedraw) => {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
WindowEvent { event, .. } => match event {
|
||||||
|
winit::event::WindowEvent::Resized(size) => {
|
||||||
|
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||||
|
// See: https://github.com/rust-windowing/winit/issues/208
|
||||||
|
// This solves an issue where the app would panic when minimizing on Windows.
|
||||||
|
if size.width > 0 && size.height > 0 {
|
||||||
|
surface_config.width = size.width;
|
||||||
|
surface_config.height = size.height;
|
||||||
|
surface.configure(&device, &surface_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
winit::event::WindowEvent::CloseRequested => {
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Resumed => {
|
||||||
|
if in_bad_state {
|
||||||
|
//https://github.com/gfx-rs/wgpu/issues/2302
|
||||||
|
warn!("WGPU new surface at {} line {}", file!(), line!());
|
||||||
|
surface = unsafe { instance.create_surface(&window) };
|
||||||
|
warn!("surface configure at {} line {}", file!(), line!());
|
||||||
|
surface.configure(&device, &surface_config);
|
||||||
|
in_bad_state = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Suspended => (),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Time of day as seconds since midnight. Used for clock in demo app.
|
||||||
|
pub fn seconds_since_midnight() -> f64 {
|
||||||
|
let time = chrono::Local::now().time();
|
||||||
|
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user