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>
This commit is contained in:
William Casarin
2024-12-03 13:42:16 -08:00
parent cf773a90fd
commit be3edc02a4
12 changed files with 464 additions and 355 deletions

208
src/ui/column/header.rs Normal file
View File

@@ -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])
}
}

3
src/ui/column/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod header;
pub use header::NavTitle;

View File

@@ -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;

View File

@@ -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};

View File

@@ -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,

View File

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