app.rs (26276B)
1 use crate::{ 2 account_manager::AccountManager, 3 app_creation::setup_cc, 4 app_size_handler::AppSizeHandler, 5 app_style::user_requested_visuals_change, 6 args::Args, 7 column::Columns, 8 draft::Drafts, 9 filter::FilterState, 10 frame_history::FrameHistory, 11 imgcache::ImageCache, 12 nav, 13 notecache::NoteCache, 14 notes_holder::NotesHolderStorage, 15 profile::Profile, 16 storage::{self, DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageType}, 17 subscriptions::{SubKind, Subscriptions}, 18 support::Support, 19 thread::Thread, 20 timeline::{self, Timeline, TimelineKind}, 21 ui::{self, DesktopSidePanel}, 22 unknowns::UnknownIds, 23 view_state::ViewState, 24 Result, 25 }; 26 27 use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool}; 28 use uuid::Uuid; 29 30 use egui::{Context, Frame, Style}; 31 use egui_extras::{Size, StripBuilder}; 32 33 use nostrdb::{Config, Filter, Ndb, Transaction}; 34 35 use std::collections::HashMap; 36 use std::path::Path; 37 use std::time::Duration; 38 use tracing::{error, info, trace, warn}; 39 40 #[derive(Debug, Eq, PartialEq, Clone)] 41 pub enum DamusState { 42 Initializing, 43 Initialized, 44 } 45 46 /// We derive Deserialize/Serialize so we can persist app state on shutdown. 47 pub struct Damus { 48 state: DamusState, 49 pub note_cache: NoteCache, 50 pub pool: RelayPool, 51 52 pub columns: Columns, 53 pub ndb: Ndb, 54 pub view_state: ViewState, 55 pub unknown_ids: UnknownIds, 56 pub drafts: Drafts, 57 pub threads: NotesHolderStorage<Thread>, 58 pub profiles: NotesHolderStorage<Profile>, 59 pub img_cache: ImageCache, 60 pub accounts: AccountManager, 61 pub subscriptions: Subscriptions, 62 pub app_rect_handler: AppSizeHandler, 63 pub support: Support, 64 65 frame_history: crate::frame_history::FrameHistory, 66 67 pub path: DataPath, 68 // TODO: make these bitflags 69 pub debug: bool, 70 pub since_optimize: bool, 71 pub textmode: bool, 72 } 73 74 fn relay_setup(pool: &mut RelayPool, ctx: &egui::Context) { 75 let ctx = ctx.clone(); 76 let wakeup = move || { 77 ctx.request_repaint(); 78 }; 79 if let Err(e) = pool.add_url("ws://localhost:8080".to_string(), wakeup.clone()) { 80 error!("{:?}", e) 81 } 82 if let Err(e) = pool.add_url("wss://relay.damus.io".to_string(), wakeup.clone()) { 83 error!("{:?}", e) 84 } 85 //if let Err(e) = pool.add_url("wss://pyramid.fiatjaf.com".to_string(), wakeup.clone()) { 86 //error!("{:?}", e) 87 //} 88 if let Err(e) = pool.add_url("wss://nos.lol".to_string(), wakeup.clone()) { 89 error!("{:?}", e) 90 } 91 if let Err(e) = pool.add_url("wss://nostr.wine".to_string(), wakeup.clone()) { 92 error!("{:?}", e) 93 } 94 if let Err(e) = pool.add_url("wss://purplepag.es".to_string(), wakeup) { 95 error!("{:?}", e) 96 } 97 } 98 99 fn handle_key_events(input: &egui::InputState, _pixels_per_point: f32, columns: &mut Columns) { 100 for event in &input.raw.events { 101 if let egui::Event::Key { 102 key, pressed: true, .. 103 } = event 104 { 105 match key { 106 egui::Key::J => { 107 columns.select_down(); 108 } 109 egui::Key::K => { 110 columns.select_up(); 111 } 112 egui::Key::H => { 113 columns.select_left(); 114 } 115 egui::Key::L => { 116 columns.select_left(); 117 } 118 _ => {} 119 } 120 } 121 } 122 } 123 124 fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { 125 let ppp = ctx.pixels_per_point(); 126 ctx.input(|i| handle_key_events(i, ppp, &mut damus.columns)); 127 128 let ctx2 = ctx.clone(); 129 let wakeup = move || { 130 ctx2.request_repaint(); 131 }; 132 damus.pool.keepalive_ping(wakeup); 133 134 // NOTE: we don't use the while let loop due to borrow issues 135 #[allow(clippy::while_let_loop)] 136 loop { 137 let ev = if let Some(ev) = damus.pool.try_recv() { 138 ev.into_owned() 139 } else { 140 break; 141 }; 142 143 match (&ev.event).into() { 144 RelayEvent::Opened => { 145 timeline::send_initial_timeline_filters( 146 &damus.ndb, 147 damus.since_optimize, 148 &mut damus.columns, 149 &mut damus.subscriptions, 150 &mut damus.pool, 151 &ev.relay, 152 ); 153 } 154 // TODO: handle reconnects 155 RelayEvent::Closed => warn!("{} connection closed", &ev.relay), 156 RelayEvent::Error(e) => error!("{}: {}", &ev.relay, e), 157 RelayEvent::Other(msg) => trace!("other event {:?}", &msg), 158 RelayEvent::Message(msg) => process_message(damus, &ev.relay, &msg), 159 } 160 } 161 162 let n_timelines = damus.columns.timelines().len(); 163 for timeline_ind in 0..n_timelines { 164 let is_ready = { 165 let timeline = &mut damus.columns.timelines[timeline_ind]; 166 timeline::is_timeline_ready( 167 &damus.ndb, 168 &mut damus.pool, 169 &mut damus.note_cache, 170 timeline, 171 ) 172 }; 173 174 if is_ready { 175 let txn = Transaction::new(&damus.ndb).expect("txn"); 176 177 if let Err(err) = Timeline::poll_notes_into_view( 178 timeline_ind, 179 damus.columns.timelines_mut(), 180 &damus.ndb, 181 &txn, 182 &mut damus.unknown_ids, 183 &mut damus.note_cache, 184 ) { 185 error!("poll_notes_into_view: {err}"); 186 } 187 } else { 188 // TODO: show loading? 189 } 190 } 191 192 if damus.unknown_ids.ready_to_send() { 193 unknown_id_send(damus); 194 } 195 196 Ok(()) 197 } 198 199 fn unknown_id_send(damus: &mut Damus) { 200 let filter = damus.unknown_ids.filter().expect("filter"); 201 info!( 202 "Getting {} unknown ids from relays", 203 damus.unknown_ids.ids().len() 204 ); 205 let msg = ClientMessage::req("unknownids".to_string(), filter); 206 damus.unknown_ids.clear(); 207 damus.pool.send(&msg); 208 } 209 210 #[cfg(feature = "profiling")] 211 fn setup_profiling() { 212 puffin::set_scopes_on(true); // tell puffin to collect data 213 } 214 215 fn update_damus(damus: &mut Damus, ctx: &egui::Context) { 216 match damus.state { 217 DamusState::Initializing => { 218 #[cfg(feature = "profiling")] 219 setup_profiling(); 220 221 damus.state = DamusState::Initialized; 222 // this lets our eose handler know to close unknownids right away 223 damus 224 .subscriptions() 225 .insert("unknownids".to_string(), SubKind::OneShot); 226 if let Err(err) = timeline::setup_initial_nostrdb_subs( 227 &damus.ndb, 228 &mut damus.note_cache, 229 &mut damus.columns, 230 ) { 231 warn!("update_damus init: {err}"); 232 } 233 } 234 235 DamusState::Initialized => (), 236 }; 237 238 if let Err(err) = try_process_event(damus, ctx) { 239 error!("error processing event: {}", err); 240 } 241 242 damus.app_rect_handler.try_save_app_size(ctx); 243 } 244 245 fn process_event(damus: &mut Damus, _subid: &str, event: &str) { 246 #[cfg(feature = "profiling")] 247 puffin::profile_function!(); 248 249 //info!("processing event {}", event); 250 if let Err(_err) = damus.ndb.process_event(event) { 251 error!("error processing event {}", event); 252 } 253 } 254 255 fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> { 256 let sub_kind = if let Some(sub_kind) = damus.subscriptions().get(subid) { 257 sub_kind 258 } else { 259 let n_subids = damus.subscriptions().len(); 260 warn!( 261 "got unknown eose subid {}, {} tracked subscriptions", 262 subid, n_subids 263 ); 264 return Ok(()); 265 }; 266 267 match *sub_kind { 268 SubKind::Timeline(_) => { 269 // eose on timeline? whatevs 270 } 271 SubKind::Initial => { 272 let txn = Transaction::new(&damus.ndb)?; 273 UnknownIds::update( 274 &txn, 275 &mut damus.unknown_ids, 276 &damus.columns, 277 &damus.ndb, 278 &mut damus.note_cache, 279 ); 280 // this is possible if this is the first time 281 if damus.unknown_ids.ready_to_send() { 282 unknown_id_send(damus); 283 } 284 } 285 286 // oneshot subs just close when they're done 287 SubKind::OneShot => { 288 let msg = ClientMessage::close(subid.to_string()); 289 damus.pool.send_to(&msg, relay_url); 290 } 291 292 SubKind::FetchingContactList(timeline_uid) => { 293 let timeline = if let Some(tl) = damus.columns.find_timeline_mut(timeline_uid) { 294 tl 295 } else { 296 error!( 297 "timeline uid:{} not found for FetchingContactList", 298 timeline_uid 299 ); 300 return Ok(()); 301 }; 302 303 let filter_state = timeline.filter.get(relay_url); 304 305 // If this request was fetching a contact list, our filter 306 // state should be "FetchingRemote". We look at the local 307 // subscription for that filter state and get the subscription id 308 let local_sub = if let FilterState::FetchingRemote(unisub) = filter_state { 309 unisub.local 310 } else { 311 // TODO: we could have multiple contact list results, we need 312 // to check to see if this one is newer and use that instead 313 warn!( 314 "Expected timeline to have FetchingRemote state but was {:?}", 315 timeline.filter 316 ); 317 return Ok(()); 318 }; 319 320 info!( 321 "got contact list from {}, updating filter_state to got_remote", 322 relay_url 323 ); 324 325 // We take the subscription id and pass it to the new state of 326 // "GotRemote". This will let future frames know that it can try 327 // to look for the contact list in nostrdb. 328 timeline 329 .filter 330 .set_relay_state(relay_url.to_string(), FilterState::got_remote(local_sub)); 331 } 332 } 333 334 Ok(()) 335 } 336 337 fn process_message(damus: &mut Damus, relay: &str, msg: &RelayMessage) { 338 match msg { 339 RelayMessage::Event(subid, ev) => process_event(damus, subid, ev), 340 RelayMessage::Notice(msg) => warn!("Notice from {}: {}", relay, msg), 341 RelayMessage::OK(cr) => info!("OK {:?}", cr), 342 RelayMessage::Eose(sid) => { 343 if let Err(err) = handle_eose(damus, sid, relay) { 344 error!("error handling eose: {}", err); 345 } 346 } 347 } 348 } 349 350 fn render_damus(damus: &mut Damus, ctx: &Context) { 351 if ui::is_narrow(ctx) { 352 render_damus_mobile(ctx, damus); 353 } else { 354 render_damus_desktop(ctx, damus); 355 } 356 357 ctx.request_repaint_after(Duration::from_secs(1)); 358 359 #[cfg(feature = "profiling")] 360 puffin_egui::profiler_window(ctx); 361 } 362 363 /* 364 fn determine_key_storage_type() -> KeyStorageType { 365 #[cfg(target_os = "macos")] 366 { 367 KeyStorageType::MacOS 368 } 369 370 #[cfg(target_os = "linux")] 371 { 372 KeyStorageType::Linux 373 } 374 375 #[cfg(not(any(target_os = "macos", target_os = "linux")))] 376 { 377 KeyStorageType::None 378 } 379 } 380 */ 381 382 impl Damus { 383 /// Called once before the first frame. 384 pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: Vec<String>) -> Self { 385 // arg parsing 386 let parsed_args = Args::parse(&args); 387 let is_mobile = parsed_args.is_mobile.unwrap_or(ui::is_compiled_as_mobile()); 388 389 setup_cc(ctx, is_mobile, parsed_args.light); 390 391 let data_path = parsed_args 392 .datapath 393 .unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string()); 394 let path = DataPath::new(&data_path); 395 let dbpath_str = parsed_args 396 .dbpath 397 .unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string()); 398 399 let _ = std::fs::create_dir_all(&dbpath_str); 400 401 let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir()); 402 let _ = std::fs::create_dir_all(imgcache_dir.clone()); 403 404 let mut config = Config::new(); 405 config.set_ingester_threads(4); 406 407 let keystore = if parsed_args.use_keystore { 408 let keys_path = path.path(DataPathType::Keys); 409 let selected_key_path = path.path(DataPathType::SelectedKey); 410 KeyStorageType::FileSystem(FileKeyStorage::new( 411 Directory::new(keys_path), 412 Directory::new(selected_key_path), 413 )) 414 } else { 415 KeyStorageType::None 416 }; 417 418 let mut accounts = AccountManager::new(keystore); 419 420 let num_keys = parsed_args.keys.len(); 421 422 let mut unknown_ids = UnknownIds::default(); 423 let ndb = Ndb::new(&dbpath_str, &config).expect("ndb"); 424 425 { 426 let txn = Transaction::new(&ndb).expect("txn"); 427 for key in parsed_args.keys { 428 info!("adding account: {}", key.pubkey); 429 accounts 430 .add_account(key) 431 .process_action(&mut unknown_ids, &ndb, &txn); 432 } 433 } 434 435 if num_keys != 0 { 436 accounts.select_account(0); 437 } 438 439 // setup relays if we have them 440 let pool = if parsed_args.relays.is_empty() { 441 let mut pool = RelayPool::new(); 442 relay_setup(&mut pool, ctx); 443 pool 444 } else { 445 let wakeup = { 446 let ctx = ctx.clone(); 447 move || { 448 ctx.request_repaint(); 449 } 450 }; 451 452 let mut pool = RelayPool::new(); 453 for relay in parsed_args.relays { 454 if let Err(e) = pool.add_url(relay.clone(), wakeup.clone()) { 455 error!("error adding relay {}: {}", relay, e); 456 } 457 } 458 pool 459 }; 460 461 let account = accounts 462 .get_selected_account() 463 .as_ref() 464 .map(|a| a.pubkey.bytes()); 465 466 let mut columns = if parsed_args.columns.is_empty() { 467 if let Some(serializable_columns) = storage::load_columns(&path) { 468 info!("Using columns from disk"); 469 serializable_columns.into_columns(&ndb, account) 470 } else { 471 info!("Could not load columns from disk"); 472 Columns::new() 473 } 474 } else { 475 info!( 476 "Using columns from command line arguments: {:?}", 477 parsed_args.columns 478 ); 479 let mut columns: Columns = Columns::new(); 480 for col in parsed_args.columns { 481 if let Some(timeline) = col.into_timeline(&ndb, account) { 482 columns.add_new_timeline_column(timeline); 483 } 484 } 485 486 columns 487 }; 488 489 let debug = parsed_args.debug; 490 491 if columns.columns().is_empty() { 492 columns.new_column_picker(); 493 } 494 495 let app_rect_handler = AppSizeHandler::new(&path); 496 let support = Support::new(&path); 497 498 Self { 499 pool, 500 debug, 501 unknown_ids, 502 subscriptions: Subscriptions::default(), 503 since_optimize: parsed_args.since_optimize, 504 threads: NotesHolderStorage::default(), 505 profiles: NotesHolderStorage::default(), 506 drafts: Drafts::default(), 507 state: DamusState::Initializing, 508 img_cache: ImageCache::new(imgcache_dir), 509 note_cache: NoteCache::default(), 510 columns, 511 textmode: parsed_args.textmode, 512 ndb, 513 accounts, 514 frame_history: FrameHistory::default(), 515 view_state: ViewState::default(), 516 path, 517 app_rect_handler, 518 support, 519 } 520 } 521 522 pub fn pool_mut(&mut self) -> &mut RelayPool { 523 &mut self.pool 524 } 525 526 pub fn ndb(&self) -> &Ndb { 527 &self.ndb 528 } 529 530 pub fn drafts_mut(&mut self) -> &mut Drafts { 531 &mut self.drafts 532 } 533 534 pub fn img_cache_mut(&mut self) -> &mut ImageCache { 535 &mut self.img_cache 536 } 537 538 pub fn accounts(&self) -> &AccountManager { 539 &self.accounts 540 } 541 542 pub fn accounts_mut(&mut self) -> &mut AccountManager { 543 &mut self.accounts 544 } 545 546 pub fn view_state_mut(&mut self) -> &mut ViewState { 547 &mut self.view_state 548 } 549 550 pub fn columns_mut(&mut self) -> &mut Columns { 551 &mut self.columns 552 } 553 554 pub fn columns(&self) -> &Columns { 555 &self.columns 556 } 557 558 pub fn gen_subid(&self, kind: &SubKind) -> String { 559 if self.debug { 560 format!("{:?}", kind) 561 } else { 562 Uuid::new_v4().to_string() 563 } 564 } 565 566 pub fn mock<P: AsRef<Path>>(data_path: P) -> Self { 567 let mut columns = Columns::new(); 568 let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap(); 569 570 let timeline = Timeline::new(TimelineKind::Universe, FilterState::ready(vec![filter])); 571 572 columns.add_new_timeline_column(timeline); 573 574 let path = DataPath::new(&data_path); 575 let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir()); 576 let _ = std::fs::create_dir_all(imgcache_dir.clone()); 577 let debug = true; 578 579 let app_rect_handler = AppSizeHandler::new(&path); 580 let support = Support::new(&path); 581 582 let mut config = Config::new(); 583 config.set_ingester_threads(2); 584 585 Self { 586 debug, 587 unknown_ids: UnknownIds::default(), 588 subscriptions: Subscriptions::default(), 589 since_optimize: true, 590 threads: NotesHolderStorage::default(), 591 profiles: NotesHolderStorage::default(), 592 drafts: Drafts::default(), 593 state: DamusState::Initializing, 594 pool: RelayPool::new(), 595 img_cache: ImageCache::new(imgcache_dir), 596 note_cache: NoteCache::default(), 597 columns, 598 textmode: false, 599 ndb: Ndb::new( 600 path.path(DataPathType::Db) 601 .to_str() 602 .expect("db path should be ok"), 603 &config, 604 ) 605 .expect("ndb"), 606 accounts: AccountManager::new(KeyStorageType::None), 607 frame_history: FrameHistory::default(), 608 view_state: ViewState::default(), 609 610 path, 611 app_rect_handler, 612 support, 613 } 614 } 615 616 pub fn subscriptions(&mut self) -> &mut HashMap<String, SubKind> { 617 &mut self.subscriptions.subs 618 } 619 620 pub fn note_cache_mut(&mut self) -> &mut NoteCache { 621 &mut self.note_cache 622 } 623 624 pub fn unknown_ids_mut(&mut self) -> &mut UnknownIds { 625 &mut self.unknown_ids 626 } 627 628 pub fn threads(&self) -> &NotesHolderStorage<Thread> { 629 &self.threads 630 } 631 632 pub fn threads_mut(&mut self) -> &mut NotesHolderStorage<Thread> { 633 &mut self.threads 634 } 635 636 pub fn note_cache(&self) -> &NoteCache { 637 &self.note_cache 638 } 639 } 640 641 /* 642 fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { 643 let stroke = ui.style().interact(&response).fg_stroke; 644 let radius = egui::lerp(2.0..=3.0, openness); 645 ui.painter() 646 .circle_filled(response.rect.center(), radius, stroke.color); 647 } 648 */ 649 650 fn top_panel(ctx: &egui::Context) -> egui::TopBottomPanel { 651 let top_margin = egui::Margin { 652 top: 4.0, 653 left: 8.0, 654 right: 8.0, 655 ..Default::default() 656 }; 657 658 let frame = Frame { 659 inner_margin: top_margin, 660 fill: ctx.style().visuals.panel_fill, 661 ..Default::default() 662 }; 663 664 egui::TopBottomPanel::top("top_panel") 665 .frame(frame) 666 .show_separator_line(false) 667 } 668 669 fn render_panel(ctx: &egui::Context, app: &mut Damus) { 670 top_panel(ctx).show(ctx, |ui| { 671 ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { 672 ui.visuals_mut().button_frame = false; 673 674 if let Some(new_visuals) = 675 user_requested_visuals_change(ui::is_oled(), ctx.style().visuals.dark_mode, ui) 676 { 677 ctx.set_visuals(new_visuals) 678 } 679 680 if ui 681 .add(egui::Button::new("A").frame(false)) 682 .on_hover_text("Text mode") 683 .clicked() 684 { 685 app.textmode = !app.textmode; 686 } 687 688 /* 689 if ui 690 .add(egui::Button::new("+").frame(false)) 691 .on_hover_text("Add Timeline") 692 .clicked() 693 { 694 app.n_panels += 1; 695 } 696 697 if app.n_panels != 1 698 && ui 699 .add(egui::Button::new("-").frame(false)) 700 .on_hover_text("Remove Timeline") 701 .clicked() 702 { 703 app.n_panels -= 1; 704 } 705 */ 706 707 //#[cfg(feature = "profiling")] 708 { 709 ui.weak(format!( 710 "FPS: {:.2}, {:10.1}ms", 711 app.frame_history.fps(), 712 app.frame_history.mean_frame_time() * 1e3 713 )); 714 715 /* 716 if !app.timelines().count().is_empty() { 717 ui.weak(format!( 718 "{} notes", 719 &app.timelines() 720 .notes(ViewFilter::NotesAndReplies) 721 .len() 722 )); 723 } 724 */ 725 } 726 }); 727 }); 728 } 729 730 fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { 731 //render_panel(ctx, app, 0); 732 733 #[cfg(feature = "profiling")] 734 puffin::profile_function!(); 735 736 //let routes = app.timelines[0].routes.clone(); 737 738 main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { 739 if !app.columns.columns().is_empty() { 740 if let Some(r) = nav::render_nav(0, app, ui) { 741 r.process_nav_response(&app.path, &mut app.columns) 742 } 743 } 744 }); 745 } 746 747 fn main_panel(style: &Style, narrow: bool) -> egui::CentralPanel { 748 let inner_margin = egui::Margin { 749 top: if narrow { 50.0 } else { 0.0 }, 750 left: 0.0, 751 right: 0.0, 752 bottom: 0.0, 753 }; 754 egui::CentralPanel::default().frame(Frame { 755 inner_margin, 756 fill: style.visuals.panel_fill, 757 ..Default::default() 758 }) 759 } 760 761 fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { 762 render_panel(ctx, app); 763 #[cfg(feature = "profiling")] 764 puffin::profile_function!(); 765 766 let screen_size = ctx.screen_rect().width(); 767 let calc_panel_width = (screen_size / app.columns.num_columns() as f32) - 30.0; 768 let min_width = 320.0; 769 let need_scroll = calc_panel_width < min_width; 770 let panel_sizes = if need_scroll { 771 Size::exact(min_width) 772 } else { 773 Size::remainder() 774 }; 775 776 main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { 777 ui.spacing_mut().item_spacing.x = 0.0; 778 if need_scroll { 779 egui::ScrollArea::horizontal().show(ui, |ui| { 780 timelines_view(ui, panel_sizes, app); 781 }); 782 } else { 783 timelines_view(ui, panel_sizes, app); 784 } 785 }); 786 } 787 788 fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { 789 StripBuilder::new(ui) 790 .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH)) 791 .sizes(sizes, app.columns.num_columns()) 792 .clip(true) 793 .horizontal(|mut strip| { 794 strip.cell(|ui| { 795 let rect = ui.available_rect_before_wrap(); 796 let side_panel = DesktopSidePanel::new( 797 &app.ndb, 798 &mut app.img_cache, 799 app.accounts.get_selected_account(), 800 ) 801 .show(ui); 802 803 if side_panel.response.clicked() { 804 DesktopSidePanel::perform_action( 805 &mut app.columns, 806 &mut app.support, 807 side_panel.action, 808 ); 809 } 810 811 // vertical sidebar line 812 ui.painter().vline( 813 rect.right(), 814 rect.y_range(), 815 ui.visuals().widgets.noninteractive.bg_stroke, 816 ); 817 }); 818 819 let mut nav_resp: Option<nav::RenderNavResponse> = None; 820 for col_index in 0..app.columns.num_columns() { 821 strip.cell(|ui| { 822 let rect = ui.available_rect_before_wrap(); 823 if let Some(r) = nav::render_nav(col_index, app, ui) { 824 nav_resp = Some(r); 825 } 826 827 // vertical line 828 ui.painter().vline( 829 rect.right(), 830 rect.y_range(), 831 ui.visuals().widgets.noninteractive.bg_stroke, 832 ); 833 }); 834 835 //strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind)); 836 } 837 838 if let Some(r) = nav_resp { 839 r.process_nav_response(&app.path, &mut app.columns); 840 } 841 }); 842 } 843 844 impl eframe::App for Damus { 845 /// Called by the frame work to save state before shutdown. 846 fn save(&mut self, _storage: &mut dyn eframe::Storage) { 847 //eframe::set_value(storage, eframe::APP_KEY, self); 848 } 849 850 /// Called each time the UI needs repainting, which may be many times per second. 851 /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. 852 fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { 853 self.frame_history 854 .on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); 855 856 #[cfg(feature = "profiling")] 857 puffin::GlobalProfiler::lock().new_frame(); 858 update_damus(self, ctx); 859 render_damus(self, ctx); 860 } 861 }