commit be3edc02a41c05d749f6ed5ef0dbc7b00c65de7b
parent cf773a90fdbd3cad410426c9fc32bf09686bbcd8
Author: William Casarin <jb55@jb55.com>
Date: Tue, 3 Dec 2024 13:42:16 -0800
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 <jb55@jb55.com>
Diffstat:
12 files changed, 463 insertions(+), 354 deletions(-)
diff --git 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
@@ -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
@@ -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<NoteAction> for RenderNavAction {
}
}
+pub type NotedeckNavResponse = NavResponse<Option<RenderNavAction>>;
+
pub struct RenderNavResponse {
column: usize,
- response: NavResponse<Option<RenderNavAction>, TitleResponse>,
+ response: NotedeckNavResponse,
}
impl RenderNavResponse {
#[allow(private_interfaces)]
- pub fn new(
- column: usize,
- response: NavResponse<Option<RenderNavAction>, 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(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(),
+ );
+ }
- 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(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;
+ }
- app.columns_mut().delete_column(col);
+ 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 {
}
}
-#[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<R> 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)
- .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(
+fn render_nav_body(
+ ui: &mut egui::Ui,
+ app: &mut Damus,
+ top: &Route,
+ col: usize,
+) -> Option<RenderNavAction> {
+ 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.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();
+ &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);
- 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);
- post_response.action.map(Into::into)
- }
- Route::AddColumn(route) => {
- render_add_column_routes(ui, app, col, route);
+ None
+ }
- None
- }
+ Route::Support => {
+ SupportView::new(&mut app.support).show(ui);
+ 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<R> in egui-nav directly
+
+ 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))
+ .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<String>,
-) -> egui::InnerResponse<TitleBarResponse<TitleResponse>> {
- 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
@@ -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<DisplayName<'a>> {
+pub fn get_profile_name<'a>(record: &ProfileRecord<'a>) -> Option<DisplayName<'a>> {
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
@@ -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<R: Clone> Router<R> {
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<R: Clone> Router<R> {
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<R> {
&self.routes
}
diff --git 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
@@ -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<RenderNavAction> {
+ 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<RenderNavAction> {
+ 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<R>(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
@@ -0,0 +1,3 @@
+mod header;
+
+pub use header::NavTitle;
diff --git 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
@@ -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
@@ -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
@@ -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())
}