nav.rs (14312B)
1 use crate::{ 2 account_manager::render_accounts_route, 3 app_style::{get_font_size, NotedeckTextStyle}, 4 column::Columns, 5 fonts::NamedFontFamily, 6 notes_holder::NotesHolder, 7 profile::Profile, 8 relay_pool_manager::RelayPoolManager, 9 route::Route, 10 storage::{self, DataPath}, 11 thread::Thread, 12 timeline::{ 13 route::{render_profile_route, render_timeline_route, AfterRouteExecution, TimelineRoute}, 14 Timeline, 15 }, 16 ui::{ 17 self, 18 add_column::render_add_column_routes, 19 anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, 20 note::PostAction, 21 support::SupportView, 22 RelayView, View, 23 }, 24 Damus, 25 }; 26 27 use egui::{pos2, Color32, InnerResponse, Stroke}; 28 use egui_nav::{Nav, NavAction, TitleBarResponse}; 29 use nostrdb::{Ndb, Transaction}; 30 use tracing::{error, info}; 31 32 pub enum RenderNavResponse { 33 ColumnChanged, 34 RemoveColumn(usize), 35 } 36 37 impl RenderNavResponse { 38 pub fn process_nav_response(&self, path: &DataPath, columns: &mut Columns) { 39 match self { 40 RenderNavResponse::ColumnChanged => { 41 storage::save_columns(path, columns.as_serializable_columns()); 42 } 43 44 RenderNavResponse::RemoveColumn(col) => { 45 columns.delete_column(*col); 46 storage::save_columns(path, columns.as_serializable_columns()); 47 } 48 } 49 } 50 } 51 52 pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> Option<RenderNavResponse> { 53 let mut resp: Option<RenderNavResponse> = None; 54 let col_id = app.columns.get_column_id_at_index(col); 55 // TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly 56 let routes = app 57 .columns() 58 .column(col) 59 .router() 60 .routes() 61 .iter() 62 .map(|r| r.get_titled_route(&app.columns, &app.ndb)) 63 .collect(); 64 let nav_response = Nav::new(routes) 65 .navigating(app.columns_mut().column_mut(col).router_mut().navigating) 66 .returning(app.columns_mut().column_mut(col).router_mut().returning) 67 .id_source(egui::Id::new(col_id)) 68 .title(48.0, title_bar) 69 .show_mut(ui, |ui, nav| { 70 let column = app.columns.column_mut(col); 71 match &nav.top().route { 72 Route::Timeline(tlr) => render_timeline_route( 73 &app.ndb, 74 &mut app.columns, 75 &mut app.pool, 76 &mut app.drafts, 77 &mut app.img_cache, 78 &mut app.unknown_ids, 79 &mut app.note_cache, 80 &mut app.threads, 81 &mut app.accounts, 82 *tlr, 83 col, 84 app.textmode, 85 ui, 86 ), 87 Route::Accounts(amr) => { 88 let action = render_accounts_route( 89 ui, 90 &app.ndb, 91 col, 92 &mut app.columns, 93 &mut app.img_cache, 94 &mut app.accounts, 95 &mut app.view_state.login, 96 *amr, 97 ); 98 let txn = Transaction::new(&app.ndb).expect("txn"); 99 action.process_action(&mut app.unknown_ids, &app.ndb, &txn); 100 None 101 } 102 Route::Relays => { 103 let manager = RelayPoolManager::new(app.pool_mut()); 104 RelayView::new(manager).ui(ui); 105 None 106 } 107 Route::ComposeNote => { 108 let kp = app.accounts.selected_or_first_nsec()?; 109 let draft = app.drafts.compose_mut(); 110 111 let txn = nostrdb::Transaction::new(&app.ndb).expect("txn"); 112 let post_response = ui::PostView::new( 113 &app.ndb, 114 draft, 115 crate::draft::DraftSource::Compose, 116 &mut app.img_cache, 117 &mut app.note_cache, 118 kp, 119 ) 120 .ui(&txn, ui); 121 122 if let Some(action) = post_response.action { 123 PostAction::execute(kp, &action, &mut app.pool, draft, |np, seckey| { 124 np.to_note(seckey) 125 }); 126 column.router_mut().go_back(); 127 } 128 129 None 130 } 131 Route::AddColumn(route) => { 132 render_add_column_routes(ui, app, col, route); 133 134 None 135 } 136 137 Route::Profile(pubkey) => render_profile_route( 138 pubkey, 139 &app.ndb, 140 &mut app.columns, 141 &mut app.profiles, 142 &mut app.pool, 143 &mut app.img_cache, 144 &mut app.note_cache, 145 &mut app.threads, 146 col, 147 ui, 148 ), 149 Route::Support => { 150 SupportView::new(&mut app.support).show(ui); 151 None 152 } 153 } 154 }); 155 156 if let Some(after_route_execution) = nav_response.inner { 157 // start returning when we're finished posting 158 match after_route_execution { 159 AfterRouteExecution::Post(resp) => { 160 if let Some(action) = resp.action { 161 match action { 162 PostAction::Post(_) => { 163 app.columns_mut().column_mut(col).router_mut().returning = true; 164 } 165 } 166 } 167 } 168 169 AfterRouteExecution::OpenProfile(pubkey) => { 170 app.columns 171 .column_mut(col) 172 .router_mut() 173 .route_to(Route::Profile(pubkey)); 174 let txn = Transaction::new(&app.ndb).expect("txn"); 175 if let Some(res) = Profile::open( 176 &app.ndb, 177 &mut app.note_cache, 178 &txn, 179 &mut app.pool, 180 &mut app.profiles, 181 pubkey.bytes(), 182 ) { 183 res.process(&app.ndb, &mut app.note_cache, &txn, &mut app.profiles); 184 } 185 } 186 } 187 } 188 189 if let Some(NavAction::Returned) = nav_response.action { 190 let r = app.columns_mut().column_mut(col).router_mut().pop(); 191 let txn = Transaction::new(&app.ndb).expect("txn"); 192 if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { 193 let root_id = { 194 crate::note::root_note_id_from_selected_id( 195 &app.ndb, 196 &mut app.note_cache, 197 &txn, 198 id.bytes(), 199 ) 200 }; 201 Thread::unsubscribe_locally( 202 &txn, 203 &app.ndb, 204 &mut app.note_cache, 205 &mut app.threads, 206 &mut app.pool, 207 root_id, 208 ); 209 } 210 211 if let Some(Route::Profile(pubkey)) = r { 212 Profile::unsubscribe_locally( 213 &txn, 214 &app.ndb, 215 &mut app.note_cache, 216 &mut app.profiles, 217 &mut app.pool, 218 pubkey.bytes(), 219 ); 220 } 221 resp = Some(RenderNavResponse::ColumnChanged) 222 } else if let Some(NavAction::Navigated) = nav_response.action { 223 let cur_router = app.columns_mut().column_mut(col).router_mut(); 224 cur_router.navigating = false; 225 if cur_router.is_replacing() { 226 cur_router.remove_previous_routes(); 227 } 228 resp = Some(RenderNavResponse::ColumnChanged) 229 } 230 231 if let Some(title_response) = nav_response.title_response { 232 match title_response { 233 TitleResponse::RemoveColumn => { 234 let tl = app.columns().find_timeline_for_column_index(col); 235 if let Some(timeline) = tl { 236 unsubscribe_timeline(app.ndb(), timeline); 237 } 238 resp = Some(RenderNavResponse::RemoveColumn(col)) 239 } 240 } 241 } 242 243 resp 244 } 245 246 fn unsubscribe_timeline(ndb: &Ndb, timeline: &Timeline) { 247 if let Some(sub_id) = timeline.subscription { 248 if let Err(e) = ndb.unsubscribe(sub_id) { 249 error!("unsubscribe error: {}", e); 250 } else { 251 info!( 252 "successfully unsubscribed from timeline {} with sub id {}", 253 timeline.id, 254 sub_id.id() 255 ); 256 } 257 } 258 } 259 260 fn title_bar( 261 ui: &mut egui::Ui, 262 allocated_response: egui::Response, 263 title_name: String, 264 back_name: Option<String>, 265 ) -> egui::InnerResponse<TitleBarResponse<TitleResponse>> { 266 let icon_width = 32.0; 267 let padding_external = 16.0; 268 let padding_internal = 8.0; 269 let has_back = back_name.is_some(); 270 271 let (spacing_rect, titlebar_rect) = allocated_response 272 .rect 273 .split_left_right_at_x(allocated_response.rect.left() + padding_external); 274 ui.advance_cursor_after_rect(spacing_rect); 275 276 let (titlebar_resp, maybe_button_resp) = if has_back { 277 let (button_rect, titlebar_rect) = titlebar_rect 278 .split_left_right_at_x(allocated_response.rect.left() + icon_width + padding_external); 279 ( 280 allocated_response.with_new_rect(titlebar_rect), 281 Some(back_button(ui, button_rect)), 282 ) 283 } else { 284 (allocated_response, None) 285 }; 286 287 title( 288 ui, 289 title_name, 290 titlebar_resp.rect, 291 icon_width, 292 if has_back { 293 padding_internal 294 } else { 295 padding_external 296 }, 297 ); 298 299 let delete_button_resp = delete_column_button(ui, titlebar_resp, icon_width, padding_external); 300 let title_response = if delete_button_resp.clicked() { 301 Some(TitleResponse::RemoveColumn) 302 } else { 303 None 304 }; 305 306 let titlebar_resp = TitleBarResponse { 307 title_response, 308 go_back: maybe_button_resp.map_or(false, |r| r.clicked()), 309 }; 310 311 InnerResponse::new(titlebar_resp, delete_button_resp) 312 } 313 314 fn back_button(ui: &mut egui::Ui, button_rect: egui::Rect) -> egui::Response { 315 let horizontal_length = 10.0; 316 let arrow_length = 5.0; 317 318 let helper = AnimationHelper::new_from_rect(ui, "note-compose-button", button_rect); 319 let painter = ui.painter_at(helper.get_animation_rect()); 320 let stroke = Stroke::new(1.5, ui.visuals().text_color()); 321 322 // Horizontal segment 323 let left_horizontal_point = pos2(-horizontal_length / 2., 0.); 324 let right_horizontal_point = pos2(horizontal_length / 2., 0.); 325 let scaled_left_horizontal_point = helper.scale_pos_from_center(left_horizontal_point); 326 let scaled_right_horizontal_point = helper.scale_pos_from_center(right_horizontal_point); 327 328 painter.line_segment( 329 [scaled_left_horizontal_point, scaled_right_horizontal_point], 330 stroke, 331 ); 332 333 // Top Arrow 334 let sqrt_2_over_2 = std::f32::consts::SQRT_2 / 2.; 335 let right_top_arrow_point = helper.scale_pos_from_center(pos2( 336 left_horizontal_point.x + (sqrt_2_over_2 * arrow_length), 337 right_horizontal_point.y + sqrt_2_over_2 * arrow_length, 338 )); 339 340 let scaled_left_arrow_point = scaled_left_horizontal_point; 341 painter.line_segment([scaled_left_arrow_point, right_top_arrow_point], stroke); 342 343 let right_bottom_arrow_point = helper.scale_pos_from_center(pos2( 344 left_horizontal_point.x + (sqrt_2_over_2 * arrow_length), 345 right_horizontal_point.y - sqrt_2_over_2 * arrow_length, 346 )); 347 348 painter.line_segment([scaled_left_arrow_point, right_bottom_arrow_point], stroke); 349 350 helper.take_animation_response() 351 } 352 353 fn delete_column_button( 354 ui: &mut egui::Ui, 355 allocation_response: egui::Response, 356 icon_width: f32, 357 padding: f32, 358 ) -> egui::Response { 359 let img_size = 16.0; 360 let max_size = icon_width * ICON_EXPANSION_MULTIPLE; 361 362 let img_data = egui::include_image!("../assets/icons/column_delete_icon_4x.png"); 363 let img = egui::Image::new(img_data).max_width(img_size); 364 365 let button_rect = { 366 let titlebar_rect = allocation_response.rect; 367 let titlebar_width = titlebar_rect.width(); 368 let titlebar_center = titlebar_rect.center(); 369 let button_center_y = titlebar_center.y; 370 let button_center_x = 371 titlebar_center.x + (titlebar_width / 2.0) - (max_size / 2.0) - padding; 372 egui::Rect::from_center_size( 373 pos2(button_center_x, button_center_y), 374 egui::vec2(max_size, max_size), 375 ) 376 }; 377 378 let helper = AnimationHelper::new_from_rect(ui, "delete-column-button", button_rect); 379 380 let cur_img_size = helper.scale_1d_pos(img_size); 381 382 let animation_rect = helper.get_animation_rect(); 383 let animation_resp = helper.take_animation_response(); 384 if allocation_response.union(animation_resp.clone()).hovered() { 385 img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0)); 386 } 387 388 animation_resp 389 } 390 391 fn title( 392 ui: &mut egui::Ui, 393 title_name: String, 394 titlebar_rect: egui::Rect, 395 icon_width: f32, 396 padding: f32, 397 ) { 398 let painter = ui.painter_at(titlebar_rect); 399 400 let font = egui::FontId::new( 401 get_font_size(ui.ctx(), &NotedeckTextStyle::Body), 402 egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), 403 ); 404 405 let max_title_width = titlebar_rect.width() - icon_width - padding * 2.; 406 let title_galley = 407 ui.fonts(|f| f.layout(title_name, font, ui.visuals().text_color(), max_title_width)); 408 409 let pos = { 410 let titlebar_center = titlebar_rect.center(); 411 let text_height = title_galley.rect.height(); 412 413 let galley_pos_x = titlebar_rect.left() + padding; 414 let galley_pos_y = titlebar_center.y - (text_height / 2.); 415 pos2(galley_pos_x, galley_pos_y) 416 }; 417 418 painter.galley(pos, title_galley, Color32::WHITE); 419 } 420 421 enum TitleResponse { 422 RemoveColumn, 423 }