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 }