From be3edc02a41c05d749f6ed5ef0dbc7b00c65de7b Mon Sep 17 00:00:00 2001 From: William Casarin Date: Tue, 3 Dec 2024 13:42:16 -0800 Subject: [PATCH] nav: refactor title rendering for flexibility Updated navigation to use a custom title renderer for more flexible rendering of navigation titles. This change decouples the rendering logic from predefined formats, enabling dynamic title compositions based on application context and data. This includes: - Refactoring `NavResponse` to introduce `NotedeckNavResponse` for handling unified navigation response data. - Adding `NavTitle` in `ui/column/header.rs` to handle rendering of navigation titles and profile images dynamically. - Updating route and timeline logic to support new rendering pipeline. - Replacing hardcoded title rendering with data-driven approaches. Benefits: - Simplifies navigation handling by consolidating title and action management. - Improves scalability for new navigation features without modifying core logic. - Enhances visual customization capabilities. Signed-off-by: William Casarin --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/nav.rs | 455 +++++++++++++------------------------- src/profile.rs | 2 +- src/route.rs | 53 ++--- src/timeline/kind.rs | 51 ++++- src/ui/column/header.rs | 208 +++++++++++++++++ src/ui/column/mod.rs | 3 + src/ui/mod.rs | 1 + src/ui/note/post.rs | 3 +- src/ui/profile/picture.rs | 1 + src/ui/profile/preview.rs | 38 ++-- 12 files changed, 464 insertions(+), 355 deletions(-) create mode 100644 src/ui/column/header.rs create mode 100644 src/ui/column/mod.rs diff --git a/Cargo.lock b/Cargo.lock index fc3e46c1..d62248d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "egui_nav" version = "0.1.0" -source = "git+https://github.com/damus-io/egui-nav?rev=fd0900bdff4be35709372e921f2b49f68b261469#fd0900bdff4be35709372e921f2b49f68b261469" +source = "git+https://github.com/damus-io/egui-nav?rev=867fb6e057a4cc0a13716d59d6d332a4c90607ea#867fb6e057a4cc0a13716d59d6d332a4c90607ea" dependencies = [ "egui", "egui_extras", diff --git a/Cargo.toml b/Cargo.toml index 33c20cb3..90eda1df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ eframe = { workspace = true } egui_extras = { workspace = true } ehttp = "0.2.0" egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" } -egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "fd0900bdff4be35709372e921f2b49f68b261469" } +egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "867fb6e057a4cc0a13716d59d6d332a4c90607ea" } egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" } reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] } image = { version = "0.25", features = ["jpeg", "png", "webp"] } diff --git a/src/nav.rs b/src/nav.rs index 85b10b0c..b9ace9c3 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -1,8 +1,6 @@ use crate::{ accounts::render_accounts_route, actionbar::NoteAction, - app_style::{get_font_size, NotedeckTextStyle}, - fonts::NamedFontFamily, notes_holder::NotesHolder, profile::Profile, relay_pool_manager::RelayPoolManager, @@ -15,7 +13,7 @@ use crate::{ ui::{ self, add_column::render_add_column_routes, - anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, + column::NavTitle, note::{PostAction, PostType}, support::SupportView, RelayView, View, @@ -23,12 +21,13 @@ use crate::{ Damus, }; -use egui::{pos2, Color32, InnerResponse, Stroke}; -use egui_nav::{Nav, NavAction, NavResponse, TitleBarResponse}; +use egui_nav::{Nav, NavAction, NavResponse, NavUiType}; use nostrdb::{Ndb, Transaction}; use tracing::{error, info}; pub enum RenderNavAction { + Back, + RemoveColumn, PostAction(PostAction), NoteAction(NoteAction), } @@ -45,17 +44,16 @@ impl From for RenderNavAction { } } +pub type NotedeckNavResponse = NavResponse>; + pub struct RenderNavResponse { column: usize, - response: NavResponse, TitleResponse>, + response: NotedeckNavResponse, } impl RenderNavResponse { #[allow(private_interfaces)] - pub fn new( - column: usize, - response: NavResponse, TitleResponse>, - ) -> Self { + pub fn new(column: usize, response: NotedeckNavResponse) -> Self { RenderNavResponse { column, response } } @@ -64,9 +62,28 @@ impl RenderNavResponse { let mut col_changed: bool = false; let col = self.column; - if let Some(action) = &self.response.inner { + if let Some(action) = self + .response + .response + .as_ref() + .or(self.response.title_response.as_ref()) + { // start returning when we're finished posting match action { + RenderNavAction::Back => { + app.columns_mut().column_mut(col).router_mut().go_back(); + } + + RenderNavAction::RemoveColumn => { + let tl = app.columns().find_timeline_for_column_index(col); + if let Some(timeline) = tl { + unsubscribe_timeline(app.ndb(), timeline); + } + + app.columns_mut().delete_column(col); + col_changed = true; + } + RenderNavAction::PostAction(post_action) => { let txn = Transaction::new(&app.ndb).expect("txn"); let _ = post_action.execute(&app.ndb, &txn, &mut app.pool, &mut app.drafts); @@ -91,61 +108,58 @@ impl RenderNavResponse { } } - if let Some(NavAction::Returned) = self.response.action { - let r = app.columns_mut().column_mut(col).router_mut().pop(); - let txn = Transaction::new(&app.ndb).expect("txn"); - if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { - let root_id = { - crate::note::root_note_id_from_selected_id( - &app.ndb, - &mut app.note_cache, - &txn, - id.bytes(), - ) - }; - Thread::unsubscribe_locally( - &txn, - &app.ndb, - &mut app.note_cache, - &mut app.threads, - &mut app.pool, - root_id, - &app.accounts.mutefun(), - ); - } - - if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r { - Profile::unsubscribe_locally( - &txn, - &app.ndb, - &mut app.note_cache, - &mut app.profiles, - &mut app.pool, - pubkey.bytes(), - &app.accounts.mutefun(), - ); - } - col_changed = true; - } else if let Some(NavAction::Navigated) = self.response.action { - let cur_router = app.columns_mut().column_mut(col).router_mut(); - cur_router.navigating = false; - if cur_router.is_replacing() { - cur_router.remove_previous_routes(); - } - col_changed = true; - } - - if let Some(title_response) = &self.response.title_response { - match title_response { - TitleResponse::RemoveColumn => { - let tl = app.columns().find_timeline_for_column_index(col); - if let Some(timeline) = tl { - unsubscribe_timeline(app.ndb(), timeline); + if let Some(action) = self.response.action { + match action { + NavAction::Returned => { + let r = app.columns_mut().column_mut(col).router_mut().pop(); + let txn = Transaction::new(&app.ndb).expect("txn"); + if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { + let root_id = { + crate::note::root_note_id_from_selected_id( + &app.ndb, + &mut app.note_cache, + &txn, + id.bytes(), + ) + }; + Thread::unsubscribe_locally( + &txn, + &app.ndb, + &mut app.note_cache, + &mut app.threads, + &mut app.pool, + root_id, + &app.accounts.mutefun(), + ); } - app.columns_mut().delete_column(col); + if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r { + Profile::unsubscribe_locally( + &txn, + &app.ndb, + &mut app.note_cache, + &mut app.profiles, + &mut app.pool, + pubkey.bytes(), + &app.accounts.mutefun(), + ); + } col_changed = true; } + + NavAction::Navigated => { + let cur_router = app.columns_mut().column_mut(col).router_mut(); + cur_router.navigating = false; + if cur_router.is_replacing() { + cur_router.remove_previous_routes(); + } + col_changed = true; + } + + NavAction::Dragging => {} + NavAction::Returning => {} + NavAction::Resetting => {} + NavAction::Navigating => {} } } @@ -153,87 +167,90 @@ impl RenderNavResponse { } } +fn render_nav_body( + ui: &mut egui::Ui, + app: &mut Damus, + top: &Route, + col: usize, +) -> Option { + match top { + Route::Timeline(tlr) => render_timeline_route( + &app.ndb, + &mut app.columns, + &mut app.drafts, + &mut app.img_cache, + &mut app.unknown_ids, + &mut app.note_cache, + &mut app.threads, + &mut app.profiles, + &mut app.accounts, + *tlr, + col, + app.textmode, + ui, + ), + Route::Accounts(amr) => { + let action = render_accounts_route( + ui, + &app.ndb, + col, + &mut app.columns, + &mut app.img_cache, + &mut app.accounts, + &mut app.view_state.login, + *amr, + ); + let txn = Transaction::new(&app.ndb).expect("txn"); + action.process_action(&mut app.unknown_ids, &app.ndb, &txn); + None + } + Route::Relays => { + let manager = RelayPoolManager::new(app.pool_mut()); + RelayView::new(manager).ui(ui); + None + } + Route::ComposeNote => { + let kp = app.accounts.get_selected_account()?.to_full()?; + let draft = app.drafts.compose_mut(); + + let txn = Transaction::new(&app.ndb).expect("txn"); + let post_response = ui::PostView::new( + &app.ndb, + draft, + PostType::New, + &mut app.img_cache, + &mut app.note_cache, + kp, + ) + .ui(&txn, ui); + + post_response.action.map(Into::into) + } + Route::AddColumn(route) => { + render_add_column_routes(ui, app, col, route); + + None + } + + Route::Support => { + SupportView::new(&mut app.support).show(ui); + None + } + } +} + #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"] pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> RenderNavResponse { let col_id = app.columns.get_column_id_at_index(col); // TODO(jb55): clean up this router_mut mess by using Router in egui-nav directly - let routes = app - .columns() - .column(col) - .router() - .routes() - .iter() - .map(|r| r.get_titled_route(&app.columns, &app.ndb)) - .collect(); - let nav_response = Nav::new(routes) + let nav_response = Nav::new(app.columns().column(col).router().routes().clone()) .navigating(app.columns_mut().column_mut(col).router_mut().navigating) .returning(app.columns_mut().column_mut(col).router_mut().returning) .id_source(egui::Id::new(col_id)) - .title(48.0, title_bar) - .show_mut(ui, |ui, nav| match &nav.top().route { - Route::Timeline(tlr) => render_timeline_route( - &app.ndb, - &mut app.columns, - &mut app.drafts, - &mut app.img_cache, - &mut app.unknown_ids, - &mut app.note_cache, - &mut app.threads, - &mut app.profiles, - &mut app.accounts, - *tlr, - col, - app.textmode, - ui, - ), - Route::Accounts(amr) => { - let action = render_accounts_route( - ui, - &app.ndb, - col, - &mut app.columns, - &mut app.img_cache, - &mut app.accounts, - &mut app.view_state.login, - *amr, - ); - let txn = Transaction::new(&app.ndb).expect("txn"); - action.process_action(&mut app.unknown_ids, &app.ndb, &txn); - None - } - Route::Relays => { - let manager = RelayPoolManager::new(app.pool_mut()); - RelayView::new(manager).ui(ui); - None - } - Route::ComposeNote => { - let kp = app.accounts.get_selected_account()?.to_full()?; - let draft = app.drafts.compose_mut(); - - let txn = nostrdb::Transaction::new(&app.ndb).expect("txn"); - let post_response = ui::PostView::new( - &app.ndb, - draft, - PostType::New, - &mut app.img_cache, - &mut app.note_cache, - kp, - ) - .ui(&txn, ui); - - post_response.action.map(Into::into) - } - Route::AddColumn(route) => { - render_add_column_routes(ui, app, col, route); - - None - } - - Route::Support => { - SupportView::new(&mut app.support).show(ui); - None - } + .show_mut(ui, |ui, render_type, nav| match render_type { + NavUiType::Title => NavTitle::new(nav.routes_arr()).show(ui), + NavUiType::Body => render_nav_body(ui, app, nav.routes().last().expect("top"), col), }); RenderNavResponse::new(col, nav_response) @@ -252,171 +269,3 @@ fn unsubscribe_timeline(ndb: &Ndb, timeline: &Timeline) { } } } - -fn title_bar( - ui: &mut egui::Ui, - allocated_response: egui::Response, - title_name: String, - back_name: Option, -) -> egui::InnerResponse> { - let icon_width = 32.0; - let padding_external = 16.0; - let padding_internal = 8.0; - let has_back = back_name.is_some(); - - let (spacing_rect, titlebar_rect) = allocated_response - .rect - .split_left_right_at_x(allocated_response.rect.left() + padding_external); - ui.advance_cursor_after_rect(spacing_rect); - - let (titlebar_resp, maybe_button_resp) = if has_back { - let (button_rect, titlebar_rect) = titlebar_rect - .split_left_right_at_x(allocated_response.rect.left() + icon_width + padding_external); - ( - allocated_response.with_new_rect(titlebar_rect), - Some(back_button(ui, button_rect)), - ) - } else { - (allocated_response, None) - }; - - title( - ui, - title_name, - titlebar_resp.rect, - icon_width, - if has_back { - padding_internal - } else { - padding_external - }, - ); - - let delete_button_resp = delete_column_button(ui, titlebar_resp, icon_width, padding_external); - let title_response = if delete_button_resp.clicked() { - Some(TitleResponse::RemoveColumn) - } else { - None - }; - - let titlebar_resp = TitleBarResponse { - title_response, - go_back: maybe_button_resp.map_or(false, |r| r.clicked()), - }; - - InnerResponse::new(titlebar_resp, delete_button_resp) -} - -fn back_button(ui: &mut egui::Ui, button_rect: egui::Rect) -> egui::Response { - let horizontal_length = 10.0; - let arrow_length = 5.0; - - let helper = AnimationHelper::new_from_rect(ui, "note-compose-button", button_rect); - let painter = ui.painter_at(helper.get_animation_rect()); - let stroke = Stroke::new(1.5, ui.visuals().text_color()); - - // Horizontal segment - let left_horizontal_point = pos2(-horizontal_length / 2., 0.); - let right_horizontal_point = pos2(horizontal_length / 2., 0.); - let scaled_left_horizontal_point = helper.scale_pos_from_center(left_horizontal_point); - let scaled_right_horizontal_point = helper.scale_pos_from_center(right_horizontal_point); - - painter.line_segment( - [scaled_left_horizontal_point, scaled_right_horizontal_point], - stroke, - ); - - // Top Arrow - let sqrt_2_over_2 = std::f32::consts::SQRT_2 / 2.; - let right_top_arrow_point = helper.scale_pos_from_center(pos2( - left_horizontal_point.x + (sqrt_2_over_2 * arrow_length), - right_horizontal_point.y + sqrt_2_over_2 * arrow_length, - )); - - let scaled_left_arrow_point = scaled_left_horizontal_point; - painter.line_segment([scaled_left_arrow_point, right_top_arrow_point], stroke); - - let right_bottom_arrow_point = helper.scale_pos_from_center(pos2( - left_horizontal_point.x + (sqrt_2_over_2 * arrow_length), - right_horizontal_point.y - sqrt_2_over_2 * arrow_length, - )); - - painter.line_segment([scaled_left_arrow_point, right_bottom_arrow_point], stroke); - - helper.take_animation_response() -} - -fn delete_column_button( - ui: &mut egui::Ui, - allocation_response: egui::Response, - icon_width: f32, - padding: f32, -) -> egui::Response { - let img_size = 16.0; - let max_size = icon_width * ICON_EXPANSION_MULTIPLE; - - let img_data = if ui.visuals().dark_mode { - egui::include_image!("../assets/icons/column_delete_icon_4x.png") - } else { - egui::include_image!("../assets/icons/column_delete_icon_light_4x.png") - }; - let img = egui::Image::new(img_data).max_width(img_size); - - let button_rect = { - let titlebar_rect = allocation_response.rect; - let titlebar_width = titlebar_rect.width(); - let titlebar_center = titlebar_rect.center(); - let button_center_y = titlebar_center.y; - let button_center_x = - titlebar_center.x + (titlebar_width / 2.0) - (max_size / 2.0) - padding; - egui::Rect::from_center_size( - pos2(button_center_x, button_center_y), - egui::vec2(max_size, max_size), - ) - }; - - let helper = AnimationHelper::new_from_rect(ui, "delete-column-button", button_rect); - - let cur_img_size = helper.scale_1d_pos_min_max(0.0, img_size); - - let animation_rect = helper.get_animation_rect(); - let animation_resp = helper.take_animation_response(); - - img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0)); - - animation_resp -} - -fn title( - ui: &mut egui::Ui, - title_name: String, - titlebar_rect: egui::Rect, - icon_width: f32, - padding: f32, -) { - let painter = ui.painter_at(titlebar_rect); - - let font = egui::FontId::new( - get_font_size(ui.ctx(), &NotedeckTextStyle::Body), - egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), - ); - - let max_title_width = titlebar_rect.width() - icon_width - padding * 2.; - let title_galley = - ui.fonts(|f| f.layout(title_name, font, ui.visuals().text_color(), max_title_width)); - - let pos = { - let titlebar_center = titlebar_rect.center(); - let text_height = title_galley.rect.height(); - - let galley_pos_x = titlebar_rect.left() + padding; - let galley_pos_y = titlebar_center.y - (text_height / 2.); - pos2(galley_pos_x, galley_pos_y) - }; - - painter.galley(pos, title_galley, Color32::WHITE); -} - -enum TitleResponse { - RemoveColumn, -} diff --git a/src/profile.rs b/src/profile.rs index 7b57de0d..2ddcf974 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -33,7 +33,7 @@ fn is_empty(s: &str) -> bool { s.chars().all(|c| c.is_whitespace()) } -pub fn get_profile_name<'a>(record: &'a ProfileRecord) -> Option> { +pub fn get_profile_name<'a>(record: &ProfileRecord<'a>) -> Option> { let profile = record.record().profile()?; let display_name = profile.display_name().filter(|n| !is_empty(n)); let name = profile.name().filter(|n| !is_empty(n)); diff --git a/src/route.rs b/src/route.rs index b102e25c..4b869612 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,5 +1,5 @@ use enostr::{NoteId, Pubkey}; -use nostrdb::Ndb; +use nostrdb::{Ndb, Transaction}; use serde::{Deserialize, Serialize}; use std::fmt::{self}; @@ -24,18 +24,6 @@ pub enum Route { Support, } -#[derive(Clone)] -pub struct TitledRoute { - pub route: Route, - pub title: String, -} - -impl fmt::Display for TitledRoute { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.title) - } -} - impl Route { pub fn timeline(timeline_id: TimelineId) -> Self { Route::Timeline(TimelineRoute::Timeline(timeline_id)) @@ -77,8 +65,8 @@ impl Route { Route::Accounts(AccountsRoute::AddAccount) } - pub fn get_titled_route(&self, columns: &Columns, ndb: &Ndb) -> TitledRoute { - let title = match self { + pub fn title(&self, columns: &Columns, ndb: &Ndb) -> String { + match self { Route::Timeline(tlr) => match tlr { TimelineRoute::Timeline(id) => { let timeline = columns @@ -87,16 +75,32 @@ impl Route { timeline.kind.to_title(ndb) } TimelineRoute::Thread(id) => { - format!("{}'s Thread", get_note_users_displayname_string(ndb, id)) + let txn = Transaction::new(ndb).expect("txn"); + format!( + "{}'s Thread", + get_note_users_displayname_string(&txn, ndb, id) + ) } TimelineRoute::Reply(id) => { - format!("{}'s Reply", get_note_users_displayname_string(ndb, id)) + let txn = Transaction::new(ndb).expect("txn"); + format!( + "{}'s Reply", + get_note_users_displayname_string(&txn, ndb, id) + ) } TimelineRoute::Quote(id) => { - format!("{}'s Quote", get_note_users_displayname_string(ndb, id)) + let txn = Transaction::new(ndb).expect("txn"); + format!( + "{}'s Quote", + get_note_users_displayname_string(&txn, ndb, id) + ) } TimelineRoute::Profile(pubkey) => { - format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey)) + let txn = Transaction::new(ndb).expect("txn"); + format!( + "{}'s Profile", + get_profile_displayname_string(&txn, ndb, pubkey) + ) } }, @@ -116,11 +120,6 @@ impl Route { AddColumnRoute::Hashtag => "Add Hashtag Column".to_owned(), }, Route::Support => "Damus Support".to_owned(), - }; - - TitledRoute { - title, - route: *self, } } } @@ -169,7 +168,7 @@ impl Router { return None; } self.returning = true; - self.routes.get(self.routes.len() - 2).cloned() + self.prev().cloned() } /// Pop a route, should only be called on a NavRespose::Returned reseponse @@ -200,6 +199,10 @@ impl Router { self.routes.last().expect("routes can't be empty") } + pub fn prev(&self) -> Option<&R> { + self.routes.get(self.routes.len() - 2) + } + pub fn routes(&self) -> &Vec { &self.routes } diff --git a/src/timeline/kind.rs b/src/timeline/kind.rs index 51960811..78af0207 100644 --- a/src/timeline/kind.rs +++ b/src/timeline/kind.rs @@ -20,6 +20,23 @@ pub enum ListKind { Contact(PubkeySource), } +impl PubkeySource { + pub fn to_pubkey<'a>(&'a self, deck_author: &'a Pubkey) -> &'a Pubkey { + match self { + PubkeySource::Explicit(pk) => pk, + PubkeySource::DeckAuthor => deck_author, + } + } +} + +impl ListKind { + pub fn pubkey_source(&self) -> Option<&PubkeySource> { + match self { + ListKind::Contact(pk_src) => Some(pk_src), + } + } +} + /// /// What kind of timeline is it? /// - Follow List @@ -58,6 +75,17 @@ impl Display for TimelineKind { } impl TimelineKind { + pub fn pubkey_source(&self) -> Option<&PubkeySource> { + match self { + TimelineKind::List(list_kind) => list_kind.pubkey_source(), + TimelineKind::Notifications(pk_src) => Some(pk_src), + TimelineKind::Profile(pk_src) => Some(pk_src), + TimelineKind::Universe => None, + TimelineKind::Generic => None, + TimelineKind::Hashtag(_ht) => None, + } + } + pub fn contact_list(pk: PubkeySource) -> Self { TimelineKind::List(ListKind::Contact(pk)) } @@ -171,22 +199,33 @@ impl TimelineKind { TimelineKind::List(list_kind) => match list_kind { ListKind::Contact(pubkey_source) => match pubkey_source { PubkeySource::Explicit(pubkey) => { - format!("{}'s Contacts", get_profile_displayname_string(ndb, pubkey)) + let txn = Transaction::new(ndb).expect("txn"); + format!( + "{}'s Contacts", + get_profile_displayname_string(&txn, ndb, pubkey) + ) } PubkeySource::DeckAuthor => "Contacts".to_owned(), }, }, TimelineKind::Notifications(pubkey_source) => match pubkey_source { PubkeySource::DeckAuthor => "Notifications".to_owned(), - PubkeySource::Explicit(pk) => format!( - "{}'s Notifications", - get_profile_displayname_string(ndb, pk) - ), + PubkeySource::Explicit(pk) => { + let txn = Transaction::new(ndb).expect("txn"); + format!( + "{}'s Notifications", + get_profile_displayname_string(&txn, ndb, pk) + ) + } }, TimelineKind::Profile(pubkey_source) => match pubkey_source { PubkeySource::DeckAuthor => "Profile".to_owned(), PubkeySource::Explicit(pk) => { - format!("{}'s Profile", get_profile_displayname_string(ndb, pk)) + let txn = Transaction::new(ndb).expect("txn"); + format!( + "{}'s Profile", + get_profile_displayname_string(&txn, ndb, pk) + ) } }, TimelineKind::Universe => "Universe".to_owned(), diff --git a/src/ui/column/header.rs b/src/ui/column/header.rs new file mode 100644 index 00000000..5c4a4f13 --- /dev/null +++ b/src/ui/column/header.rs @@ -0,0 +1,208 @@ +use crate::{ + app_style::{get_font_size, NotedeckTextStyle}, + fonts::NamedFontFamily, + nav::RenderNavAction, + route::Route, + ui::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, +}; + +use egui::{pos2, Color32, Stroke}; + +pub struct NavTitle<'a> { + routes: &'a [Route], +} + +impl<'a> NavTitle<'a> { + pub fn new(routes: &'a [Route]) -> Self { + NavTitle { routes } + } + + pub fn show(&mut self, ui: &mut egui::Ui) -> Option { + let mut rect = ui.available_rect_before_wrap(); + rect.set_height(48.0); + let bar = ui.allocate_rect(rect, egui::Sense::hover()); + + self.title_bar(ui, bar) + } + + fn title_bar( + &mut self, + ui: &mut egui::Ui, + allocated_response: egui::Response, + ) -> Option { + let icon_width = 32.0; + let padding_external = 16.0; + let padding_internal = 8.0; + let has_back = prev(self.routes).is_some(); + + let (spacing_rect, titlebar_rect) = allocated_response + .rect + .split_left_right_at_x(allocated_response.rect.left() + padding_external); + + ui.advance_cursor_after_rect(spacing_rect); + + let (titlebar_resp, back_button_resp) = if has_back { + let (button_rect, titlebar_rect) = titlebar_rect.split_left_right_at_x( + allocated_response.rect.left() + icon_width + padding_external, + ); + ( + allocated_response.with_new_rect(titlebar_rect), + Some(self.back_button(ui, button_rect)), + ) + } else { + (allocated_response, None) + }; + + self.title( + ui, + self.routes.last().unwrap(), + titlebar_resp.rect, + icon_width, + if has_back { + padding_internal + } else { + padding_external + }, + ); + + let delete_button_resp = + self.delete_column_button(ui, titlebar_resp, icon_width, padding_external); + + if delete_button_resp.clicked() { + Some(RenderNavAction::RemoveColumn) + } else if back_button_resp.map_or(false, |r| r.clicked()) { + Some(RenderNavAction::Back) + } else { + None + } + } + + fn back_button(&self, ui: &mut egui::Ui, button_rect: egui::Rect) -> egui::Response { + let horizontal_length = 10.0; + let arrow_length = 5.0; + + let helper = AnimationHelper::new_from_rect(ui, "note-compose-button", button_rect); + let painter = ui.painter_at(helper.get_animation_rect()); + let stroke = Stroke::new(1.5, ui.visuals().text_color()); + + // Horizontal segment + let left_horizontal_point = pos2(-horizontal_length / 2., 0.); + let right_horizontal_point = pos2(horizontal_length / 2., 0.); + let scaled_left_horizontal_point = helper.scale_pos_from_center(left_horizontal_point); + let scaled_right_horizontal_point = helper.scale_pos_from_center(right_horizontal_point); + + painter.line_segment( + [scaled_left_horizontal_point, scaled_right_horizontal_point], + stroke, + ); + + // Top Arrow + let sqrt_2_over_2 = std::f32::consts::SQRT_2 / 2.; + let right_top_arrow_point = helper.scale_pos_from_center(pos2( + left_horizontal_point.x + (sqrt_2_over_2 * arrow_length), + right_horizontal_point.y + sqrt_2_over_2 * arrow_length, + )); + + let scaled_left_arrow_point = scaled_left_horizontal_point; + painter.line_segment([scaled_left_arrow_point, right_top_arrow_point], stroke); + + let right_bottom_arrow_point = helper.scale_pos_from_center(pos2( + left_horizontal_point.x + (sqrt_2_over_2 * arrow_length), + right_horizontal_point.y - sqrt_2_over_2 * arrow_length, + )); + + painter.line_segment([scaled_left_arrow_point, right_bottom_arrow_point], stroke); + + helper.take_animation_response() + } + + fn delete_column_button( + &self, + ui: &mut egui::Ui, + allocation_response: egui::Response, + icon_width: f32, + padding: f32, + ) -> egui::Response { + let img_size = 16.0; + let max_size = icon_width * ICON_EXPANSION_MULTIPLE; + + let img_data = if ui.visuals().dark_mode { + egui::include_image!("../../../assets/icons/column_delete_icon_4x.png") + } else { + egui::include_image!("../../../assets/icons/column_delete_icon_light_4x.png") + }; + let img = egui::Image::new(img_data).max_width(img_size); + + let button_rect = { + let titlebar_rect = allocation_response.rect; + let titlebar_width = titlebar_rect.width(); + let titlebar_center = titlebar_rect.center(); + let button_center_y = titlebar_center.y; + let button_center_x = + titlebar_center.x + (titlebar_width / 2.0) - (max_size / 2.0) - padding; + egui::Rect::from_center_size( + pos2(button_center_x, button_center_y), + egui::vec2(max_size, max_size), + ) + }; + + let helper = AnimationHelper::new_from_rect(ui, "delete-column-button", button_rect); + + let cur_img_size = helper.scale_1d_pos_min_max(0.0, img_size); + + let animation_rect = helper.get_animation_rect(); + let animation_resp = helper.take_animation_response(); + + img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0)); + + animation_resp + } + + fn title( + &mut self, + ui: &mut egui::Ui, + top: &Route, + titlebar_rect: egui::Rect, + icon_width: f32, + padding: f32, + ) { + let painter = ui.painter_at(titlebar_rect); + + let font = egui::FontId::new( + get_font_size(ui.ctx(), &NotedeckTextStyle::Body), + egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), + ); + + let max_title_width = titlebar_rect.width() - icon_width - padding * 2.; + + let title_galley = ui.fonts(|f| { + f.layout( + top.to_string(), + font, + ui.visuals().text_color(), + max_title_width, + ) + }); + + let pos = { + let titlebar_center = titlebar_rect.center(); + let text_height = title_galley.rect.height(); + + let galley_pos_x = titlebar_rect.left() + padding; + let galley_pos_y = titlebar_center.y - (text_height / 2.); + pos2(galley_pos_x, galley_pos_y) + }; + + painter.galley(pos, title_galley, Color32::WHITE); + } +} + +fn prev(xs: &[R]) -> Option<&R> { + let len = xs.len() as i32; + let ind = len - 2; + if ind < 0 { + None + } else { + Some(&xs[ind as usize]) + } +} diff --git a/src/ui/column/mod.rs b/src/ui/column/mod.rs new file mode 100644 index 00000000..004265d8 --- /dev/null +++ b/src/ui/column/mod.rs @@ -0,0 +1,3 @@ +mod header; + +pub use header::NavTitle; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 95e4afcc..330525f9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,6 +2,7 @@ pub mod account_login_view; pub mod accounts; pub mod add_column; pub mod anim; +pub mod column; pub mod mention; pub mod note; pub mod preview; diff --git a/src/ui/note/post.rs b/src/ui/note/post.rs index 7f30e02a..1c15baf6 100644 --- a/src/ui/note/post.rs +++ b/src/ui/note/post.rs @@ -2,8 +2,7 @@ use crate::draft::{Draft, Drafts}; use crate::imgcache::ImageCache; use crate::notecache::NoteCache; use crate::post::NewPost; -use crate::ui; -use crate::ui::{Preview, PreviewConfig, View}; +use crate::ui::{self, Preview, PreviewConfig, View}; use crate::Result; use egui::widgets::text_edit::TextEdit; use egui::{Frame, Layout}; diff --git a/src/ui/profile/picture.rs b/src/ui/profile/picture.rs index b6d42cd6..f9b2cc6e 100644 --- a/src/ui/profile/picture.rs +++ b/src/ui/profile/picture.rs @@ -2,6 +2,7 @@ use crate::images::ImageType; use crate::imgcache::ImageCache; use crate::ui::{Preview, PreviewConfig, View}; use egui::{vec2, Sense, TextureHandle}; +use nostrdb::{Ndb, Transaction}; pub struct ProfilePic<'cache, 'url> { cache: &'cache mut ImageCache, diff --git a/src/ui/profile/preview.rs b/src/ui/profile/preview.rs index 62926c0c..e0a691e3 100644 --- a/src/ui/profile/preview.rs +++ b/src/ui/profile/preview.rs @@ -7,8 +7,8 @@ use crate::{colors, images, DisplayName}; use egui::load::TexturePoll; use egui::{Frame, Label, RichText, Sense, Widget}; use egui_extras::Size; -use enostr::NoteId; -use nostrdb::ProfileRecord; +use enostr::{NoteId, Pubkey}; +use nostrdb::{Ndb, ProfileRecord, Transaction}; pub struct ProfilePreview<'a, 'cache> { profile: &'a ProfileRecord<'a>, @@ -176,7 +176,7 @@ mod previews { } } -pub fn get_display_name<'a>(profile: Option<&'a ProfileRecord<'a>>) -> DisplayName<'a> { +pub fn get_display_name<'a>(profile: Option<&ProfileRecord<'a>>) -> DisplayName<'a> { if let Some(name) = profile.and_then(|p| crate::profile::get_profile_name(p)) { name } else { @@ -184,7 +184,7 @@ pub fn get_display_name<'a>(profile: Option<&'a ProfileRecord<'a>>) -> DisplayNa } } -pub fn get_profile_url<'a>(profile: Option<&'a ProfileRecord<'a>>) -> &'a str { +pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { url } else { @@ -279,8 +279,11 @@ pub fn one_line_display_name_widget( } } -fn about_section_widget<'a>(profile: &'a ProfileRecord<'a>) -> impl egui::Widget + 'a { - |ui: &mut egui::Ui| { +fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b +where + 'b: 'a, +{ + move |ui: &mut egui::Ui| { if let Some(about) = profile.record().profile().and_then(|p| p.about()) { ui.label(about) } else { @@ -290,27 +293,30 @@ fn about_section_widget<'a>(profile: &'a ProfileRecord<'a>) -> impl egui::Widget } } -fn get_display_name_as_string(profile: Option<&'_ ProfileRecord<'_>>) -> String { +fn get_display_name_as_string<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str { let display_name = get_display_name(profile); match display_name { - DisplayName::One(n) => n.to_string(), - DisplayName::Both { display_name, .. } => display_name.to_string(), + DisplayName::One(n) => n, + DisplayName::Both { display_name, .. } => display_name, } } -pub fn get_profile_displayname_string(ndb: &nostrdb::Ndb, pk: &enostr::Pubkey) -> String { - let txn = nostrdb::Transaction::new(ndb).expect("Transaction should have worked"); - let profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok(); +pub fn get_profile_displayname_string<'a>(txn: &'a Transaction, ndb: &Ndb, pk: &Pubkey) -> &'a str { + let profile = ndb.get_profile_by_pubkey(txn, pk.bytes()).ok(); get_display_name_as_string(profile.as_ref()) } -pub fn get_note_users_displayname_string(ndb: &nostrdb::Ndb, id: &NoteId) -> String { - let txn = nostrdb::Transaction::new(ndb).expect("Transaction should have worked"); - let note = ndb.get_note_by_id(&txn, id.bytes()); +pub fn get_note_users_displayname_string<'a>( + txn: &'a Transaction, + ndb: &Ndb, + id: &NoteId, +) -> &'a str { + let note = ndb.get_note_by_id(txn, id.bytes()); let profile = if let Ok(note) = note { - ndb.get_profile_by_pubkey(&txn, note.pubkey()).ok() + ndb.get_profile_by_pubkey(txn, note.pubkey()).ok() } else { None }; + get_display_name_as_string(profile.as_ref()) }