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 }