diff --git a/Cargo.lock b/Cargo.lock index b71df80d..c55267e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3148,9 +3148,11 @@ dependencies = [ "egui", "egui-winit", "egui_extras", + "nostrdb", "notedeck", "notedeck_columns", "notedeck_dave", + "notedeck_ui", "profiling", "puffin", "puffin_egui", @@ -3188,6 +3190,7 @@ dependencies = [ "indexmap", "nostrdb", "notedeck", + "notedeck_ui", "open", "poll-promise", "pretty_assertions", @@ -3236,6 +3239,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "notedeck_ui" +version = "0.3.1" +dependencies = [ + "egui", + "egui_extras", + "ehttp", + "image", + "nostrdb", + "notedeck", + "poll-promise", + "profiling", + "tokio", + "tracing", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index abbdb04d..dc20d69f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,9 @@ members = [ "crates/notedeck_chrome", "crates/notedeck_columns", "crates/notedeck_dave", + "crates/notedeck_ui", - "crates/enostr", "crates/tokenator", "crates/notedeck_dave", + "crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", ] [workspace.dependencies] @@ -42,6 +43,7 @@ notedeck = { path = "crates/notedeck" } notedeck_chrome = { path = "crates/notedeck_chrome" } notedeck_columns = { path = "crates/notedeck_columns" } notedeck_dave = { path = "crates/notedeck_dave" } +notedeck_ui = { path = "crates/notedeck_ui" } tokenator = { path = "crates/tokenator" } open = "5.3.0" poll-promise = { version = "0.3.0", features = ["tokio"] } diff --git a/crates/notedeck_chrome/Cargo.toml b/crates/notedeck_chrome/Cargo.toml index 5c9e9a24..d9e3bee6 100644 --- a/crates/notedeck_chrome/Cargo.toml +++ b/crates/notedeck_chrome/Cargo.toml @@ -13,8 +13,10 @@ eframe = { workspace = true } egui_extras = { workspace = true } egui = { workspace = true } notedeck_columns = { workspace = true } +notedeck_ui = { workspace = true } notedeck_dave = { workspace = true } notedeck = { workspace = true } +nostrdb = { workspace = true } puffin = { workspace = true, optional = true } puffin_egui = { workspace = true, optional = true } serde_json = { workspace = true } diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs index 1f47391f..2391d608 100644 --- a/crates/notedeck_chrome/src/chrome.rs +++ b/crates/notedeck_chrome/src/chrome.rs @@ -1,19 +1,31 @@ - // Entry point for wasm //#[cfg(target_arch = "wasm32")] //use wasm_bindgen::prelude::*; +use egui::{Button, Label, Layout, RichText, ThemePreference, Widget}; +use egui_extras::{Size, StripBuilder}; +use nostrdb::{ProfileRecord, Transaction}; +use notedeck::{AppContext, NotedeckTextStyle, UserAccount}; +use notedeck_ui::{profile::get_profile_url, AnimationHelper, ProfilePic}; +static ICON_WIDTH: f32 = 40.0; +pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; + +#[derive(Default)] pub struct Chrome { active: i32, apps: Vec>, } +pub enum ChromePanelAction { + Support, + Settings, + Account, + SaveTheme(ThemePreference), +} + impl Chrome { pub fn new() -> Self { - Chrome { - active: 0, - apps: vec![], - } + Chrome::default() } pub fn add_app(&mut self, app: impl notedeck::App + 'static) { @@ -23,15 +35,264 @@ impl Chrome { pub fn set_active(&mut self, app: i32) { self.active = app; } + + /// Show the side menu or bar, depending on if we're on a narrow + /// or wide screen. + /// + /// The side menu should hover over the screen, while the side bar + /// is collapsible but persistent on the screen. + fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) { + ui.spacing_mut().item_spacing.x = 0.0; + + let side_panel_width: f32 = 68.0; + StripBuilder::new(ui) + .size(Size::exact(side_panel_width)) // collapsible sidebar + .size(Size::remainder()) // the main app contents + .clip(true) + .horizontal(|mut strip| { + strip.cell(|ui| { + let rect = ui.available_rect_before_wrap(); + if !ui.visuals().dark_mode { + let rect = ui.available_rect_before_wrap(); + ui.painter().rect( + rect, + 0, + notedeck_ui::colors::ALMOST_WHITE, + egui::Stroke::new(0.0, egui::Color32::TRANSPARENT), + egui::StrokeKind::Inside, + ); + } + + ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { + self.topdown_sidebar(ui); + }); + + ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| { + self.bottomup_sidebar(ctx, ui); + }); + + // vertical sidebar line + ui.painter().vline( + rect.right(), + rect.y_range(), + ui.visuals().widgets.noninteractive.bg_stroke, + ); + }); + + strip.cell(|ui| { + /* + let rect = ui.available_rect_before_wrap(); + ui.painter().rect( + rect, + 0, + egui::Color32::RED, + egui::Stroke::new(1.0, egui::Color32::BLUE), + egui::StrokeKind::Inside, + ); + */ + + self.apps[self.active as usize].update(ctx, ui); + }); + }); + } + + /// The section of the chrome sidebar that starts at the + /// bottom and goes up + fn bottomup_sidebar( + &mut self, + ctx: &mut AppContext, + ui: &mut egui::Ui, + ) -> Option { + let dark_mode = ui.ctx().style().visuals.dark_mode; + let pfp_resp = self.pfp_button(ctx, ui); + let settings_resp = ui.add(settings_button(dark_mode)); + + let theme_action = match ui.ctx().theme() { + egui::Theme::Dark => { + let resp = ui + .add(Button::new("☀").frame(false)) + .on_hover_text("Switch to light mode"); + if resp.clicked() { + Some(ChromePanelAction::SaveTheme(ThemePreference::Light)) + } else { + None + } + } + egui::Theme::Light => { + let resp = ui + .add(Button::new("🌙").frame(false)) + .on_hover_text("Switch to dark mode"); + if resp.clicked() { + Some(ChromePanelAction::SaveTheme(ThemePreference::Light)) + } else { + None + } + } + }; + + if ui.add(support_button()).clicked() { + return Some(ChromePanelAction::Support); + } + + if theme_action.is_some() { + return theme_action; + } + + if pfp_resp.clicked() { + Some(ChromePanelAction::Account) + } else if settings_resp.clicked() || settings_resp.hovered() { + Some(ChromePanelAction::Settings) + } else { + None + } + } + + fn pfp_button(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response { + let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget + let helper = AnimationHelper::new(ui, "pfp-button", egui::vec2(max_size, max_size)); + + let min_pfp_size = ICON_WIDTH; + let cur_pfp_size = helper.scale_1d_pos(min_pfp_size); + + let txn = Transaction::new(ctx.ndb).expect("should be able to create txn"); + let profile_url = get_account_url(&txn, ctx.ndb, ctx.accounts.get_selected_account()); + + let widget = ProfilePic::new(ctx.img_cache, profile_url).size(cur_pfp_size); + + ui.put(helper.get_animation_rect(), widget); + + helper.take_animation_response() + } + + fn topdown_sidebar(&mut self, ui: &mut egui::Ui) { + // macos needs a bit of space to make room for window + // minimize/close buttons + if cfg!(target_os = "macos") { + ui.add_space(28.0); + } + + if ui.add(expand_side_panel_button()).clicked() { + self.active = (self.active + 1) % (self.apps.len() as i32); + } + + ui.add_space(4.0); + ui.add(milestone_name()); + ui.add_space(16.0); + //let dark_mode = ui.ctx().style().visuals.dark_mode; + //ui.add(add_column_button(dark_mode)) + } } impl notedeck::App for Chrome { fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) { - let active = self.active; - self.apps[active as usize].update(ctx, ui); - //for i in 0..self.apps.len() { - // self.apps[i].update(ctx, ui); - //} + self.show(ctx, ui); + // TODO: unify this constant with the columns side panel width. ui crate? } } +fn milestone_name() -> impl Widget { + |ui: &mut egui::Ui| -> egui::Response { + ui.vertical_centered(|ui| { + let font = egui::FontId::new( + notedeck::fonts::get_font_size( + ui.ctx(), + &NotedeckTextStyle::Tiny, + ), + egui::FontFamily::Name(notedeck::fonts::NamedFontFamily::Bold.as_str().into()), + ); + ui.add(Label::new( + RichText::new("ALPHA") + .color( ui.style().visuals.noninteractive().fg_stroke.color) + .font(font), + ).selectable(false)).on_hover_text("Notedeck is an alpha product. Expect bugs and contact us when you run into issues.").on_hover_cursor(egui::CursorIcon::Help) + }) + .inner + } +} + +fn expand_side_panel_button() -> impl Widget { + |ui: &mut egui::Ui| -> egui::Response { + let img_size = 40.0; + let img_data = egui::include_image!("../../../assets/damus_rounded_80.png"); + let img = egui::Image::new(img_data) + .max_width(img_size) + .sense(egui::Sense::click()); + + ui.add(img) + } +} + +fn support_button() -> impl Widget { + |ui: &mut egui::Ui| -> egui::Response { + let img_size = 16.0; + + let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget + let img_data = if ui.visuals().dark_mode { + egui::include_image!("../../../assets/icons/help_icon_dark_4x.png") + } else { + egui::include_image!("../../../assets/icons/help_icon_inverted_4x.png") + }; + let img = egui::Image::new(img_data).max_width(img_size); + + let helper = AnimationHelper::new(ui, "help-button", egui::vec2(max_size, max_size)); + + let cur_img_size = helper.scale_1d_pos(img_size); + img.paint_at( + ui, + helper + .get_animation_rect() + .shrink((max_size - cur_img_size) / 2.0), + ); + + helper.take_animation_response() + } +} + +fn settings_button(dark_mode: bool) -> impl Widget { + move |ui: &mut egui::Ui| { + let img_size = 24.0; + let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget + let img_data = if dark_mode { + egui::include_image!("../../../assets/icons/settings_dark_4x.png") + } else { + egui::include_image!("../../../assets/icons/settings_light_4x.png") + }; + let img = egui::Image::new(img_data).max_width(img_size); + + let helper = AnimationHelper::new(ui, "settings-button", egui::vec2(max_size, max_size)); + + let cur_img_size = helper.scale_1d_pos(img_size); + img.paint_at( + ui, + helper + .get_animation_rect() + .shrink((max_size - cur_img_size) / 2.0), + ); + + helper.take_animation_response() + } +} + +pub fn get_profile_url_owned(profile: Option>) -> &str { + if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { + url + } else { + ProfilePic::no_pfp_url() + } +} + +pub fn get_account_url<'a>( + txn: &'a nostrdb::Transaction, + ndb: &nostrdb::Ndb, + account: Option<&UserAccount>, +) -> &'a str { + if let Some(selected_account) = account { + if let Ok(profile) = ndb.get_profile_by_pubkey(txn, selected_account.key.pubkey.bytes()) { + get_profile_url_owned(Some(profile)) + } else { + get_profile_url_owned(None) + } + } else { + get_profile_url(None) + } +} diff --git a/crates/notedeck_chrome/src/preview.rs b/crates/notedeck_chrome/src/preview.rs index 2fdba419..4bd8a84f 100644 --- a/crates/notedeck_chrome/src/preview.rs +++ b/crates/notedeck_chrome/src/preview.rs @@ -4,7 +4,7 @@ use notedeck_columns::ui::configure_deck::ConfigureDeckView; use notedeck_columns::ui::edit_deck::EditDeckView; use notedeck_columns::ui::profile::EditProfileView; use notedeck_columns::ui::{ - account_login_view::AccountLoginView, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, + account_login_view::AccountLoginView, PostView, Preview, PreviewApp, PreviewConfig, ProfilePreview, RelayView, }; use std::env; @@ -99,7 +99,6 @@ async fn main() { RelayView, AccountLoginView, ProfilePreview, - ProfilePic, PostView, ConfigureDeckView, EditDeckView, diff --git a/crates/notedeck_columns/Cargo.toml b/crates/notedeck_columns/Cargo.toml index 67d3905f..3c65e5f0 100644 --- a/crates/notedeck_columns/Cargo.toml +++ b/crates/notedeck_columns/Cargo.toml @@ -30,6 +30,7 @@ hex = { workspace = true } image = { workspace = true } indexmap = { workspace = true } nostrdb = { workspace = true } +notedeck_ui = { workspace = true } open = { workspace = true } poll-promise = { workspace = true } puffin = { workspace = true, optional = true } diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index a6031175..912785a3 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -554,26 +554,33 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut App let mut side_panel_action: Option = None; strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - let side_panel = DesktopSidePanel::new( - ctx.ndb, - ctx.img_cache, - ctx.accounts.get_selected_account(), - &app.decks_cache, - ) - .show(ui); + let side_panel = + DesktopSidePanel::new(ctx.accounts.get_selected_account(), &app.decks_cache) + .show(ui); - if side_panel.response.clicked() || side_panel.response.secondary_clicked() { - if let Some(action) = DesktopSidePanel::perform_action( - &mut app.decks_cache, - ctx.accounts, - &mut app.support, - ctx.theme, - side_panel.action, - ) { - side_panel_action = Some(action); + if let Some(side_panel) = side_panel { + if side_panel.response.clicked() || side_panel.response.secondary_clicked() { + if let Some(action) = DesktopSidePanel::perform_action( + &mut app.decks_cache, + ctx.accounts, + side_panel.action, + ) { + side_panel_action = Some(action); + } } } + // debug + /* + ui.painter().rect( + rect, + 0, + egui::Color32::RED, + egui::Stroke::new(1.0, egui::Color32::BLUE), + egui::StrokeKind::Inside, + ); + */ + // vertical sidebar line ui.painter().vline( rect.right(), diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs index e5388bf7..12956e0c 100644 --- a/crates/notedeck_columns/src/lib.rs +++ b/crates/notedeck_columns/src/lib.rs @@ -9,14 +9,11 @@ mod actionbar; pub mod app_creation; mod app_style; mod args; -mod colors; mod column; mod deck_state; mod decks; mod draft; mod frame_history; -mod gif; -mod images; mod key_parsing; pub mod login_manager; mod media_upload; diff --git a/crates/notedeck_columns/src/media_upload.rs b/crates/notedeck_columns/src/media_upload.rs index 21acd091..beb4f488 100644 --- a/crates/notedeck_columns/src/media_upload.rs +++ b/crates/notedeck_columns/src/media_upload.rs @@ -8,7 +8,8 @@ use poll_promise::Promise; use sha2::{Digest, Sha256}; use url::Url; -use crate::{images::fetch_binary_from_disk, Error}; +use crate::Error; +use notedeck_ui::images::fetch_binary_from_disk; pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap(); const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json"; diff --git a/crates/notedeck_columns/src/post.rs b/crates/notedeck_columns/src/post.rs index 4f9b9005..b8f1d38b 100644 --- a/crates/notedeck_columns/src/post.rs +++ b/crates/notedeck_columns/src/post.rs @@ -369,7 +369,7 @@ impl PostBuffer { pub fn to_layout_job(&self, ui: &egui::Ui) -> LayoutJob { let mut job = LayoutJob::default(); - let colored_fmt = default_text_format_colored(ui, crate::colors::PINK); + let colored_fmt = default_text_format_colored(ui, notedeck_ui::colors::PINK); let mut prev_text_char_index = 0; let mut prev_text_byte_index = 0; diff --git a/crates/notedeck_columns/src/ui/accounts.rs b/crates/notedeck_columns/src/ui/accounts.rs index 30d4ff06..4aeeed17 100644 --- a/crates/notedeck_columns/src/ui/accounts.rs +++ b/crates/notedeck_columns/src/ui/accounts.rs @@ -1,4 +1,4 @@ -use crate::colors::PINK; +use notedeck_ui::colors::PINK; use egui::{ Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2, }; diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs index 975ea732..d3f1c3b1 100644 --- a/crates/notedeck_columns/src/ui/add_column.rs +++ b/crates/notedeck_columns/src/ui/add_column.rs @@ -554,13 +554,34 @@ impl<'a> AddColumnView<'a> { } fn find_user_button() -> impl Widget { - styled_button("Find User", crate::colors::PINK) + styled_button("Find User", notedeck_ui::colors::PINK) } fn add_column_button() -> impl Widget { - styled_button("Add", crate::colors::PINK) + styled_button("Add", notedeck_ui::colors::PINK) } +/* +pub(crate) fn sized_button(text: &str) -> impl Widget + '_ { + move |ui: &mut egui::Ui| -> egui::Response { + let painter = ui.painter(); + let galley = painter.layout( + text.to_owned(), + NotedeckTextStyle::Body.get_font_id(ui.ctx()), + Color32::WHITE, + ui.available_width(), + ); + + ui.add_sized( + galley.rect.expand2(vec2(16.0, 8.0)).size(), + egui::Button::new(galley) + .corner_radius(8.0) + .fill(notedeck_ui::colors::PINK), + ) + } +} +*/ + struct ColumnOptionData { title: &'static str, description: &'static str, diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs index 29dd8156..2606ab9e 100644 --- a/crates/notedeck_columns/src/ui/column/header.rs +++ b/crates/notedeck_columns/src/ui/column/header.rs @@ -1,4 +1,3 @@ -use crate::colors; use crate::column::ColumnsAction; use crate::nav::RenderNavAction; use crate::nav::SwitchingAction; @@ -302,7 +301,7 @@ impl<'a> NavTitle<'a> { let col_resp = if col == self.col_id { ui.dnd_drag_source(item_id, col, |ui| { item_frame - .stroke(egui::Stroke::new(2.0, colors::PINK)) + .stroke(egui::Stroke::new(2.0, notedeck_ui::colors::PINK)) .fill(ui.visuals().widgets.noninteractive.bg_stroke.color) .show(ui, |ui| self.move_tooltip_col_presentation(ui, col)); }) diff --git a/crates/notedeck_columns/src/ui/configure_deck.rs b/crates/notedeck_columns/src/ui/configure_deck.rs index 73c41c8c..884bd7c5 100644 --- a/crates/notedeck_columns/src/ui/configure_deck.rs +++ b/crates/notedeck_columns/src/ui/configure_deck.rs @@ -1,6 +1,7 @@ -use crate::{app_style::deck_icon_font_sized, colors::PINK, deck_state::DeckState}; +use crate::{app_style::deck_icon_font_sized, deck_state::DeckState}; use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget}; use notedeck::{NamedFontFamily, NotedeckTextStyle}; +use notedeck_ui::colors::PINK; use super::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, diff --git a/crates/notedeck_columns/src/ui/images.rs b/crates/notedeck_columns/src/ui/images.rs index b0bb042c..e69de29b 100644 --- a/crates/notedeck_columns/src/ui/images.rs +++ b/crates/notedeck_columns/src/ui/images.rs @@ -1,75 +0,0 @@ -use notedeck::{GifStateMap, Images, MediaCache, MediaCacheType, TexturedImage}; - -use crate::images::ImageType; - -use super::ProfilePic; - -#[allow(clippy::too_many_arguments)] -pub fn render_images( - ui: &mut egui::Ui, - images: &mut Images, - url: &str, - img_type: ImageType, - cache_type: MediaCacheType, - show_waiting: impl FnOnce(&mut egui::Ui), - show_error: impl FnOnce(&mut egui::Ui, String), - show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap), -) -> egui::Response { - let cache = match cache_type { - MediaCacheType::Image => &mut images.static_imgs, - MediaCacheType::Gif => &mut images.gifs, - }; - - render_media_cache( - ui, - cache, - &mut images.gif_states, - url, - img_type, - cache_type, - show_waiting, - show_error, - show_success, - ) -} - -#[allow(clippy::too_many_arguments)] -fn render_media_cache( - ui: &mut egui::Ui, - cache: &mut MediaCache, - gif_states: &mut GifStateMap, - url: &str, - img_type: ImageType, - cache_type: MediaCacheType, - show_waiting: impl FnOnce(&mut egui::Ui), - show_error: impl FnOnce(&mut egui::Ui, String), - show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap), -) -> egui::Response { - let m_cached_promise = cache.map().get(url); - - if m_cached_promise.is_none() { - let res = crate::images::fetch_img(cache, ui.ctx(), url, img_type, cache_type.clone()); - cache.map_mut().insert(url.to_owned(), res); - } - - egui::Frame::NONE - .show(ui, |ui| { - match cache.map_mut().get_mut(url).and_then(|p| p.ready_mut()) { - None => show_waiting(ui), - Some(Err(err)) => { - let err = err.to_string(); - let no_pfp = crate::images::fetch_img( - cache, - ui.ctx(), - ProfilePic::no_pfp_url(), - ImageType::Profile(128), - cache_type, - ); - cache.map_mut().insert(url.to_owned(), no_pfp); - show_error(ui, err) - } - Some(Ok(renderable_media)) => show_success(ui, url, renderable_media, gif_states), - } - }) - .response -} diff --git a/crates/notedeck_columns/src/ui/mod.rs b/crates/notedeck_columns/src/ui/mod.rs index 1eef71c5..c0e5dd0d 100644 --- a/crates/notedeck_columns/src/ui/mod.rs +++ b/crates/notedeck_columns/src/ui/mod.rs @@ -24,8 +24,9 @@ pub mod widgets; pub use accounts::AccountsView; pub use mention::Mention; pub use note::{NoteResponse, NoteView, PostReplyView, PostView}; +pub use notedeck_ui::ProfilePic; pub use preview::{Preview, PreviewApp, PreviewConfig}; -pub use profile::{ProfilePic, ProfilePreview}; +pub use profile::ProfilePreview; pub use relay::RelayView; pub use side_panel::{DesktopSidePanel, SidePanelAction}; pub use thread::ThreadView; diff --git a/crates/notedeck_columns/src/ui/note/contents.rs b/crates/notedeck_columns/src/ui/note/contents.rs index ee818828..cb71ad44 100644 --- a/crates/notedeck_columns/src/ui/note/contents.rs +++ b/crates/notedeck_columns/src/ui/note/contents.rs @@ -1,13 +1,16 @@ -use crate::gif::{handle_repaint, retrieve_latest_texture}; -use crate::ui::images::render_images; use crate::ui::{ self, note::{NoteOptions, NoteResponse}, }; -use crate::{actionbar::NoteAction, images::ImageType, timeline::TimelineKind}; +use crate::{actionbar::NoteAction, timeline::TimelineKind}; use egui::{Button, Color32, Hyperlink, Image, Response, RichText, Sense, Window}; use enostr::KeypairUnowned; use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; +use notedeck_ui::images::ImageType; +use notedeck_ui::{ + gif::{handle_repaint, retrieve_latest_texture}, + images::render_images, +}; use tracing::warn; use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteCache, Zaps}; diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs index 55c6ca8e..f41a2c5b 100644 --- a/crates/notedeck_columns/src/ui/note/post.rs +++ b/crates/notedeck_columns/src/ui/note/post.rs @@ -1,9 +1,7 @@ use crate::draft::{Draft, Drafts, MentionHint}; -use crate::gif::{handle_repaint, retrieve_latest_texture}; use crate::media_upload::{nostrbuild_nip96_upload, MediaPath}; use crate::post::{downcast_post_buffer, MentionType, NewPost}; use crate::profile::get_display_name; -use crate::ui::images::render_images; use crate::ui::search_results::SearchResultsView; use crate::ui::{self, Preview, PreviewConfig}; use crate::Result; @@ -13,6 +11,10 @@ use egui::widgets::text_edit::TextEdit; use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer}; use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; +use notedeck_ui::{ + gif::{handle_repaint, retrieve_latest_texture}, + images::render_images, +}; use notedeck::supported_mime_hosted_at_url; use tracing::error; @@ -428,7 +430,7 @@ impl<'a, 'd> PostView<'a, 'd> { ui, self.note_context.img_cache, &media.url, - crate::images::ImageType::Content, + notedeck_ui::images::ImageType::Content, cache_type, |ui| { ui.spinner(); diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs index 633758f2..dd693dc5 100644 --- a/crates/notedeck_columns/src/ui/profile/edit.rs +++ b/crates/notedeck_columns/src/ui/profile/edit.rs @@ -3,9 +3,11 @@ use core::f32; use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit}; use notedeck::{Images, NotedeckTextStyle}; -use crate::{colors, profile_state::ProfileState}; +use crate::profile_state::ProfileState; -use super::{banner, unwrap_profile_url, ProfilePic}; +use super::banner; + +use notedeck_ui::{profile::unwrap_profile_url, ProfilePic}; pub struct EditProfileView<'a> { state: &'a mut ProfileState, @@ -34,7 +36,7 @@ impl<'a> EditProfileView<'a> { crate::ui::padding(padding, ui, |ui| { ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| { if ui - .add(button("Save changes", 119.0).fill(colors::PINK)) + .add(button("Save changes", 119.0).fill(notedeck_ui::colors::PINK)) .clicked() { save = true; diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index 2c2ff9fe..e510a7f9 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -1,5 +1,4 @@ pub mod edit; -pub mod picture; pub mod preview; pub use edit::EditProfileView; @@ -7,13 +6,11 @@ use egui::load::TexturePoll; use egui::{vec2, Color32, CornerRadius, Label, Layout, Rect, RichText, ScrollArea, Sense, Stroke}; use enostr::Pubkey; use nostrdb::{ProfileRecord, Transaction}; -pub use picture::ProfilePic; pub use preview::ProfilePreview; use tracing::error; use crate::{ actionbar::NoteAction, - colors, images, profile::get_display_name, timeline::{TimelineCache, TimelineKind}, ui::timeline::{tabs_ui, TimelineTabView}, @@ -21,6 +18,7 @@ use crate::{ }; use notedeck::{Accounts, MuteFun, NotedeckTextStyle, UnknownIds}; +use notedeck_ui::{images, profile::get_profile_url, ProfilePic}; use super::note::contents::NoteContext; use super::note::NoteOptions; @@ -215,7 +213,7 @@ fn handle_link(ui: &mut egui::Ui, website_url: &str) { "../../../../../assets/icons/links_4x.png" )); if ui - .label(RichText::new(website_url).color(colors::PINK)) + .label(RichText::new(website_url).color(notedeck_ui::colors::PINK)) .on_hover_cursor(egui::CursorIcon::PointingHand) .interact(Sense::click()) .clicked() @@ -231,7 +229,7 @@ fn handle_lud16(ui: &mut egui::Ui, lud16: &str) { "../../../../../assets/icons/zap_4x.png" )); - let _ = ui.label(RichText::new(lud16).color(colors::PINK)); + let _ = ui.label(RichText::new(lud16).color(notedeck_ui::colors::PINK)); } fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ { @@ -360,7 +358,7 @@ fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl Label::new( RichText::new(format!("@{}", username)) .size(16.0) - .color(colors::MID_GRAY), + .color(notedeck_ui::colors::MID_GRAY), ) .selectable(false), ) @@ -371,7 +369,9 @@ fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl "../../../../../assets/icons/verified_4x.png" )); ui.add(Label::new( - RichText::new(nip05).size(16.0).color(colors::TEAL), + RichText::new(nip05) + .size(16.0) + .color(notedeck_ui::colors::TEAL), )) }); @@ -396,18 +396,6 @@ fn display_name_widget(name: NostrName<'_>, add_placeholder_space: bool) -> impl } } -pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { - unwrap_profile_url(profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture()))) -} - -pub fn unwrap_profile_url(maybe_url: Option<&str>) -> &str { - if let Some(url) = maybe_url { - url - } else { - ProfilePic::no_pfp_url() - } -} - fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b where 'b: 'a, diff --git a/crates/notedeck_columns/src/ui/profile/picture.rs b/crates/notedeck_columns/src/ui/profile/picture.rs deleted file mode 100644 index b09e24c0..00000000 --- a/crates/notedeck_columns/src/ui/profile/picture.rs +++ /dev/null @@ -1,265 +0,0 @@ -use crate::gif::{handle_repaint, retrieve_latest_texture}; -use crate::images::ImageType; -use crate::ui::images::render_images; -use crate::ui::{Preview, PreviewConfig}; -use egui::{vec2, Sense, Stroke, TextureHandle}; -use nostrdb::{Ndb, Transaction}; -use tracing::info; - -use notedeck::{supported_mime_hosted_at_url, AppContext, Images}; - -pub struct ProfilePic<'cache, 'url> { - cache: &'cache mut Images, - url: &'url str, - size: f32, - border: Option, -} - -impl egui::Widget for ProfilePic<'_, '_> { - fn ui(self, ui: &mut egui::Ui) -> egui::Response { - render_pfp(ui, self.cache, self.url, self.size, self.border) - } -} - -impl<'cache, 'url> ProfilePic<'cache, 'url> { - pub fn new(cache: &'cache mut Images, url: &'url str) -> Self { - let size = Self::default_size() as f32; - ProfilePic { - cache, - url, - size, - border: None, - } - } - - pub fn border_stroke(ui: &egui::Ui) -> Stroke { - Stroke::new(4.0, ui.visuals().panel_fill) - } - - pub fn from_profile( - cache: &'cache mut Images, - profile: &nostrdb::ProfileRecord<'url>, - ) -> Option { - profile - .record() - .profile() - .and_then(|p| p.picture()) - .map(|url| ProfilePic::new(cache, url)) - } - - #[inline] - pub fn default_size() -> i8 { - 38 - } - - #[inline] - pub fn medium_size() -> i8 { - 32 - } - - #[inline] - pub fn small_size() -> i8 { - 24 - } - - #[inline] - pub fn no_pfp_url() -> &'static str { - "https://damus.io/img/no-profile.svg" - } - - #[inline] - pub fn size(mut self, size: f32) -> Self { - self.size = size; - self - } - - #[inline] - pub fn border(mut self, stroke: Stroke) -> Self { - self.border = Some(stroke); - self - } -} - -#[profiling::function] -fn render_pfp( - ui: &mut egui::Ui, - img_cache: &mut Images, - url: &str, - ui_size: f32, - border: Option, -) -> egui::Response { - // We will want to downsample these so it's not blurry on hi res displays - let img_size = 128u32; - - let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url) - .unwrap_or(notedeck::MediaCacheType::Image); - - render_images( - ui, - img_cache, - url, - ImageType::Profile(img_size), - cache_type, - |ui| { - paint_circle(ui, ui_size, border); - }, - |ui, _| { - paint_circle(ui, ui_size, border); - }, - |ui, url, renderable_media, gifs| { - let texture_handle = - handle_repaint(ui, retrieve_latest_texture(url, gifs, renderable_media)); - pfp_image(ui, texture_handle, ui_size, border); - }, - ) -} - -#[profiling::function] -fn pfp_image( - ui: &mut egui::Ui, - img: &TextureHandle, - size: f32, - border: Option, -) -> egui::Response { - let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); - if let Some(stroke) = border { - draw_bg_border(ui, rect.center(), size, stroke); - } - ui.put(rect, egui::Image::new(img).max_width(size)); - - response -} - -fn paint_circle(ui: &mut egui::Ui, size: f32, border: Option) -> egui::Response { - let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); - - if let Some(stroke) = border { - draw_bg_border(ui, rect.center(), size, stroke); - } - - ui.painter() - .circle_filled(rect.center(), size / 2.0, ui.visuals().weak_text_color()); - - response -} - -fn draw_bg_border(ui: &mut egui::Ui, center: egui::Pos2, size: f32, stroke: Stroke) { - let border_size = size + (stroke.width * 2.0); - ui.painter() - .circle_filled(center, border_size / 2.0, stroke.color); -} - -mod preview { - use super::*; - use crate::ui; - use nostrdb::*; - use std::collections::HashSet; - - pub struct ProfilePicPreview { - keys: Option>, - } - - impl ProfilePicPreview { - fn new() -> Self { - ProfilePicPreview { keys: None } - } - - fn show(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) { - egui::ScrollArea::both().show(ui, |ui| { - ui.horizontal_wrapped(|ui| { - let txn = Transaction::new(app.ndb).unwrap(); - - let keys = if let Some(keys) = &self.keys { - keys - } else { - return; - }; - - for key in keys { - let profile = app.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, _resp) = ui::anim::hover_expand( - ui, - egui::Id::new(profile.key().unwrap()), - ui::ProfilePic::default_size() as f32, - expand_size, - anim_speed, - ); - - ui.put( - rect, - ui::ProfilePic::new(app.img_cache, url) - .size(size) - .border(ui::ProfilePic::border_stroke(ui)), - ) - .on_hover_ui_at_pointer(|ui| { - ui.set_max_width(300.0); - ui.add(ui::ProfilePreview::new(&profile, app.img_cache)); - }); - } - }); - }); - } - - fn setup(&mut self, ndb: &Ndb) { - let txn = Transaction::new(ndb).unwrap(); - let filters = vec![Filter::new().kinds(vec![0]).build()]; - let mut pks = HashSet::new(); - let mut keys = HashSet::new(); - - for query_result in ndb.query(&txn, &filters, 20000).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: Vec = keys.into_iter().collect(); - info!("Loaded {} profiles", keys.len()); - self.keys = Some(keys); - } - } - - impl notedeck::App for ProfilePicPreview { - fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { - if self.keys.is_none() { - self.setup(ctx.ndb); - } - - self.show(ctx, ui) - } - } - - impl Preview for ProfilePic<'_, '_> { - type Prev = ProfilePicPreview; - - fn preview(_cfg: PreviewConfig) -> Self::Prev { - ProfilePicPreview::new() - } - } -} diff --git a/crates/notedeck_columns/src/ui/profile/preview.rs b/crates/notedeck_columns/src/ui/profile/preview.rs index 88b47145..a9877079 100644 --- a/crates/notedeck_columns/src/ui/profile/preview.rs +++ b/crates/notedeck_columns/src/ui/profile/preview.rs @@ -4,9 +4,10 @@ use egui::{Frame, Label, RichText, Widget}; use egui_extras::Size; use nostrdb::ProfileRecord; -use notedeck::{Images, NotedeckTextStyle, UserAccount}; +use notedeck::{Images, NotedeckTextStyle}; -use super::{about_section_widget, banner, display_name_widget, get_display_name, get_profile_url}; +use super::{about_section_widget, banner, display_name_widget, get_display_name}; +use notedeck_ui::profile::get_profile_url; pub struct ProfilePreview<'a, 'cache> { profile: &'a ProfileRecord<'a>, @@ -152,30 +153,6 @@ mod previews { } } -pub fn get_profile_url_owned(profile: Option>) -> &str { - if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { - url - } else { - ProfilePic::no_pfp_url() - } -} - -pub fn get_account_url<'a>( - txn: &'a nostrdb::Transaction, - ndb: &nostrdb::Ndb, - account: Option<&UserAccount>, -) -> &'a str { - if let Some(selected_account) = account { - if let Ok(profile) = ndb.get_profile_by_pubkey(txn, selected_account.key.pubkey.bytes()) { - get_profile_url_owned(Some(profile)) - } else { - get_profile_url_owned(None) - } - } else { - get_profile_url(None) - } -} - pub fn one_line_display_name_widget<'a>( visuals: &egui::Visuals, display_name: NostrName<'a>, diff --git a/crates/notedeck_columns/src/ui/relay.rs b/crates/notedeck_columns/src/ui/relay.rs index da29e79e..5bdb78cf 100644 --- a/crates/notedeck_columns/src/ui/relay.rs +++ b/crates/notedeck_columns/src/ui/relay.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::colors::PINK; +use notedeck_ui::colors::PINK; use crate::relay_pool_manager::{RelayPoolManager, RelayStatus}; use crate::ui::{Preview, PreviewConfig, View}; use egui::{ @@ -197,7 +197,7 @@ fn add_relay_button() -> Button<'static> { fn add_relay_button2(is_enabled: bool) -> impl egui::Widget + 'static { move |ui: &mut egui::Ui| -> egui::Response { - let button_widget = styled_button("Add", crate::colors::PINK); + let button_widget = styled_button("Add", notedeck_ui::colors::PINK); ui.add_enabled(is_enabled, button_widget) } } diff --git a/crates/notedeck_columns/src/ui/search_results.rs b/crates/notedeck_columns/src/ui/search_results.rs index c6d22484..2cf6789e 100644 --- a/crates/notedeck_columns/src/ui/search_results.rs +++ b/crates/notedeck_columns/src/ui/search_results.rs @@ -8,8 +8,8 @@ use crate::{ ui::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, }; -use super::widgets::x_button; -use super::{profile::get_profile_url, ProfilePic}; +use super::{widgets::x_button, ProfilePic}; +use notedeck_ui::profile::get_profile_url; pub struct SearchResultsView<'a> { ndb: &'a Ndb, diff --git a/crates/notedeck_columns/src/ui/side_panel.rs b/crates/notedeck_columns/src/ui/side_panel.rs index e61ee7ed..bd060556 100644 --- a/crates/notedeck_columns/src/ui/side_panel.rs +++ b/crates/notedeck_columns/src/ui/side_panel.rs @@ -1,35 +1,29 @@ use egui::{ - vec2, Button, Color32, InnerResponse, Label, Layout, Margin, RichText, ScrollArea, Separator, - Stroke, ThemePreference, Widget, + vec2, Color32, InnerResponse, Layout, Margin, RichText, ScrollArea, Separator, Stroke, Widget, }; use tracing::{error, info}; use crate::{ - accounts::AccountsRoute, app::{get_active_columns_mut, get_decks_mut}, app_style::DECK_ICON_SIZE, - colors, decks::{DecksAction, DecksCache}, nav::SwitchingAction, route::Route, - support::Support, }; -use notedeck::{Accounts, Images, NotedeckTextStyle, ThemeHandler, UserAccount}; +use notedeck::{Accounts, UserAccount}; +use notedeck_ui::colors; use super::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, configure_deck::deck_icon, - profile::preview::get_account_url, - ProfilePic, View, + View, }; pub static SIDE_PANEL_WIDTH: f32 = 68.0; static ICON_WIDTH: f32 = 40.0; pub struct DesktopSidePanel<'a> { - ndb: &'a nostrdb::Ndb, - img_cache: &'a mut Images, selected_account: Option<&'a UserAccount>, decks_cache: &'a DecksCache, } @@ -42,18 +36,13 @@ impl View for DesktopSidePanel<'_> { #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum SidePanelAction { - Panel, - Account, - Settings, Columns, ComposeNote, Search, ExpandSidePanel, - Support, NewDeck, SwitchDeck(usize), EditDeck(usize), - SaveTheme(ThemePreference), Wallet, } @@ -69,228 +58,133 @@ impl SidePanelResponse { } impl<'a> DesktopSidePanel<'a> { - pub fn new( - ndb: &'a nostrdb::Ndb, - img_cache: &'a mut Images, - selected_account: Option<&'a UserAccount>, - decks_cache: &'a DecksCache, - ) -> Self { + pub fn new(selected_account: Option<&'a UserAccount>, decks_cache: &'a DecksCache) -> Self { Self { - ndb, - img_cache, selected_account, decks_cache, } } - pub fn show(&mut self, ui: &mut egui::Ui) -> SidePanelResponse { - let mut frame = egui::Frame::new().inner_margin(Margin::same(8)); + pub fn show(&mut self, ui: &mut egui::Ui) -> Option { + let frame = egui::Frame::new().inner_margin(Margin::same(8)); if !ui.visuals().dark_mode { - frame = frame.fill(colors::ALMOST_WHITE); + let rect = ui.available_rect_before_wrap(); + ui.painter().rect( + rect, + 0, + colors::ALMOST_WHITE, + egui::Stroke::new(0.0, egui::Color32::TRANSPARENT), + egui::StrokeKind::Inside, + ); } frame.show(ui, |ui| self.show_inner(ui)).inner } - fn show_inner(&mut self, ui: &mut egui::Ui) -> SidePanelResponse { + fn show_inner(&mut self, ui: &mut egui::Ui) -> Option { let dark_mode = ui.ctx().style().visuals.dark_mode; let inner = ui .vertical(|ui| { - let top_resp = ui - .with_layout(Layout::top_down(egui::Align::Center), |ui| { - // macos needs a bit of space to make room for window - // minimize/close buttons - if cfg!(target_os = "macos") { - ui.add_space(24.0); - } + ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { + // macos needs a bit of space to make room for window + // minimize/close buttons + //if cfg!(target_os = "macos") { + // ui.add_space(24.0); + //} - let expand_resp = ui.add(expand_side_panel_button()); - ui.add_space(4.0); - ui.add(milestone_name()); - ui.add_space(16.0); - let is_interactive = self - .selected_account - .is_some_and(|s| s.key.secret_key.is_some()); - let compose_resp = ui.add(compose_note_button(is_interactive, dark_mode)); - let compose_resp = if is_interactive { - compose_resp - } else { - compose_resp.on_hover_cursor(egui::CursorIcon::NotAllowed) - }; - let search_resp = ui.add(search_button()); - let column_resp = ui.add(add_column_button(dark_mode)); + let is_interactive = self + .selected_account + .is_some_and(|s| s.key.secret_key.is_some()); + let compose_resp = ui.add(compose_note_button(is_interactive, dark_mode)); + let compose_resp = if is_interactive { + compose_resp + } else { + compose_resp.on_hover_cursor(egui::CursorIcon::NotAllowed) + }; + let search_resp = ui.add(search_button()); + let column_resp = ui.add(add_column_button(dark_mode)); - ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0)); + ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0)); - ui.add_space(8.0); - ui.add(egui::Label::new( - RichText::new("DECKS") - .size(11.0) - .color(ui.visuals().noninteractive().fg_stroke.color), - )); - ui.add_space(8.0); - let add_deck_resp = ui.add(add_deck_button()); + ui.add_space(8.0); + ui.add(egui::Label::new( + RichText::new("DECKS") + .size(11.0) + .color(ui.visuals().noninteractive().fg_stroke.color), + )); + ui.add_space(8.0); + let add_deck_resp = ui.add(add_deck_button()); - let decks_inner = ScrollArea::vertical() - .max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0))) - .show(ui, |ui| { - show_decks(ui, self.decks_cache, self.selected_account) - }) - .inner; - if expand_resp.clicked() { + let decks_inner = ScrollArea::vertical() + .max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0))) + .show(ui, |ui| { + show_decks(ui, self.decks_cache, self.selected_account) + }) + .inner; + + /* + if expand_resp.clicked() { + Some(InnerResponse::new( + SidePanelAction::ExpandSidePanel, + expand_resp, + )) + */ + if compose_resp.clicked() { + Some(InnerResponse::new( + SidePanelAction::ComposeNote, + compose_resp, + )) + } else if search_resp.clicked() { + Some(InnerResponse::new(SidePanelAction::Search, search_resp)) + } else if column_resp.clicked() { + Some(InnerResponse::new(SidePanelAction::Columns, column_resp)) + } else if add_deck_resp.clicked() { + Some(InnerResponse::new(SidePanelAction::NewDeck, add_deck_resp)) + } else if decks_inner.response.secondary_clicked() { + info!("decks inner secondary click"); + if let Some(clicked_index) = decks_inner.inner { Some(InnerResponse::new( - SidePanelAction::ExpandSidePanel, - expand_resp, + SidePanelAction::EditDeck(clicked_index), + decks_inner.response, )) - } else if compose_resp.clicked() { - Some(InnerResponse::new( - SidePanelAction::ComposeNote, - compose_resp, - )) - } else if search_resp.clicked() { - Some(InnerResponse::new(SidePanelAction::Search, search_resp)) - } else if column_resp.clicked() { - Some(InnerResponse::new(SidePanelAction::Columns, column_resp)) - } else if add_deck_resp.clicked() { - Some(InnerResponse::new(SidePanelAction::NewDeck, add_deck_resp)) - } else if decks_inner.response.secondary_clicked() { - info!("decks inner secondary click"); - if let Some(clicked_index) = decks_inner.inner { - Some(InnerResponse::new( - SidePanelAction::EditDeck(clicked_index), - decks_inner.response, - )) - } else { - None - } - } else if decks_inner.response.clicked() { - if let Some(clicked_index) = decks_inner.inner { - Some(InnerResponse::new( - SidePanelAction::SwitchDeck(clicked_index), - decks_inner.response, - )) - } else { - None - } } else { None } - }) - .inner; - - ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0)); - let (pfp_resp, bottom_resp) = ui - .with_layout(Layout::bottom_up(egui::Align::Center), |ui| { - let pfp_resp = self.pfp_button(ui); - let settings_resp = ui.add(settings_button(dark_mode)); - - let save_theme = if let Some((theme, resp)) = match ui.ctx().theme() { - egui::Theme::Dark => { - let resp = ui - .add(Button::new("☀").frame(false)) - .on_hover_text("Switch to light mode"); - if resp.clicked() { - Some((ThemePreference::Light, resp)) - } else { - None - } - } - egui::Theme::Light => { - let resp = ui - .add(Button::new("🌙").frame(false)) - .on_hover_text("Switch to dark mode"); - if resp.clicked() { - Some((ThemePreference::Dark, resp)) - } else { - None - } - } - } { - ui.ctx().set_theme(theme); - Some((theme, resp)) - } else { - None - }; - - let support_resp = ui.add(support_button()); - - let wallet_resp = ui.add(wallet_button()); - - let optional_inner = if pfp_resp.clicked() { - Some(egui::InnerResponse::new( - SidePanelAction::Account, - pfp_resp.clone(), - )) - } else if settings_resp.clicked() || settings_resp.hovered() { - Some(egui::InnerResponse::new( - SidePanelAction::Settings, - settings_resp, - )) - } else if support_resp.clicked() { - Some(egui::InnerResponse::new( - SidePanelAction::Support, - support_resp, - )) - } else if let Some((theme, resp)) = save_theme { - Some(egui::InnerResponse::new( - SidePanelAction::SaveTheme(theme), - resp, - )) - } else if wallet_resp.clicked() { - Some(egui::InnerResponse::new( - SidePanelAction::Wallet, - wallet_resp, + } else if decks_inner.response.clicked() { + if let Some(clicked_index) = decks_inner.inner { + Some(InnerResponse::new( + SidePanelAction::SwitchDeck(clicked_index), + decks_inner.response, )) } else { None - }; - - (pfp_resp, optional_inner) - }) - .inner; - - if let Some(bottom_inner) = bottom_resp { - bottom_inner - } else if let Some(top_inner) = top_resp { - top_inner - } else { - egui::InnerResponse::new(SidePanelAction::Panel, pfp_resp) - } + } + } else { + None + } + }) + .inner }) .inner; - SidePanelResponse::new(inner.inner, inner.response) - } - - fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response { - let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget - let helper = AnimationHelper::new(ui, "pfp-button", vec2(max_size, max_size)); - - let min_pfp_size = ICON_WIDTH; - let cur_pfp_size = helper.scale_1d_pos(min_pfp_size); - - let txn = nostrdb::Transaction::new(self.ndb).expect("should be able to create txn"); - let profile_url = get_account_url(&txn, self.ndb, self.selected_account); - - let widget = ProfilePic::new(self.img_cache, profile_url).size(cur_pfp_size); - - ui.put(helper.get_animation_rect(), widget); - - helper.take_animation_response() + if let Some(inner) = inner { + Some(SidePanelResponse::new(inner.inner, inner.response)) + } else { + None + } } pub fn perform_action( decks_cache: &mut DecksCache, accounts: &Accounts, - support: &mut Support, - theme_handler: &mut ThemeHandler, action: SidePanelAction, ) -> Option { let router = get_active_columns_mut(accounts, decks_cache).get_first_router(); let mut switching_response = None; match action { + /* SidePanelAction::Panel => {} // TODO SidePanelAction::Account => { if router @@ -312,6 +206,15 @@ impl<'a> DesktopSidePanel<'a> { router.route_to(Route::relays()); } } + SidePanelAction::Support => { + if router.routes().iter().any(|r| r == &Route::Support) { + router.go_back(); + } else { + support.refresh(); + router.route_to(Route::Support); + } + } + */ SidePanelAction::Columns => { if router .routes() @@ -342,14 +245,6 @@ impl<'a> DesktopSidePanel<'a> { // TODO info!("Clicked expand side panel button"); } - SidePanelAction::Support => { - if router.routes().iter().any(|r| r == &Route::Support) { - router.go_back(); - } else { - support.refresh(); - router.route_to(Route::Support); - } - } SidePanelAction::NewDeck => { if router.routes().iter().any(|r| r == &Route::NewDeck) { router.go_back(); @@ -382,9 +277,6 @@ impl<'a> DesktopSidePanel<'a> { } } } - SidePanelAction::SaveTheme(theme) => { - theme_handler.save(theme); - } SidePanelAction::Wallet => 's: { if router .routes() @@ -402,31 +294,6 @@ impl<'a> DesktopSidePanel<'a> { } } -fn settings_button(dark_mode: bool) -> impl Widget { - move |ui: &mut egui::Ui| { - let img_size = 24.0; - let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget - let img_data = if dark_mode { - egui::include_image!("../../../../assets/icons/settings_dark_4x.png") - } else { - egui::include_image!("../../../../assets/icons/settings_light_4x.png") - }; - let img = egui::Image::new(img_data).max_width(img_size); - - let helper = AnimationHelper::new(ui, "settings-button", vec2(max_size, max_size)); - - let cur_img_size = helper.scale_1d_pos(img_size); - img.paint_at( - ui, - helper - .get_animation_rect() - .shrink((max_size - cur_img_size) / 2.0), - ); - - helper.take_animation_response() - } -} - fn add_column_button(dark_mode: bool) -> impl Widget { move |ui: &mut egui::Ui| { let img_size = 24.0; @@ -554,41 +421,6 @@ pub fn search_button() -> impl Widget { } // TODO: convert to responsive button when expanded side panel impl is finished -fn expand_side_panel_button() -> impl Widget { - |ui: &mut egui::Ui| -> egui::Response { - let img_size = 40.0; - let img_data = egui::include_image!("../../../../assets/damus_rounded_80.png"); - let img = egui::Image::new(img_data).max_width(img_size); - - ui.add(img) - } -} - -fn support_button() -> impl Widget { - |ui: &mut egui::Ui| -> egui::Response { - let img_size = 16.0; - - let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget - let img_data = if ui.visuals().dark_mode { - egui::include_image!("../../../../assets/icons/help_icon_dark_4x.png") - } else { - egui::include_image!("../../../../assets/icons/help_icon_inverted_4x.png") - }; - let img = egui::Image::new(img_data).max_width(img_size); - - let helper = AnimationHelper::new(ui, "help-button", vec2(max_size, max_size)); - - let cur_img_size = helper.scale_1d_pos(img_size); - img.paint_at( - ui, - helper - .get_animation_rect() - .shrink((max_size - cur_img_size) / 2.0), - ); - - helper.take_animation_response() - } -} fn add_deck_button() -> impl Widget { |ui: &mut egui::Ui| -> egui::Response { @@ -676,23 +508,3 @@ fn show_decks<'a>( } InnerResponse::new(clicked_index, resp) } - -fn milestone_name() -> impl Widget { - |ui: &mut egui::Ui| -> egui::Response { - ui.vertical_centered(|ui| { - let font = egui::FontId::new( - notedeck::fonts::get_font_size( - ui.ctx(), - &NotedeckTextStyle::Tiny, - ), - egui::FontFamily::Name(notedeck::fonts::NamedFontFamily::Bold.as_str().into()), - ); - ui.add(Label::new( - RichText::new("ALPHA") - .color( ui.style().visuals.noninteractive().fg_stroke.color) - .font(font), - ).selectable(false)).on_hover_text("Notedeck is an alpha product. Expect bugs and contact us when you run into issues.").on_hover_cursor(egui::CursorIcon::Help) - }) - .inner - } -} diff --git a/crates/notedeck_columns/src/ui/support.rs b/crates/notedeck_columns/src/ui/support.rs index 948e5828..252bcc86 100644 --- a/crates/notedeck_columns/src/ui/support.rs +++ b/crates/notedeck_columns/src/ui/support.rs @@ -1,7 +1,8 @@ use egui::{vec2, Button, Label, Layout, RichText}; use tracing::error; -use crate::{colors::PINK, support::Support}; +use crate::support::Support; +use notedeck_ui::colors::PINK; use super::padding; use notedeck::{NamedFontFamily, NotedeckTextStyle}; diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs index 797fcb78..bbea1ece 100644 --- a/crates/notedeck_columns/src/ui/timeline.rs +++ b/crates/notedeck_columns/src/ui/timeline.rs @@ -193,7 +193,7 @@ fn goto_top_button(center: Pos2) -> impl egui::Widget { }); let painter = ui.painter(); - painter.circle_filled(center, helper.scale_1d_pos(radius), crate::colors::PINK); + painter.circle_filled(center, helper.scale_1d_pos(radius), notedeck_ui::colors::PINK); let create_pt = |angle: f32| { let side = radius / 2.0; diff --git a/crates/notedeck_columns/src/ui/wallet.rs b/crates/notedeck_columns/src/ui/wallet.rs index d918c3cc..454fb957 100644 --- a/crates/notedeck_columns/src/ui/wallet.rs +++ b/crates/notedeck_columns/src/ui/wallet.rs @@ -156,7 +156,7 @@ fn show_no_wallet( } ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { - ui.add(styled_button("Add Wallet", crate::colors::PINK)) + ui.add(styled_button("Add Wallet", notedeck_ui::colors::PINK)) .clicked() .then_some(WalletAction::SaveURI) }) diff --git a/crates/notedeck_dave/src/avatar.rs b/crates/notedeck_dave/src/avatar.rs index dd612a1f..c76dd77d 100644 --- a/crates/notedeck_dave/src/avatar.rs +++ b/crates/notedeck_dave/src/avatar.rs @@ -1,6 +1,6 @@ use std::num::NonZeroU64; -use crate::{Vec3, Quaternion}; +use crate::{Quaternion, Vec3}; use eframe::egui_wgpu::{self, wgpu}; use egui::{Rect, Response}; use rand::Rng; diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs index 572610bd..8ad74c9d 100644 --- a/crates/notedeck_dave/src/lib.rs +++ b/crates/notedeck_dave/src/lib.rs @@ -25,12 +25,12 @@ use avatar::DaveAvatar; use egui::{Rect, Vec2}; use egui_wgpu::RenderState; -pub use vec3::Vec3; pub use quaternion::Quaternion; +pub use vec3::Vec3; mod avatar; -mod vec3; mod quaternion; +mod vec3; #[derive(Debug, Clone)] pub enum Message { @@ -608,7 +608,7 @@ impl notedeck::App for Dave { #[derive(Debug, Clone)] enum ArgType { String, - Number, + //Number, Enum(Vec<&'static str>), } @@ -616,7 +616,7 @@ impl ArgType { pub fn type_string(&self) -> &'static str { match self { Self::String => "string", - Self::Number => "number", + //Self::Number => "number", Self::Enum(_) => "string", } } diff --git a/crates/notedeck_ui/Cargo.toml b/crates/notedeck_ui/Cargo.toml new file mode 100644 index 00000000..c13d3909 --- /dev/null +++ b/crates/notedeck_ui/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "notedeck_ui" +edition = "2021" +version.workspace = true + +[dependencies] +egui = { workspace = true } +egui_extras = { workspace = true } +ehttp = { workspace = true } +nostrdb = { workspace = true } +tracing = { workspace = true } +poll-promise = { workspace = true } +profiling = { workspace = true } +tokio = { workspace = true } +notedeck = { workspace = true } +image = { workspace = true } diff --git a/crates/notedeck_ui/src/anim.rs b/crates/notedeck_ui/src/anim.rs new file mode 100644 index 00000000..94007d9e --- /dev/null +++ b/crates/notedeck_ui/src/anim.rs @@ -0,0 +1,140 @@ +use egui::{Pos2, Rect, Response, Sense}; + +/* +pub fn hover_expand( + ui: &mut egui::Ui, + id: egui::Id, + size: f32, + expand_size: f32, + anim_speed: f32, +) -> (egui::Rect, f32, egui::Response) { + // 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::click()); + + let val = ui + .ctx() + .animate_bool_with_time(id, response.hovered(), anim_speed); + + let size = size + val * expand_size; + (rect, size, response) +} + +pub fn hover_expand_small(ui: &mut egui::Ui, id: egui::Id) -> (egui::Rect, f32, egui::Response) { + let size = 10.0; + let expand_size = 5.0; + let anim_speed = 0.05; + + hover_expand(ui, id, size, expand_size, anim_speed) +} +*/ + +pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; +pub static ANIM_SPEED: f32 = 0.05; +pub struct AnimationHelper { + rect: Rect, + center: Pos2, + response: Response, + animation_progress: f32, + expansion_multiple: f32, +} + +impl AnimationHelper { + pub fn new( + ui: &mut egui::Ui, + animation_name: impl std::hash::Hash, + max_size: egui::Vec2, + ) -> Self { + let id = ui.id().with(animation_name); + let (rect, response) = ui.allocate_exact_size(max_size, Sense::click()); + + let animation_progress = + ui.ctx() + .animate_bool_with_time(id, response.hovered(), ANIM_SPEED); + + Self { + rect, + center: rect.center(), + response, + animation_progress, + expansion_multiple: ICON_EXPANSION_MULTIPLE, + } + } + + pub fn no_animation(ui: &mut egui::Ui, size: egui::Vec2) -> Self { + let (rect, response) = ui.allocate_exact_size(size, Sense::hover()); + + Self { + rect, + center: rect.center(), + response, + animation_progress: 0.0, + expansion_multiple: ICON_EXPANSION_MULTIPLE, + } + } + + pub fn new_from_rect( + ui: &mut egui::Ui, + animation_name: impl std::hash::Hash, + animation_rect: egui::Rect, + ) -> Self { + let id = ui.id().with(animation_name); + let response = ui.allocate_rect(animation_rect, Sense::click()); + + let animation_progress = + ui.ctx() + .animate_bool_with_time(id, response.hovered(), ANIM_SPEED); + + Self { + rect: animation_rect, + center: animation_rect.center(), + response, + animation_progress, + expansion_multiple: ICON_EXPANSION_MULTIPLE, + } + } + + pub fn scale_1d_pos(&self, min_object_size: f32) -> f32 { + let max_object_size = min_object_size * self.expansion_multiple; + + if self.response.is_pointer_button_down_on() { + min_object_size + } else { + min_object_size + ((max_object_size - min_object_size) * self.animation_progress) + } + } + + pub fn scale_radius(&self, min_diameter: f32) -> f32 { + self.scale_1d_pos((min_diameter - 1.0) / 2.0) + } + + pub fn get_animation_rect(&self) -> egui::Rect { + self.rect + } + + pub fn center(&self) -> Pos2 { + self.rect.center() + } + + pub fn take_animation_response(self) -> egui::Response { + self.response + } + + // Scale a minimum position from center to the current animation position + pub fn scale_from_center(&self, x_min: f32, y_min: f32) -> Pos2 { + Pos2::new( + self.center.x + self.scale_1d_pos(x_min), + self.center.y + self.scale_1d_pos(y_min), + ) + } + + pub fn scale_pos_from_center(&self, min_pos: Pos2) -> Pos2 { + self.scale_from_center(min_pos.x, min_pos.y) + } + + /// New method for min/max scaling when needed + pub fn scale_1d_pos_min_max(&self, min_object_size: f32, max_object_size: f32) -> f32 { + min_object_size + ((max_object_size - min_object_size) * self.animation_progress) + } +} diff --git a/crates/notedeck_columns/src/colors.rs b/crates/notedeck_ui/src/colors.rs similarity index 100% rename from crates/notedeck_columns/src/colors.rs rename to crates/notedeck_ui/src/colors.rs diff --git a/crates/notedeck_columns/src/gif.rs b/crates/notedeck_ui/src/gif.rs similarity index 100% rename from crates/notedeck_columns/src/gif.rs rename to crates/notedeck_ui/src/gif.rs diff --git a/crates/notedeck_columns/src/images.rs b/crates/notedeck_ui/src/images.rs similarity index 82% rename from crates/notedeck_columns/src/images.rs rename to crates/notedeck_ui/src/images.rs index 24396041..197125a7 100644 --- a/crates/notedeck_columns/src/images.rs +++ b/crates/notedeck_ui/src/images.rs @@ -1,17 +1,12 @@ +use crate::ProfilePic; use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint}; use image::codecs::gif::GifDecoder; use image::imageops::FilterType; -use image::AnimationDecoder; -use image::DynamicImage; -use image::FlatSamples; -use image::Frame; -use notedeck::Animation; -use notedeck::ImageFrame; -use notedeck::MediaCache; -use notedeck::MediaCacheType; -use notedeck::Result; -use notedeck::TextureFrame; -use notedeck::TexturedImage; +use image::{AnimationDecoder, DynamicImage, FlatSamples, Frame}; +use notedeck::{ + Animation, GifStateMap, ImageFrame, Images, MediaCache, MediaCacheType, TextureFrame, + TexturedImage, +}; use poll_promise::Promise; use std::collections::VecDeque; use std::io::Cursor; @@ -153,7 +148,10 @@ fn process_pfp_bitmap(imgtyp: ImageType, mut image: image::DynamicImage) -> Colo } #[profiling::function] -fn parse_img_response(response: ehttp::Response, imgtyp: ImageType) -> Result { +fn parse_img_response( + response: ehttp::Response, + imgtyp: ImageType, +) -> Result { let content_type = response.content_type().unwrap_or_default(); let size_hint = match imgtyp { ImageType::Profile(size) => SizeHint::Size(size, size), @@ -181,10 +179,11 @@ fn fetch_img_from_disk( url: &str, path: &path::Path, cache_type: MediaCacheType, -) -> Promise> { +) -> Promise> { let ctx = ctx.clone(); let url = url.to_owned(); let path = path.to_owned(); + Promise::spawn_async(async move { match cache_type { MediaCacheType::Image => { @@ -220,7 +219,7 @@ fn generate_gif( data: Vec, write_to_disk: bool, process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static, -) -> Result { +) -> Result { let decoder = { let reader = Cursor::new(data.as_slice()); GifDecoder::new(reader)? @@ -336,7 +335,7 @@ fn buffer_to_color_image( ColorImage::from_rgba_unmultiplied([width as usize, height as usize], flat_samples.as_slice()) } -pub fn fetch_binary_from_disk(path: PathBuf) -> Result> { +pub fn fetch_binary_from_disk(path: PathBuf) -> Result, notedeck::Error> { std::fs::read(path).map_err(|e| notedeck::Error::Generic(e.to_string())) } @@ -355,7 +354,7 @@ pub fn fetch_img( url: &str, imgtyp: ImageType, cache_type: MediaCacheType, -) -> Promise> { +) -> Promise> { let key = MediaCache::key(url); let path = img_cache.cache_dir.join(key); @@ -374,7 +373,7 @@ fn fetch_img_from_net( url: &str, imgtyp: ImageType, cache_type: MediaCacheType, -) -> Promise> { +) -> Promise> { let (sender, promise) = Promise::new(); let request = ehttp::Request::get(url); let ctx = ctx.clone(); @@ -417,3 +416,73 @@ fn fetch_img_from_net( promise } + +#[allow(clippy::too_many_arguments)] +pub fn render_images( + ui: &mut egui::Ui, + images: &mut Images, + url: &str, + img_type: ImageType, + cache_type: MediaCacheType, + show_waiting: impl FnOnce(&mut egui::Ui), + show_error: impl FnOnce(&mut egui::Ui, String), + show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap), +) -> egui::Response { + let cache = match cache_type { + MediaCacheType::Image => &mut images.static_imgs, + MediaCacheType::Gif => &mut images.gifs, + }; + + render_media_cache( + ui, + cache, + &mut images.gif_states, + url, + img_type, + cache_type, + show_waiting, + show_error, + show_success, + ) +} + +#[allow(clippy::too_many_arguments)] +fn render_media_cache( + ui: &mut egui::Ui, + cache: &mut MediaCache, + gif_states: &mut GifStateMap, + url: &str, + img_type: ImageType, + cache_type: MediaCacheType, + show_waiting: impl FnOnce(&mut egui::Ui), + show_error: impl FnOnce(&mut egui::Ui, String), + show_success: impl FnOnce(&mut egui::Ui, &str, &mut TexturedImage, &mut GifStateMap), +) -> egui::Response { + let m_cached_promise = cache.map().get(url); + + if m_cached_promise.is_none() { + let res = crate::images::fetch_img(cache, ui.ctx(), url, img_type, cache_type.clone()); + cache.map_mut().insert(url.to_owned(), res); + } + + egui::Frame::NONE + .show(ui, |ui| { + match cache.map_mut().get_mut(url).and_then(|p| p.ready_mut()) { + None => show_waiting(ui), + Some(Err(err)) => { + let err = err.to_string(); + let no_pfp = crate::images::fetch_img( + cache, + ui.ctx(), + ProfilePic::no_pfp_url(), + ImageType::Profile(128), + cache_type, + ); + cache.map_mut().insert(url.to_owned(), no_pfp); + show_error(ui, err) + } + Some(Ok(renderable_media)) => show_success(ui, url, renderable_media, gif_states), + } + }) + .response +} diff --git a/crates/notedeck_ui/src/lib.rs b/crates/notedeck_ui/src/lib.rs new file mode 100644 index 00000000..75223c70 --- /dev/null +++ b/crates/notedeck_ui/src/lib.rs @@ -0,0 +1,8 @@ +mod anim; +pub mod colors; +pub mod gif; +pub mod images; +pub mod profile; + +pub use anim::AnimationHelper; +pub use profile::ProfilePic; diff --git a/crates/notedeck_ui/src/profile/mod.rs b/crates/notedeck_ui/src/profile/mod.rs new file mode 100644 index 00000000..f631931e --- /dev/null +++ b/crates/notedeck_ui/src/profile/mod.rs @@ -0,0 +1,17 @@ +use nostrdb::ProfileRecord; + +pub mod picture; + +pub use picture::ProfilePic; + +pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { + unwrap_profile_url(profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture()))) +} + +pub fn unwrap_profile_url(maybe_url: Option<&str>) -> &str { + if let Some(url) = maybe_url { + url + } else { + ProfilePic::no_pfp_url() + } +} diff --git a/crates/notedeck_ui/src/profile/picture.rs b/crates/notedeck_ui/src/profile/picture.rs new file mode 100644 index 00000000..c4b04d35 --- /dev/null +++ b/crates/notedeck_ui/src/profile/picture.rs @@ -0,0 +1,146 @@ +use crate::gif::{handle_repaint, retrieve_latest_texture}; +use crate::images::{render_images, ImageType}; +use egui::{vec2, Sense, Stroke, TextureHandle}; + +use notedeck::{supported_mime_hosted_at_url, Images}; + +pub struct ProfilePic<'cache, 'url> { + cache: &'cache mut Images, + url: &'url str, + size: f32, + border: Option, +} + +impl egui::Widget for ProfilePic<'_, '_> { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + render_pfp(ui, self.cache, self.url, self.size, self.border) + } +} + +impl<'cache, 'url> ProfilePic<'cache, 'url> { + pub fn new(cache: &'cache mut Images, url: &'url str) -> Self { + let size = Self::default_size() as f32; + ProfilePic { + cache, + url, + size, + border: None, + } + } + + pub fn border_stroke(ui: &egui::Ui) -> Stroke { + Stroke::new(4.0, ui.visuals().panel_fill) + } + + pub fn from_profile( + cache: &'cache mut Images, + profile: &nostrdb::ProfileRecord<'url>, + ) -> Option { + profile + .record() + .profile() + .and_then(|p| p.picture()) + .map(|url| ProfilePic::new(cache, url)) + } + + #[inline] + pub fn default_size() -> i8 { + 38 + } + + #[inline] + pub fn medium_size() -> i8 { + 32 + } + + #[inline] + pub fn small_size() -> i8 { + 24 + } + + #[inline] + pub fn no_pfp_url() -> &'static str { + "https://damus.io/img/no-profile.svg" + } + + #[inline] + pub fn size(mut self, size: f32) -> Self { + self.size = size; + self + } + + #[inline] + pub fn border(mut self, stroke: Stroke) -> Self { + self.border = Some(stroke); + self + } +} + +#[profiling::function] +fn render_pfp( + ui: &mut egui::Ui, + img_cache: &mut Images, + url: &str, + ui_size: f32, + border: Option, +) -> egui::Response { + // We will want to downsample these so it's not blurry on hi res displays + let img_size = 128u32; + + let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url) + .unwrap_or(notedeck::MediaCacheType::Image); + + render_images( + ui, + img_cache, + url, + ImageType::Profile(img_size), + cache_type, + |ui| { + paint_circle(ui, ui_size, border); + }, + |ui, _| { + paint_circle(ui, ui_size, border); + }, + |ui, url, renderable_media, gifs| { + let texture_handle = + handle_repaint(ui, retrieve_latest_texture(url, gifs, renderable_media)); + pfp_image(ui, texture_handle, ui_size, border); + }, + ) +} + +#[profiling::function] +fn pfp_image( + ui: &mut egui::Ui, + img: &TextureHandle, + size: f32, + border: Option, +) -> egui::Response { + let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); + if let Some(stroke) = border { + draw_bg_border(ui, rect.center(), size, stroke); + } + ui.put(rect, egui::Image::new(img).max_width(size)); + + response +} + +fn paint_circle(ui: &mut egui::Ui, size: f32, border: Option) -> egui::Response { + let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); + + if let Some(stroke) = border { + draw_bg_border(ui, rect.center(), size, stroke); + } + + ui.painter() + .circle_filled(rect.center(), size / 2.0, ui.visuals().weak_text_color()); + + response +} + +fn draw_bg_border(ui: &mut egui::Ui, center: egui::Pos2, size: f32, stroke: Stroke) { + let border_size = size + (stroke.width * 2.0); + ui.painter() + .circle_filled(center, border_size / 2.0, stroke.color); +}