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()) }