notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

nav.rs (8343B)


      1 use egui::{CornerRadius, CursorIcon, Frame, Margin, Sense, Stroke};
      2 use egui_nav::{NavResponse, RouteResponse};
      3 use enostr::Pubkey;
      4 use nostrdb::Ndb;
      5 use notedeck::{
      6     tr, ui::is_narrow, ContactState, Images, Localization, MediaJobSender, Router, Settings,
      7 };
      8 use notedeck_ui::{
      9     app_images,
     10     header::{chevron, HorizontalHeader},
     11 };
     12 
     13 use crate::{
     14     cache::{ConversationCache, ConversationStates},
     15     nav::{MessagesAction, Route},
     16     ui::{
     17         conversation_header_impl, convo::conversation_ui, convo_list::ConversationListUi,
     18         create_convo::CreateConvoUi, title_label,
     19     },
     20 };
     21 
     22 #[allow(clippy::too_many_arguments)]
     23 pub fn render_nav(
     24     ui: &mut egui::Ui,
     25     router: &Router<Route>,
     26     settings: &Settings,
     27     cache: &ConversationCache,
     28     states: &mut ConversationStates,
     29     jobs: &MediaJobSender,
     30     ndb: &Ndb,
     31     selected_pubkey: &Pubkey,
     32     img_cache: &mut Images,
     33     contacts: &ContactState,
     34     i18n: &mut Localization,
     35 ) -> NavResponse<Option<MessagesAction>> {
     36     ui.painter().rect(
     37         ui.available_rect_before_wrap(),
     38         CornerRadius::ZERO,
     39         ui.visuals().faint_bg_color,
     40         Stroke::NONE,
     41         egui::StrokeKind::Inside,
     42     );
     43 
     44     if cfg!(target_os = "macos") {
     45         ui.add_space(16.0);
     46     }
     47 
     48     egui_nav::Nav::new(router.routes())
     49         .navigating(router.navigating)
     50         .returning(router.returning)
     51         .animate_transitions(settings.animate_nav_transitions)
     52         .show_mut(ui, |ui, render_type, nav| match render_type {
     53             egui_nav::NavUiType::Title => {
     54                 let mut nav_title = NavTitle::new(
     55                     nav.routes(),
     56                     cache,
     57                     jobs,
     58                     ndb,
     59                     selected_pubkey,
     60                     img_cache,
     61                     i18n,
     62                 );
     63                 let response = nav_title.show(ui);
     64 
     65                 RouteResponse {
     66                     response,
     67                     can_take_drag_from: Vec::new(),
     68                 }
     69             }
     70             egui_nav::NavUiType::Body => {
     71                 let Some(top) = nav.routes().last() else {
     72                     return RouteResponse {
     73                         response: None,
     74                         can_take_drag_from: Vec::new(),
     75                     };
     76                 };
     77 
     78                 render_nav_body(
     79                     top,
     80                     cache,
     81                     states,
     82                     jobs,
     83                     ndb,
     84                     selected_pubkey,
     85                     ui,
     86                     img_cache,
     87                     contacts,
     88                     i18n,
     89                 )
     90             }
     91         })
     92 }
     93 
     94 #[allow(clippy::too_many_arguments)]
     95 fn render_nav_body(
     96     top: &Route,
     97     cache: &ConversationCache,
     98     states: &mut ConversationStates,
     99     jobs: &MediaJobSender,
    100     ndb: &Ndb,
    101     selected_pubkey: &Pubkey,
    102     ui: &mut egui::Ui,
    103     img_cache: &mut Images,
    104     contacts: &ContactState,
    105     i18n: &mut Localization,
    106 ) -> RouteResponse<Option<MessagesAction>> {
    107     let response = match top {
    108         Route::ConvoList => {
    109             let mut frame = Frame::new();
    110             if !is_narrow(ui.ctx()) {
    111                 frame = frame.inner_margin(Margin {
    112                     left: 12,
    113                     right: 12,
    114                     top: 0,
    115                     bottom: 10,
    116                 });
    117             }
    118             frame
    119                 .show(ui, |ui| {
    120                     ConversationListUi::new(cache, states, jobs, ndb, img_cache, i18n)
    121                         .ui(ui, selected_pubkey)
    122                 })
    123                 .inner
    124         }
    125         Route::CreateConvo => 's: {
    126             let Some(r) = CreateConvoUi::new(ndb, jobs, img_cache, contacts, i18n).ui(ui) else {
    127                 break 's None;
    128             };
    129 
    130             Some(MessagesAction::Create {
    131                 recipient: r.recipient,
    132             })
    133         }
    134         Route::Conversation => conversation_ui(
    135             cache,
    136             states,
    137             jobs,
    138             ndb,
    139             ui,
    140             img_cache,
    141             i18n,
    142             selected_pubkey,
    143         ),
    144     };
    145 
    146     RouteResponse {
    147         response,
    148         can_take_drag_from: vec![],
    149     }
    150 }
    151 
    152 pub struct NavTitle<'a> {
    153     routes: &'a [Route],
    154     cache: &'a ConversationCache,
    155     jobs: &'a MediaJobSender,
    156     ndb: &'a Ndb,
    157     selected_pubkey: &'a Pubkey,
    158     img_cache: &'a mut Images,
    159     i18n: &'a mut Localization,
    160 }
    161 
    162 impl<'a> NavTitle<'a> {
    163     pub fn new(
    164         routes: &'a [Route],
    165         cache: &'a ConversationCache,
    166         jobs: &'a MediaJobSender,
    167         ndb: &'a Ndb,
    168         selected_pubkey: &'a Pubkey,
    169         img_cache: &'a mut Images,
    170         i18n: &'a mut Localization,
    171     ) -> Self {
    172         Self {
    173             routes,
    174             cache,
    175             jobs,
    176             ndb,
    177             selected_pubkey,
    178             img_cache,
    179             i18n,
    180         }
    181     }
    182 
    183     pub fn show(&mut self, ui: &mut egui::Ui) -> Option<MessagesAction> {
    184         self.title_bar(ui)
    185     }
    186 
    187     fn title_bar(&mut self, ui: &mut egui::Ui) -> Option<MessagesAction> {
    188         let top = self.routes.last()?;
    189 
    190         let mut right_action = None;
    191         let mut left_action = None;
    192 
    193         HorizontalHeader::new(48.0)
    194             .with_margin(Margin::symmetric(12, 8))
    195             .ui(
    196                 ui,
    197                 0,
    198                 1,
    199                 2,
    200                 |ui: &mut egui::Ui| {
    201                     let chev_width = 12.0;
    202                     left_action = if prev(self.routes).is_some() {
    203                         back_button(ui, egui::vec2(chev_width, 20.0))
    204                             .on_hover_cursor(CursorIcon::PointingHand)
    205                             .clicked()
    206                             .then_some(MessagesAction::Back)
    207                     } else {
    208                         ui.add(app_images::damus_image().max_width(32.0))
    209                             .interact(Sense::click())
    210                             .on_hover_cursor(CursorIcon::PointingHand)
    211                             .clicked()
    212                             .then_some(MessagesAction::ToggleChrome)
    213                     }
    214                 },
    215                 |ui| {
    216                     self.title(ui, top);
    217                 },
    218                 |ui: &mut egui::Ui| match top {
    219                     Route::ConvoList => {
    220                         let new_msg_icon = app_images::new_message_image().max_height(24.0);
    221                         if ui
    222                             .add(new_msg_icon)
    223                             .on_hover_cursor(CursorIcon::PointingHand)
    224                             .interact(egui::Sense::click())
    225                             .clicked()
    226                         {
    227                             tracing::info!("CLICKED NEW MSG");
    228                             right_action = Some(MessagesAction::Creating);
    229                         }
    230                     }
    231                     Route::CreateConvo => {}
    232                     Route::Conversation => {}
    233                 },
    234             );
    235 
    236         right_action.or(left_action)
    237     }
    238 
    239     fn title(&mut self, ui: &mut egui::Ui, route: &Route) {
    240         match route {
    241             Route::ConvoList => {
    242                 let label = tr!(
    243                     self.i18n,
    244                     "Chats",
    245                     "Title for the list of chat conversations"
    246                 );
    247                 title_label(ui, &label);
    248             }
    249             Route::CreateConvo => {
    250                 let label = tr!(
    251                     self.i18n,
    252                     "New Chat",
    253                     "Title shown when composing a new conversation"
    254                 );
    255                 title_label(ui, &label);
    256             }
    257             Route::Conversation => self.conversation_title_section(ui),
    258         }
    259     }
    260 
    261     fn conversation_title_section(&mut self, ui: &mut egui::Ui) {
    262         conversation_header_impl(
    263             ui,
    264             self.i18n,
    265             self.cache,
    266             self.selected_pubkey,
    267             self.ndb,
    268             self.jobs,
    269             self.img_cache,
    270         );
    271     }
    272 }
    273 
    274 fn back_button(ui: &mut egui::Ui, chev_size: egui::Vec2) -> egui::Response {
    275     let color = ui.style().visuals.noninteractive().fg_stroke.color;
    276     chevron(ui, 2.0, chev_size, egui::Stroke::new(2.0, color))
    277 }
    278 
    279 fn prev<R>(xs: &[R]) -> Option<&R> {
    280     xs.get(xs.len().checked_sub(2)?)
    281 }