notedeck

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

nav.rs (8701B)


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