use egui_virtual_list for rendering

absolutely insane performance increase

Fixes: https://github.com/damus-io/notedeck/issues/32
Suggested-by: @lucasmerlin
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2024-04-28 11:03:47 -07:00
parent d0cfeee79f
commit 26128c3456
6 changed files with 142 additions and 102 deletions

31
Cargo.lock generated
View File

@@ -1055,7 +1055,7 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"web-time", "web-time 0.2.4",
"wgpu", "wgpu",
"winapi", "winapi",
"winit", "winit",
@@ -1090,7 +1090,7 @@ dependencies = [
"puffin", "puffin",
"thiserror", "thiserror",
"type-map", "type-map",
"web-time", "web-time 0.2.4",
"wgpu", "wgpu",
"winit", "winit",
] ]
@@ -1107,7 +1107,7 @@ dependencies = [
"puffin", "puffin",
"raw-window-handle 0.6.0", "raw-window-handle 0.6.0",
"smithay-clipboard", "smithay-clipboard",
"web-time", "web-time 0.2.4",
"webbrowser", "webbrowser",
"winit", "winit",
] ]
@@ -1145,6 +1145,16 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "egui_virtual_list"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142d3a0ad2ae4743e323ad1cb384bffd45abc36dac6f9833f0980c2b4d76af1a"
dependencies = [
"egui",
"web-time 1.1.0",
]
[[package]] [[package]]
name = "ehttp" name = "ehttp"
version = "0.2.0" version = "0.2.0"
@@ -2580,6 +2590,7 @@ dependencies = [
"eframe", "eframe",
"egui", "egui",
"egui_extras", "egui_extras",
"egui_virtual_list",
"ehttp 0.2.0", "ehttp 0.2.0",
"enostr", "enostr",
"env_logger 0.10.2", "env_logger 0.10.2",
@@ -3059,7 +3070,7 @@ dependencies = [
"puffin", "puffin",
"time", "time",
"vec1", "vec1",
"web-time", "web-time 0.2.4",
] ]
[[package]] [[package]]
@@ -4679,6 +4690,16 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "webbrowser" name = "webbrowser"
version = "0.8.12" version = "0.8.12"
@@ -5156,7 +5177,7 @@ dependencies = [
"wayland-protocols", "wayland-protocols",
"wayland-protocols-plasma", "wayland-protocols-plasma",
"web-sys", "web-sys",
"web-time", "web-time 0.2.4",
"windows-sys 0.48.0", "windows-sys 0.48.0",
"x11-dl", "x11-dl",
"x11rb 0.13.0", "x11rb 0.13.0",

View File

@@ -39,6 +39,7 @@ nostr-sdk = "0.29.0"
strum = "0.26" strum = "0.26"
strum_macros = "0.26" strum_macros = "0.26"
bitflags = "2.5.0" bitflags = "2.5.0"
egui_virtual_list = "0.3.0"
[features] [features]

View File

@@ -1,4 +1,4 @@
[{"limit": 100, [{"limit": 1000,
"kinds": [ "kinds": [
1 1
], ],

View File

@@ -1 +1 @@
[{"limit": 100, "kinds":[1], "#p": ["32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"]}] [{"limit": 1000, "kinds":[1], "#p": ["32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"]}]

View File

@@ -5,18 +5,16 @@ use crate::frame_history::FrameHistory;
use crate::imgcache::ImageCache; use crate::imgcache::ImageCache;
use crate::notecache::NoteCache; use crate::notecache::NoteCache;
use crate::timeline; use crate::timeline;
use crate::ui; use crate::timeline::{NoteRef, Timeline};
use crate::ui::is_mobile; use crate::ui::is_mobile;
use crate::Result; use crate::Result;
use egui::containers::scroll_area::ScrollBarVisibility;
use egui::{Context, Frame, Margin, Style}; use egui::{Context, Frame, Margin, Style};
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use enostr::{ClientMessage, Filter, Pubkey, RelayEvent, RelayMessage}; use enostr::{ClientMessage, Filter, Pubkey, RelayEvent, RelayMessage};
use nostrdb::{BlockType, Config, Mention, Ndb, Note, NoteKey, Subscription, Transaction}; use nostrdb::{BlockType, Config, Mention, Ndb, Note, NoteKey, Transaction};
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::hash::Hash; use std::hash::Hash;
use std::path::Path; use std::path::Path;
@@ -31,47 +29,6 @@ pub enum DamusState {
Initialized, Initialized,
} }
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct NoteRef {
pub key: NoteKey,
pub created_at: u64,
}
impl Ord for NoteRef {
fn cmp(&self, other: &Self) -> Ordering {
match self.created_at.cmp(&other.created_at) {
Ordering::Equal => self.key.cmp(&other.key),
Ordering::Less => Ordering::Greater,
Ordering::Greater => Ordering::Less,
}
}
}
impl PartialOrd for NoteRef {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
struct Timeline {
pub filter: Vec<Filter>,
pub notes: Vec<NoteRef>,
pub subscription: Option<Subscription>,
}
impl Timeline {
pub fn new(filter: Vec<Filter>) -> Self {
let notes: Vec<NoteRef> = Vec::with_capacity(1000);
let subscription: Option<Subscription> = None;
Timeline {
filter,
notes,
subscription,
}
}
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown. /// We derive Deserialize/Serialize so we can persist app state on shutdown.
pub struct Damus { pub struct Damus {
state: DamusState, state: DamusState,
@@ -80,7 +37,7 @@ pub struct Damus {
pool: RelayPool, pool: RelayPool,
pub textmode: bool, pub textmode: bool,
timelines: Vec<Timeline>, pub timelines: Vec<Timeline>,
pub img_cache: ImageCache, pub img_cache: ImageCache,
pub ndb: Ndb, pub ndb: Ndb,
@@ -557,52 +514,6 @@ fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
} }
*/ */
fn render_notes(ui: &mut egui::Ui, damus: &mut Damus, timeline: usize) -> Result<()> {
#[cfg(feature = "profiling")]
puffin::profile_function!();
let num_notes = damus.timelines[timeline].notes.len();
let txn = Transaction::new(&damus.ndb)?;
for i in 0..num_notes {
let note_key = damus.timelines[timeline].notes[i].key;
let note = if let Ok(note) = damus.ndb.get_note_by_key(&txn, note_key) {
note
} else {
warn!("failed to query note {:?}", note_key);
continue;
};
let note_ui = ui::Note::new(damus, &note);
ui.add(note_ui);
ui.add(egui::Separator::default().spacing(0.0));
}
Ok(())
}
fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) {
//padding(4.0, ui, |ui| ui.heading("Notifications"));
/*
let font_id = egui::TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
*/
egui::ScrollArea::vertical()
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
//.auto_shrink([false; 2])
/*
.show_viewport(ui, |ui, viewport| {
render_notes_in_viewport(ui, app, viewport, row_height, font_id);
});
*/
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 0.0;
ui.spacing_mut().item_spacing.x = 4.0;
let _ = render_notes(ui, app, timeline);
});
}
fn top_panel(ctx: &egui::Context) -> egui::TopBottomPanel { fn top_panel(ctx: &egui::Context) -> egui::TopBottomPanel {
let top_margin = egui::Margin { let top_margin = egui::Margin {
top: 4.0, top: 4.0,
@@ -684,7 +595,7 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) {
puffin::profile_function!(); puffin::profile_function!();
main_panel(&ctx.style()).show(ctx, |ui| { main_panel(&ctx.style()).show(ctx, |ui| {
timeline_view(ui, app, 0); timeline::timeline_view(ui, app, 0);
}); });
} }
@@ -713,7 +624,7 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
if app.timelines.len() == 1 { if app.timelines.len() == 1 {
main_panel(&ctx.style()).show(ctx, |ui| { main_panel(&ctx.style()).show(ctx, |ui| {
timeline_view(ui, app, 0); timeline::timeline_view(ui, app, 0);
}); });
return; return;
@@ -737,7 +648,7 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, timelines: us
.clip(true) .clip(true)
.horizontal(|mut strip| { .horizontal(|mut strip| {
for timeline_ind in 0..timelines { for timeline_ind in 0..timelines {
strip.cell(|ui| timeline_view(ui, app, timeline_ind)); strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind));
} }
}); });
} }

View File

@@ -1,3 +1,110 @@
use crate::{ui, Damus};
use egui::containers::scroll_area::ScrollBarVisibility;
use egui_virtual_list::VirtualList;
use enostr::Filter;
use nostrdb::{NoteKey, Subscription, Transaction};
use std::cmp::Ordering;
use std::sync::{Arc, Mutex};
use log::warn;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct NoteRef {
pub key: NoteKey,
pub created_at: u64,
}
impl Ord for NoteRef {
fn cmp(&self, other: &Self) -> Ordering {
match self.created_at.cmp(&other.created_at) {
Ordering::Equal => self.key.cmp(&other.key),
Ordering::Less => Ordering::Greater,
Ordering::Greater => Ordering::Less,
}
}
}
impl PartialOrd for NoteRef {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub struct Timeline {
pub filter: Vec<Filter>,
pub notes: Vec<NoteRef>,
/// Our nostrdb subscription
pub subscription: Option<Subscription>,
/// State for our virtual list egui widget
pub list: Arc<Mutex<VirtualList>>,
}
impl Timeline {
pub fn new(filter: Vec<Filter>) -> Self {
let notes: Vec<NoteRef> = Vec::with_capacity(1000);
let subscription: Option<Subscription> = None;
let list = Arc::new(Mutex::new(VirtualList::new()));
Timeline {
filter,
notes,
subscription,
list,
}
}
}
pub fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) {
//padding(4.0, ui, |ui| ui.heading("Notifications"));
/*
let font_id = egui::TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
*/
egui::ScrollArea::vertical()
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
//.auto_shrink([false; 2])
/*
.show_viewport(ui, |ui, viewport| {
render_notes_in_viewport(ui, app, viewport, row_height, font_id);
});
*/
.show(ui, |ui| {
let len = app.timelines[timeline].notes.len();
let list = app.timelines[timeline].list.clone();
list.lock()
.unwrap()
.ui_custom_layout(ui, len, |ui, start_index| {
ui.spacing_mut().item_spacing.y = 0.0;
ui.spacing_mut().item_spacing.x = 4.0;
let note_key = app.timelines[timeline].notes[start_index].key;
let txn = if let Ok(txn) = Transaction::new(&app.ndb) {
txn
} else {
warn!("failed to create transaction for {:?}", note_key);
return 0;
};
let note = if let Ok(note) = app.ndb.get_note_by_key(&txn, note_key) {
note
} else {
warn!("failed to query note {:?}", note_key);
return 0;
};
let note_ui = ui::Note::new(app, &note);
ui.add(note_ui);
ui.add(egui::Separator::default().spacing(0.0));
1
});
});
}
pub fn merge_sorted_vecs<T: Ord + Copy>(vec1: &[T], vec2: &[T]) -> Vec<T> { pub fn merge_sorted_vecs<T: Ord + Copy>(vec1: &[T], vec2: &[T]) -> Vec<T> {
let mut merged = Vec::with_capacity(vec1.len() + vec2.len()); let mut merged = Vec::with_capacity(vec1.len() + vec2.len());
let mut i = 0; let mut i = 0;