Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: add profile picture hover animation #29

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ serde_json = "1.0.89"
env_logger = "0.10.0"
puffin_egui = { version = "0.27.0", optional = true }
puffin = { version = "0.19.0", optional = true }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "b6c5d8fb9f3f79f14d69dd2d6eca2712e0f0a42d" }
nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "1489a5aee49996d8a6a54b4c3c9c8397e3e8d40f" }
#nostrdb = "0.3.2"
hex = "0.4.3"
base32 = "0.4.0"
Expand Down
2 changes: 1 addition & 1 deletion preview
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo run --bin ui_preview -- "$@"
cargo run --bin ui_preview --release -- "$@"
19 changes: 19 additions & 0 deletions src/ui/anim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pub fn hover_expand(
ui: &mut egui::Ui,
id: egui::Id,
size: f32,
expand_size: f32,
anim_speed: f32,
) -> (egui::Rect, f32) {
// Allocate space for the profile picture with a fixed size
let default_size = size + expand_size;
let (rect, response) =
ui.allocate_exact_size(egui::vec2(default_size, default_size), egui::Sense::hover());

let val = ui
.ctx()
.animate_bool_with_time(id, response.hovered(), anim_speed);

let size = size + val * expand_size;
(rect, size)
}
1 change: 1 addition & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod anim;
pub mod note;
pub mod preview;
pub mod profile;
Expand Down
51 changes: 28 additions & 23 deletions src/ui/note/contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,29 @@ fn render_note_preview(
.response
}

fn mention_ui(app: &mut Damus, txn: &Transaction, pk: &[u8; 32], ui: &mut egui::Ui) {
#[cfg(feature = "profiling")]
puffin::profile_function!();

let profile = app.ndb.get_profile_by_pubkey(txn, pk).ok();

let name: String =
if let Some(name) = profile.as_ref().and_then(crate::profile::get_profile_name) {
format!("@{}", name.username())
} else {
"??".to_string()
};

let resp = ui.colored_label(colors::PURPLE, &name);

if let Some(rec) = profile.as_ref() {
resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
ui.add(ui::ProfilePreview::new(rec, &mut app.img_cache));
});
}
}

fn render_note_contents(
ui: &mut egui::Ui,
damus: &mut Damus,
Expand Down Expand Up @@ -125,30 +148,12 @@ fn render_note_contents(
for block in blocks.iter(note) {
match block.blocktype() {
BlockType::MentionBech32 => match block.as_mention().unwrap() {
Mention::Profile(profile) => {
mention_ui(damus, txn, profile.pubkey(), ui);
}

Mention::Pubkey(npub) => {
#[cfg(feature = "profiling")]
puffin::profile_scope!("pubkey contents");

ui.horizontal(|ui| {
let profile = damus.ndb.get_profile_by_pubkey(txn, npub.pubkey()).ok();

let name: String = if let Some(name) =
profile.as_ref().and_then(crate::profile::get_profile_name)
{
format!("@{}", name.username())
} else {
"??".to_string()
};

let resp = ui.colored_label(colors::PURPLE, &name);

if let Some(rec) = profile.as_ref() {
resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
ui.add(ui::ProfilePreview::new(rec, &mut damus.img_cache));
});
}
});
mention_ui(damus, txn, npub.pubkey(), ui);
}

Mention::Note(note) if options.has_note_previews() => {
Expand Down
43 changes: 42 additions & 1 deletion src/ui/note/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub use options::NoteOptions;

use crate::{colors, ui, Damus};
use egui::{Label, RichText, Sense};
use std::hash::{Hash, Hasher};

pub struct Note<'a> {
app: &'a mut Damus,
Expand All @@ -23,6 +24,20 @@ impl<'a> egui::Widget for Note<'a> {
}
}

#[derive(Eq, PartialEq, Debug, Clone, Copy)]
struct ProfileAnimId {
profile_key: u64,
note_key: u64,
}

impl Hash for ProfileAnimId {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u8(0x12);
self.profile_key.hash(state);
self.note_key.hash(state);
}
}

impl<'a> Note<'a> {
pub fn new(app: &'a mut Damus, note: &'a nostrdb::Note<'a>) -> Self {
let flags = NoteOptions::actionbar | NoteOptions::note_previews;
Expand Down Expand Up @@ -104,7 +119,33 @@ impl<'a> Note<'a> {
// these have different lifetimes and types,
// so the calls must be separate
Some(pic) => {
ui.add(ui::ProfilePic::new(&mut self.app.img_cache, pic));
let expand_size = 5.0;
let anim_speed = 0.05;
let profile_key = profile.as_ref().unwrap().record().note_key();
let note_key = note_key.as_u64();

let (rect, size) = ui::anim::hover_expand(
ui,
egui::Id::new(ProfileAnimId {
profile_key,
note_key,
}),
ui::ProfilePic::default_size(),
expand_size,
anim_speed,
);

ui.put(
rect,
ui::ProfilePic::new(&mut self.app.img_cache, pic).size(size),
)
.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
ui.add(ui::ProfilePreview::new(
profile.as_ref().unwrap(),
&mut self.app.img_cache,
));
});
}
None => {
ui.add(ui::ProfilePic::new(
Expand Down
101 changes: 99 additions & 2 deletions src/ui/profile/picture.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::imgcache::ImageCache;

use crate::ui::{Preview, View};
use egui::{vec2, Sense, TextureHandle};

pub struct ProfilePic<'cache, 'url> {
Expand All @@ -16,10 +16,14 @@ impl<'cache, 'url> egui::Widget for ProfilePic<'cache, 'url> {

impl<'cache, 'url> ProfilePic<'cache, 'url> {
pub fn new(cache: &'cache mut ImageCache, url: &'url str) -> Self {
let size = 32.0;
let size = Self::default_size();
ProfilePic { cache, url, size }
}

pub fn default_size() -> f32 {
32.0
}

pub fn no_pfp_url() -> &'static str {
"https://damus.io/img/no-profile.svg"
}
Expand Down Expand Up @@ -93,3 +97,96 @@ fn paint_circle(ui: &mut egui::Ui, size: f32) -> egui::Response {

response
}

mod preview {
use super::*;
use crate::ui;
use nostrdb::*;
use std::collections::HashSet;

pub struct ProfilePicPreview {
cache: ImageCache,
ndb: Ndb,
keys: Vec<ProfileKey>,
}

impl ProfilePicPreview {
fn new() -> Self {
let config = Config::new();
let ndb = Ndb::new(".", &config).expect("ndb");
let txn = Transaction::new(&ndb).unwrap();
let filters = vec![Filter::new().kinds(vec![0]).build()];
let cache = ImageCache::new("cache/img".into());
let mut pks = HashSet::new();
let mut keys = HashSet::new();

for query_result in ndb.query(&txn, filters, 2000).unwrap() {
pks.insert(query_result.note.pubkey());
}

for pk in pks {
let profile = if let Ok(profile) = ndb.get_profile_by_pubkey(&txn, pk) {
profile
} else {
continue;
};

if profile
.record()
.profile()
.and_then(|p| p.picture())
.is_none()
{
continue;
}

keys.insert(profile.key().expect("should not be owned"));
}

let keys = keys.into_iter().collect();
ProfilePicPreview { cache, ndb, keys }
}
}

impl View for ProfilePicPreview {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal_wrapped(|ui| {
let txn = Transaction::new(&self.ndb).unwrap();
for key in &self.keys {
let profile = self.ndb.get_profile_by_key(&txn, *key).unwrap();
let url = profile
.record()
.profile()
.expect("should have profile")
.picture()
.expect("should have picture");

let expand_size = 10.0;
let anim_speed = 0.05;

let (rect, size) = ui::anim::hover_expand(
ui,
egui::Id::new(profile.key().unwrap()),
ui::ProfilePic::default_size(),
expand_size,
anim_speed,
);

ui.put(rect, ui::ProfilePic::new(&mut self.cache, url).size(size))
.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
ui.add(ui::ProfilePreview::new(&profile, &mut self.cache));
});
}
});
}
}

impl<'cache, 'url> Preview for ProfilePic<'cache, 'url> {
type Prev = ProfilePicPreview;

fn preview() -> Self::Prev {
ProfilePicPreview::new()
}
}
}
2 changes: 1 addition & 1 deletion src/ui/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ mod preview {
impl View for RelayViewPreview {
fn ui(&mut self, ui: &mut egui::Ui) {
self.pool.try_recv();
RelayView::new(RelayPoolManager::new(&mut self.pool)).ui(ui)
RelayView::new(RelayPoolManager::new(&mut self.pool)).ui(ui);
}
}

Expand Down
11 changes: 9 additions & 2 deletions src/ui_preview/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use notedeck::account_login_view::AccountLoginView;
use notedeck::app_creation::{
generate_mobile_emulator_native_options, generate_native_options, setup_cc,
};
use notedeck::ui::{Preview, PreviewApp, ProfilePreview, RelayView};
use notedeck::ui::{Preview, PreviewApp, ProfilePic, ProfilePreview, RelayView};
use std::env;

struct PreviewRunner {
Expand Down Expand Up @@ -73,5 +73,12 @@ async fn main() {

let runner = PreviewRunner::new(is_mobile);

previews!(runner, name, RelayView, AccountLoginView, ProfilePreview,);
previews!(
runner,
name,
RelayView,
AccountLoginView,
ProfilePreview,
ProfilePic
);
}
Loading