chrome.rs (36370B)
1 // Entry point for wasm 2 //#[cfg(target_arch = "wasm32")] 3 //use wasm_bindgen::prelude::*; 4 use crate::app::NotedeckApp; 5 use eframe::CreationContext; 6 use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget}; 7 use egui_extras::{Size, StripBuilder}; 8 use nostrdb::{ProfileRecord, Transaction}; 9 use notedeck::Error; 10 use notedeck::{ 11 tr, App, AppAction, AppContext, Localization, Notedeck, NotedeckOptions, NotedeckTextStyle, 12 UserAccount, WalletType, 13 }; 14 use notedeck_columns::{ 15 column::SelectionResult, 16 timeline::{kind::ListKind, TimelineKind}, 17 Damus, 18 }; 19 use notedeck_dave::{Dave, DaveAvatar}; 20 use notedeck_notebook::Notebook; 21 use notedeck_ui::{app_images, AnimationHelper, ProfilePic}; 22 23 static ICON_WIDTH: f32 = 40.0; 24 pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; 25 26 pub struct Chrome { 27 active: i32, 28 open: bool, 29 tab_selected: i32, 30 apps: Vec<NotedeckApp>, 31 32 #[cfg(feature = "memory")] 33 show_memory_debug: bool, 34 } 35 36 impl Default for Chrome { 37 fn default() -> Self { 38 Self { 39 active: 0, 40 tab_selected: 0, 41 // sidemenu is not open by default on mobile/narrow uis 42 open: !notedeck::ui::is_compiled_as_mobile(), 43 apps: vec![], 44 45 #[cfg(feature = "memory")] 46 show_memory_debug: false, 47 } 48 } 49 } 50 51 /// When you click the toolbar button, these actions 52 /// are returned 53 #[derive(Debug, Eq, PartialEq)] 54 pub enum ToolbarAction { 55 Notifications, 56 Dave, 57 Home, 58 } 59 60 pub enum ChromePanelAction { 61 Support, 62 Settings, 63 Account, 64 Wallet, 65 Toolbar(ToolbarAction), 66 SaveTheme(ThemePreference), 67 Profile(notedeck::enostr::Pubkey), 68 } 69 70 impl ChromePanelAction { 71 fn columns_switch(ctx: &mut AppContext, chrome: &mut Chrome, kind: &TimelineKind) { 72 chrome.switch_to_columns(); 73 74 let Some(columns_app) = chrome.get_columns_app() else { 75 return; 76 }; 77 78 if let Some(active_columns) = columns_app 79 .decks_cache 80 .active_columns_mut(ctx.i18n, ctx.accounts) 81 { 82 match active_columns.select_by_kind(kind) { 83 SelectionResult::NewSelection(_index) => { 84 // great! no need to go to top yet 85 } 86 87 SelectionResult::AlreadySelected(_n) => { 88 // we already selected this, so scroll to top 89 columns_app.scroll_to_top(); 90 } 91 92 SelectionResult::Failed => { 93 // oh no, something went wrong 94 // TODO(jb55): handle tab selection failure 95 } 96 } 97 } 98 } 99 100 fn columns_navigate(ctx: &mut AppContext, chrome: &mut Chrome, route: notedeck_columns::Route) { 101 chrome.switch_to_columns(); 102 103 if let Some(c) = chrome.get_columns_app().and_then(|columns| { 104 columns 105 .decks_cache 106 .selected_column_mut(ctx.i18n, ctx.accounts) 107 }) { 108 if c.router().routes().iter().any(|r| r == &route) { 109 // return if we are already routing to accounts 110 c.router_mut().go_back(); 111 } else { 112 c.router_mut().route_to(route); 113 //c..route_to(Route::relays()); 114 } 115 }; 116 } 117 118 fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) { 119 match self { 120 Self::SaveTheme(theme) => { 121 ui.ctx().set_theme(*theme); 122 ctx.settings.set_theme(*theme); 123 } 124 125 Self::Toolbar(toolbar_action) => match toolbar_action { 126 ToolbarAction::Dave => chrome.switch_to_dave(), 127 128 ToolbarAction::Home => { 129 Self::columns_switch( 130 ctx, 131 chrome, 132 &TimelineKind::List(ListKind::Contact( 133 ctx.accounts.get_selected_account().key.pubkey, 134 )), 135 ); 136 } 137 138 ToolbarAction::Notifications => { 139 Self::columns_switch( 140 ctx, 141 chrome, 142 &TimelineKind::Notifications( 143 ctx.accounts.get_selected_account().key.pubkey, 144 ), 145 ); 146 } 147 }, 148 149 Self::Support => { 150 Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Support); 151 } 152 153 Self::Account => { 154 Self::columns_navigate(ctx, chrome, notedeck_columns::Route::accounts()); 155 } 156 157 Self::Settings => { 158 Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Settings); 159 } 160 161 Self::Wallet => { 162 Self::columns_navigate( 163 ctx, 164 chrome, 165 notedeck_columns::Route::Wallet(WalletType::Auto), 166 ); 167 } 168 Self::Profile(pk) => { 169 columns_route_to_profile(pk, chrome, ctx, ui); 170 } 171 } 172 } 173 } 174 175 /// Some people have been running notedeck in debug, let's catch that! 176 fn stop_debug_mode(options: NotedeckOptions) { 177 if !options.contains(NotedeckOptions::Tests) 178 && cfg!(debug_assertions) 179 && !options.contains(NotedeckOptions::Debug) 180 { 181 println!("--- WELCOME TO DAMUS NOTEDECK! ---"); 182 println!( 183 "It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want." 184 ); 185 println!("If you are a developer, run `cargo run -- --debug` to skip this message."); 186 println!("For everyone else, try again with `cargo run --release`. Enjoy!"); 187 println!("---------------------------------"); 188 panic!(); 189 } 190 } 191 192 impl Chrome { 193 /// Create a new chrome with the default app setup 194 pub fn new_with_apps( 195 cc: &CreationContext, 196 app_args: &[String], 197 notedeck: &mut Notedeck, 198 ) -> Result<Self, Error> { 199 stop_debug_mode(notedeck.options()); 200 201 let context = &mut notedeck.app_context(); 202 let dave = Dave::new(cc.wgpu_render_state.as_ref()); 203 let columns = Damus::new(context, app_args); 204 let mut chrome = Chrome::default(); 205 206 notedeck.check_args(columns.unrecognized_args())?; 207 208 chrome.add_app(NotedeckApp::Columns(Box::new(columns))); 209 chrome.add_app(NotedeckApp::Dave(Box::new(dave))); 210 211 if notedeck.has_option(NotedeckOptions::FeatureNotebook) { 212 chrome.add_app(NotedeckApp::Notebook(Box::default())); 213 } 214 215 chrome.set_active(0); 216 217 Ok(chrome) 218 } 219 220 pub fn toggle(&mut self) { 221 self.open = !self.open; 222 } 223 224 pub fn add_app(&mut self, app: NotedeckApp) { 225 self.apps.push(app); 226 } 227 228 fn get_columns_app(&mut self) -> Option<&mut Damus> { 229 for app in &mut self.apps { 230 if let NotedeckApp::Columns(cols) = app { 231 return Some(cols); 232 } 233 } 234 235 None 236 } 237 238 fn get_dave(&mut self) -> Option<&mut Dave> { 239 for app in &mut self.apps { 240 if let NotedeckApp::Dave(dave) = app { 241 return Some(dave); 242 } 243 } 244 245 None 246 } 247 248 fn get_notebook(&mut self) -> Option<&mut Notebook> { 249 for app in &mut self.apps { 250 if let NotedeckApp::Notebook(notebook) = app { 251 return Some(notebook); 252 } 253 } 254 255 None 256 } 257 258 fn switch_to_dave(&mut self) { 259 for (i, app) in self.apps.iter().enumerate() { 260 if let NotedeckApp::Dave(_) = app { 261 self.active = i as i32; 262 } 263 } 264 } 265 266 fn switch_to_notebook(&mut self) { 267 for (i, app) in self.apps.iter().enumerate() { 268 if let NotedeckApp::Notebook(_) = app { 269 self.active = i as i32; 270 } 271 } 272 } 273 274 fn switch_to_columns(&mut self) { 275 for (i, app) in self.apps.iter().enumerate() { 276 if let NotedeckApp::Columns(_) = app { 277 self.active = i as i32; 278 } 279 } 280 } 281 282 pub fn set_active(&mut self, app: i32) { 283 self.active = app; 284 } 285 286 /// The chrome side panel 287 fn panel( 288 &mut self, 289 app_ctx: &mut AppContext, 290 builder: StripBuilder, 291 amt_open: f32, 292 ) -> Option<ChromePanelAction> { 293 let mut got_action: Option<ChromePanelAction> = None; 294 295 builder 296 .size(Size::exact(amt_open)) // collapsible sidebar 297 .size(Size::remainder()) // the main app contents 298 .clip(true) 299 .horizontal(|mut hstrip| { 300 hstrip.cell(|ui| { 301 let rect = ui.available_rect_before_wrap(); 302 if !ui.visuals().dark_mode { 303 let rect = ui.available_rect_before_wrap(); 304 ui.painter().rect( 305 rect, 306 0, 307 notedeck_ui::colors::ALMOST_WHITE, 308 egui::Stroke::new(0.0, Color32::TRANSPARENT), 309 egui::StrokeKind::Inside, 310 ); 311 } 312 313 StripBuilder::new(ui) 314 .size(Size::remainder()) 315 .size(Size::remainder()) 316 .vertical(|mut vstrip| { 317 vstrip.cell(|ui| { 318 _ = ui.vertical_centered(|ui| { 319 self.topdown_sidebar(ui, app_ctx.i18n); 320 }) 321 }); 322 vstrip.cell(|ui| { 323 ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| { 324 if let Some(action) = bottomup_sidebar(self, app_ctx, ui) { 325 got_action = Some(action); 326 } 327 }); 328 }); 329 }); 330 331 // vertical sidebar line 332 ui.painter().vline( 333 rect.right(), 334 rect.y_range(), 335 ui.visuals().widgets.noninteractive.bg_stroke, 336 ); 337 }); 338 339 hstrip.cell(|ui| { 340 /* 341 let rect = ui.available_rect_before_wrap(); 342 ui.painter().rect( 343 rect, 344 0, 345 egui::Color32::RED, 346 egui::Stroke::new(1.0, egui::Color32::BLUE), 347 egui::StrokeKind::Inside, 348 ); 349 */ 350 351 if let Some(action) = self.apps[self.active as usize].update(app_ctx, ui) { 352 chrome_handle_app_action(self, app_ctx, action, ui); 353 } 354 }); 355 }); 356 357 got_action 358 } 359 360 /// How far is the chrome panel expanded? 361 fn amount_open(&self, ui: &mut egui::Ui) -> f32 { 362 let open_id = egui::Id::new("chrome_open"); 363 let side_panel_width: f32 = 74.0; 364 ui.ctx().animate_bool(open_id, self.open) * side_panel_width 365 } 366 367 fn toolbar_height() -> f32 { 368 48.0 369 } 370 371 /// On narrow layouts, we have a toolbar 372 fn toolbar_chrome( 373 &mut self, 374 ctx: &mut AppContext, 375 ui: &mut egui::Ui, 376 ) -> Option<ChromePanelAction> { 377 let mut got_action: Option<ChromePanelAction> = None; 378 let amt_open = self.amount_open(ui); 379 380 StripBuilder::new(ui) 381 .size(Size::remainder()) // top cell 382 .size(Size::exact(Self::toolbar_height())) // bottom cell 383 .vertical(|mut strip| { 384 strip.strip(|builder| { 385 // the chrome panel is nested above the toolbar 386 got_action = self.panel(ctx, builder, amt_open); 387 }); 388 389 strip.cell(|ui| { 390 let pk = ctx.accounts.get_selected_account().key.pubkey; 391 392 let unseen_notification = 393 unseen_notification(self.get_columns_app(), ctx.ndb, pk); 394 395 if let Some(action) = self.toolbar(ui, unseen_notification) { 396 got_action = Some(ChromePanelAction::Toolbar(action)) 397 } 398 }); 399 }); 400 401 got_action 402 } 403 404 fn toolbar(&mut self, ui: &mut egui::Ui, unseen_notification: bool) -> Option<ToolbarAction> { 405 use egui_tabs::{TabColor, Tabs}; 406 407 let rect = ui.available_rect_before_wrap(); 408 ui.painter().hline( 409 rect.x_range(), 410 rect.top(), 411 ui.visuals().widgets.noninteractive.bg_stroke, 412 ); 413 414 if !ui.visuals().dark_mode { 415 ui.painter().rect( 416 rect, 417 0, 418 notedeck_ui::colors::ALMOST_WHITE, 419 egui::Stroke::new(0.0, Color32::TRANSPARENT), 420 egui::StrokeKind::Inside, 421 ); 422 } 423 424 let rs = Tabs::new(3) 425 .selected(self.tab_selected) 426 .hover_bg(TabColor::none()) 427 .selected_fg(TabColor::none()) 428 .selected_bg(TabColor::none()) 429 .height(Self::toolbar_height()) 430 .layout(Layout::centered_and_justified(egui::Direction::TopDown)) 431 .show(ui, |ui, state| { 432 let index = state.index(); 433 434 let mut action: Option<ToolbarAction> = None; 435 436 let btn_size: f32 = 20.0; 437 if index == 0 { 438 if home_button(ui, btn_size).clicked() { 439 action = Some(ToolbarAction::Home); 440 } 441 } else if index == 1 { 442 if let Some(dave) = self.get_dave() { 443 let rect = dave_toolbar_rect(ui, btn_size * 2.0); 444 if dave_button(dave.avatar_mut(), ui, rect).clicked() { 445 action = Some(ToolbarAction::Dave); 446 } 447 } 448 } else if index == 2 449 && notifications_button(ui, btn_size, unseen_notification).clicked() 450 { 451 action = Some(ToolbarAction::Notifications); 452 } 453 454 action 455 }) 456 .inner(); 457 458 for maybe_r in rs { 459 if maybe_r.inner.is_some() { 460 return maybe_r.inner; 461 } 462 } 463 464 None 465 } 466 467 /// Show the side menu or bar, depending on if we're on a narrow 468 /// or wide screen. 469 /// 470 /// The side menu should hover over the screen, while the side bar 471 /// is collapsible but persistent on the screen. 472 fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<ChromePanelAction> { 473 ui.spacing_mut().item_spacing.x = 0.0; 474 475 if notedeck::ui::is_narrow(ui.ctx()) { 476 self.toolbar_chrome(ctx, ui) 477 } else { 478 let amt_open = self.amount_open(ui); 479 self.panel(ctx, StripBuilder::new(ui), amt_open) 480 } 481 } 482 483 fn topdown_sidebar(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) { 484 // macos needs a bit of space to make room for window 485 // minimize/close buttons 486 if cfg!(target_os = "macos") { 487 ui.add_space(30.0); 488 } else { 489 // we still want *some* padding so that it aligns with the + button regardless 490 ui.add_space(notedeck_ui::constants::FRAME_MARGIN.into()); 491 } 492 493 if ui.add(expand_side_panel_button()).clicked() { 494 //self.active = (self.active + 1) % (self.apps.len() as i32); 495 self.open = !self.open; 496 } 497 498 ui.add_space(4.0); 499 ui.add(milestone_name(i18n)); 500 ui.add_space(16.0); 501 //let dark_mode = ui.ctx().style().visuals.dark_mode; 502 if columns_button(ui) 503 .on_hover_cursor(egui::CursorIcon::PointingHand) 504 .clicked() 505 { 506 self.active = 0; 507 } 508 ui.add_space(32.0); 509 510 if let Some(dave) = self.get_dave() { 511 let rect = dave_sidebar_rect(ui); 512 let dave_resp = dave_button(dave.avatar_mut(), ui, rect) 513 .on_hover_cursor(egui::CursorIcon::PointingHand); 514 if dave_resp.clicked() { 515 self.switch_to_dave(); 516 } 517 } 518 //ui.add_space(32.0); 519 520 if let Some(_notebook) = self.get_notebook() { 521 if notebook_button(ui) 522 .on_hover_cursor(egui::CursorIcon::PointingHand) 523 .clicked() 524 { 525 self.switch_to_notebook(); 526 } 527 } 528 } 529 } 530 531 fn unseen_notification( 532 columns: Option<&mut Damus>, 533 ndb: &nostrdb::Ndb, 534 current_pk: notedeck::enostr::Pubkey, 535 ) -> bool { 536 let Some(columns) = columns else { 537 return false; 538 }; 539 540 let Some(tl) = columns 541 .timeline_cache 542 .get_mut(&TimelineKind::Notifications(current_pk)) 543 else { 544 return false; 545 }; 546 547 let freshness = &mut tl.current_view_mut().freshness; 548 freshness.update(|timestamp_last_viewed| { 549 let filter = notedeck_columns::timeline::kind::notifications_filter(¤t_pk) 550 .since_mut(timestamp_last_viewed); 551 let txn = Transaction::new(ndb).expect("txn"); 552 553 let Some(res) = ndb.query(&txn, &[filter], 1).ok() else { 554 return false; 555 }; 556 557 !res.is_empty() 558 }); 559 560 freshness.has_unseen() 561 } 562 563 impl notedeck::App for Chrome { 564 fn update(&mut self, ctx: &mut notedeck::AppContext, ui: &mut egui::Ui) -> Option<AppAction> { 565 if let Some(action) = self.show(ctx, ui) { 566 action.process(ctx, self, ui); 567 } 568 // TODO: unify this constant with the columns side panel width. ui crate? 569 None 570 } 571 } 572 573 fn milestone_name<'a>(i18n: &'a mut Localization) -> impl Widget + 'a { 574 |ui: &mut egui::Ui| -> egui::Response { 575 ui.vertical_centered(|ui| { 576 let font = egui::FontId::new( 577 notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Tiny), 578 egui::FontFamily::Name(notedeck::fonts::NamedFontFamily::Bold.as_str().into()), 579 ); 580 ui.add( 581 Label::new( 582 RichText::new(tr!(i18n, "BETA", "Beta version label")) 583 .color(ui.style().visuals.noninteractive().fg_stroke.color) 584 .font(font), 585 ) 586 .selectable(false), 587 ) 588 .on_hover_text(tr!( 589 i18n, 590 "Notedeck is a beta product. Expect bugs and contact us when you run into issues.", 591 "Beta product warning message" 592 )) 593 .on_hover_cursor(egui::CursorIcon::Help) 594 }) 595 .inner 596 } 597 } 598 599 fn expand_side_panel_button() -> impl Widget { 600 |ui: &mut egui::Ui| -> egui::Response { 601 let img_size = 40.0; 602 let img = app_images::damus_image() 603 .max_width(img_size) 604 .sense(egui::Sense::click()); 605 606 ui.add(img) 607 } 608 } 609 610 fn expanding_button( 611 name: &'static str, 612 img_size: f32, 613 light_img: egui::Image, 614 dark_img: egui::Image, 615 ui: &mut egui::Ui, 616 unseen_indicator: bool, 617 ) -> egui::Response { 618 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 619 let img = if ui.visuals().dark_mode { 620 dark_img 621 } else { 622 light_img 623 }; 624 625 let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size)); 626 627 let cur_img_size = helper.scale_1d_pos(img_size); 628 629 let paint_rect = helper 630 .get_animation_rect() 631 .shrink((max_size - cur_img_size) / 2.0); 632 img.paint_at(ui, paint_rect); 633 634 if unseen_indicator { 635 paint_unseen_indicator(ui, paint_rect, helper.scale_1d_pos(3.0)); 636 } 637 638 helper.take_animation_response() 639 } 640 641 fn paint_unseen_indicator(ui: &mut egui::Ui, rect: egui::Rect, radius: f32) { 642 let center = rect.center(); 643 let top_right = rect.right_top(); 644 let distance = center.distance(top_right); 645 let midpoint = { 646 let mut cur = center; 647 cur.x += distance / 2.0; 648 cur.y -= distance / 2.0; 649 cur 650 }; 651 652 let painter = ui.painter_at(rect); 653 painter.circle_filled(midpoint, radius, notedeck_ui::colors::PINK); 654 } 655 656 fn support_button(ui: &mut egui::Ui) -> egui::Response { 657 expanding_button( 658 "help-button", 659 16.0, 660 app_images::help_light_image(), 661 app_images::help_dark_image(), 662 ui, 663 false, 664 ) 665 } 666 667 fn settings_button(ui: &mut egui::Ui) -> egui::Response { 668 expanding_button( 669 "settings-button", 670 32.0, 671 app_images::settings_light_image(), 672 app_images::settings_dark_image(), 673 ui, 674 false, 675 ) 676 } 677 678 fn notifications_button(ui: &mut egui::Ui, size: f32, unseen_indicator: bool) -> egui::Response { 679 expanding_button( 680 "notifications-button", 681 size, 682 app_images::notifications_light_image(), 683 app_images::notifications_dark_image(), 684 ui, 685 unseen_indicator, 686 ) 687 } 688 689 fn home_button(ui: &mut egui::Ui, size: f32) -> egui::Response { 690 expanding_button( 691 "home-button", 692 size, 693 app_images::home_light_image(), 694 app_images::home_dark_image(), 695 ui, 696 false, 697 ) 698 } 699 700 fn columns_button(ui: &mut egui::Ui) -> egui::Response { 701 expanding_button( 702 "columns-button", 703 40.0, 704 app_images::columns_image(), 705 app_images::columns_image(), 706 ui, 707 false, 708 ) 709 } 710 711 fn accounts_button(ui: &mut egui::Ui) -> egui::Response { 712 expanding_button( 713 "accounts-button", 714 24.0, 715 app_images::accounts_image().tint(ui.visuals().text_color()), 716 app_images::accounts_image(), 717 ui, 718 false, 719 ) 720 } 721 722 fn notebook_button(ui: &mut egui::Ui) -> egui::Response { 723 expanding_button( 724 "notebook-button", 725 40.0, 726 app_images::algo_image(), 727 app_images::algo_image(), 728 ui, 729 false, 730 ) 731 } 732 733 fn dave_sidebar_rect(ui: &mut egui::Ui) -> Rect { 734 let size = vec2(60.0, 60.0); 735 let available = ui.available_rect_before_wrap(); 736 let center_x = available.center().x; 737 let center_y = available.top(); 738 egui::Rect::from_center_size(egui::pos2(center_x, center_y), size) 739 } 740 741 fn dave_toolbar_rect(ui: &mut egui::Ui, size: f32) -> Rect { 742 let size = vec2(size, size); 743 let available = ui.available_rect_before_wrap(); 744 let center_x = available.center().x; 745 let center_y = available.center().y; 746 egui::Rect::from_center_size(egui::pos2(center_x, center_y), size) 747 } 748 749 fn dave_button(avatar: Option<&mut DaveAvatar>, ui: &mut egui::Ui, rect: Rect) -> egui::Response { 750 if let Some(avatar) = avatar { 751 avatar.render(rect, ui) 752 } else { 753 // plain icon if wgpu device not available?? 754 ui.label("fixme") 755 } 756 } 757 758 pub fn get_profile_url_owned(profile: Option<ProfileRecord<'_>>) -> &str { 759 if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { 760 url 761 } else { 762 notedeck::profile::no_pfp_url() 763 } 764 } 765 766 pub fn get_account_url<'a>( 767 txn: &'a nostrdb::Transaction, 768 ndb: &nostrdb::Ndb, 769 account: &UserAccount, 770 ) -> &'a str { 771 if let Ok(profile) = ndb.get_profile_by_pubkey(txn, account.key.pubkey.bytes()) { 772 get_profile_url_owned(Some(profile)) 773 } else { 774 get_profile_url_owned(None) 775 } 776 } 777 778 fn wallet_button() -> impl Widget { 779 |ui: &mut egui::Ui| -> egui::Response { 780 let img_size = 24.0; 781 782 let max_size = img_size * ICON_EXPANSION_MULTIPLE; 783 784 let img = if !ui.visuals().dark_mode { 785 app_images::wallet_light_image() 786 } else { 787 app_images::wallet_dark_image() 788 } 789 .max_width(img_size); 790 791 let helper = AnimationHelper::new(ui, "wallet-icon", vec2(max_size, max_size)); 792 793 let cur_img_size = helper.scale_1d_pos(img_size); 794 img.paint_at( 795 ui, 796 helper 797 .get_animation_rect() 798 .shrink((max_size - cur_img_size) / 2.0), 799 ); 800 801 helper.take_animation_response() 802 } 803 } 804 805 fn chrome_handle_app_action( 806 chrome: &mut Chrome, 807 ctx: &mut AppContext, 808 action: AppAction, 809 ui: &mut egui::Ui, 810 ) { 811 match action { 812 AppAction::ToggleChrome => { 813 chrome.toggle(); 814 } 815 816 AppAction::Note(note_action) => { 817 chrome.switch_to_columns(); 818 let Some(columns) = chrome.get_columns_app() else { 819 return; 820 }; 821 822 let txn = Transaction::new(ctx.ndb).unwrap(); 823 824 let cols = columns 825 .decks_cache 826 .active_columns_mut(ctx.i18n, ctx.accounts) 827 .unwrap(); 828 let m_action = notedeck_columns::actionbar::execute_and_process_note_action( 829 note_action, 830 ctx.ndb, 831 cols, 832 0, 833 &mut columns.timeline_cache, 834 &mut columns.threads, 835 ctx.note_cache, 836 ctx.pool, 837 &txn, 838 ctx.unknown_ids, 839 ctx.accounts, 840 ctx.global_wallet, 841 ctx.zaps, 842 ctx.img_cache, 843 &mut columns.view_state, 844 ui, 845 ); 846 847 if let Some(action) = m_action { 848 let col = cols.selected_mut(); 849 850 action.process(&mut col.router, &mut col.sheet_router); 851 } 852 } 853 } 854 } 855 856 fn columns_route_to_profile( 857 pk: ¬edeck::enostr::Pubkey, 858 chrome: &mut Chrome, 859 ctx: &mut AppContext, 860 ui: &mut egui::Ui, 861 ) { 862 chrome.switch_to_columns(); 863 let Some(columns) = chrome.get_columns_app() else { 864 return; 865 }; 866 867 let cols = columns 868 .decks_cache 869 .active_columns_mut(ctx.i18n, ctx.accounts) 870 .unwrap(); 871 872 let router = cols.get_selected_router(); 873 if router.routes().iter().any(|r| { 874 matches!( 875 r, 876 notedeck_columns::Route::Timeline(TimelineKind::Profile(_)) 877 ) 878 }) { 879 router.go_back(); 880 return; 881 } 882 883 let txn = Transaction::new(ctx.ndb).unwrap(); 884 let m_action = notedeck_columns::actionbar::execute_and_process_note_action( 885 notedeck::NoteAction::Profile(*pk), 886 ctx.ndb, 887 cols, 888 0, 889 &mut columns.timeline_cache, 890 &mut columns.threads, 891 ctx.note_cache, 892 ctx.pool, 893 &txn, 894 ctx.unknown_ids, 895 ctx.accounts, 896 ctx.global_wallet, 897 ctx.zaps, 898 ctx.img_cache, 899 &mut columns.view_state, 900 ui, 901 ); 902 903 if let Some(action) = m_action { 904 let col = cols.selected_mut(); 905 906 action.process(&mut col.router, &mut col.sheet_router); 907 } 908 } 909 910 fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response { 911 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 912 let helper = AnimationHelper::new(ui, "pfp-button", egui::vec2(max_size, max_size)); 913 914 let min_pfp_size = ICON_WIDTH; 915 let cur_pfp_size = helper.scale_1d_pos(min_pfp_size); 916 917 let txn = Transaction::new(ctx.ndb).expect("should be able to create txn"); 918 let profile_url = get_account_url(&txn, ctx.ndb, ctx.accounts.get_selected_account()); 919 920 let mut widget = ProfilePic::new(ctx.img_cache, profile_url).size(cur_pfp_size); 921 922 ui.put(helper.get_animation_rect(), &mut widget); 923 924 helper.take_animation_response() 925 926 // let selected = ctx.accounts.cache.selected(); 927 928 // pfp_resp.context_menu(|ui| { 929 // for (pk, account) in &ctx.accounts.cache { 930 // let profile = ctx.ndb.get_profile_by_pubkey(&txn, pk).ok(); 931 // let is_selected = *pk == selected.key.pubkey; 932 // let has_nsec = account.key.secret_key.is_some(); 933 934 // let profile_peview_view = { 935 // let max_size = egui::vec2(ui.available_width(), 77.0); 936 // let resp = ui.allocate_response(max_size, egui::Sense::click()); 937 // ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| { 938 // ui.add( 939 // &mut ProfilePic::new(ctx.img_cache, get_profile_url(profile.as_ref())) 940 // .size(24.0), 941 // ) 942 // }) 943 // }; 944 945 // // if let Some(op) = profile_peview_view { 946 // // return_op = Some(match op { 947 // // ProfilePreviewAction::SwitchTo => AccountsViewResponse::SelectAccount(*pk), 948 // // ProfilePreviewAction::RemoveAccount => AccountsViewResponse::RemoveAccount(*pk), 949 // // }); 950 // // } 951 // } 952 // // if ui.menu_image_button(image, add_contents).clicked() { 953 // // // ui.ctx().copy_text(url.to_owned()); 954 // // ui.close_menu(); 955 // // } 956 // }); 957 } 958 959 /// The section of the chrome sidebar that starts at the 960 /// bottom and goes up 961 fn bottomup_sidebar( 962 _chrome: &mut Chrome, 963 ctx: &mut AppContext, 964 ui: &mut egui::Ui, 965 ) -> Option<ChromePanelAction> { 966 ui.add_space(8.0); 967 968 let pfp_resp = pfp_button(ctx, ui).on_hover_cursor(egui::CursorIcon::PointingHand); 969 let accounts_resp = accounts_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand); 970 let settings_resp = settings_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand); 971 972 let theme_action = match ui.ctx().theme() { 973 egui::Theme::Dark => { 974 let resp = ui 975 .add(Button::new("☀").frame(false)) 976 .on_hover_cursor(egui::CursorIcon::PointingHand) 977 .on_hover_text(tr!( 978 ctx.i18n, 979 "Switch to light mode", 980 "Hover text for light mode toggle button" 981 )); 982 if resp.clicked() { 983 Some(ChromePanelAction::SaveTheme(ThemePreference::Light)) 984 } else { 985 None 986 } 987 } 988 egui::Theme::Light => { 989 let resp = ui 990 .add(Button::new("🌙").frame(false)) 991 .on_hover_cursor(egui::CursorIcon::PointingHand) 992 .on_hover_text(tr!( 993 ctx.i18n, 994 "Switch to dark mode", 995 "Hover text for dark mode toggle button" 996 )); 997 if resp.clicked() { 998 Some(ChromePanelAction::SaveTheme(ThemePreference::Dark)) 999 } else { 1000 None 1001 } 1002 } 1003 }; 1004 1005 let support_resp = support_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand); 1006 1007 let wallet_resp = ui 1008 .add(wallet_button()) 1009 .on_hover_cursor(egui::CursorIcon::PointingHand); 1010 1011 if ctx.args.options.contains(NotedeckOptions::Debug) { 1012 ui.weak(format!("{}", ctx.frame_history.fps() as i32)); 1013 ui.weak(format!( 1014 "{:10.1}", 1015 ctx.frame_history.mean_frame_time() * 1e3 1016 )); 1017 1018 #[cfg(feature = "memory")] 1019 { 1020 let mem_use = re_memory::MemoryUse::capture(); 1021 if let Some(counted) = mem_use.counted { 1022 if ui 1023 .label(format!("{}", format_bytes(counted as f64))) 1024 .on_hover_cursor(egui::CursorIcon::PointingHand) 1025 .clicked() 1026 { 1027 _chrome.show_memory_debug = !_chrome.show_memory_debug; 1028 } 1029 } 1030 if let Some(resident) = mem_use.resident { 1031 ui.weak(format!("{}", format_bytes(resident as f64))); 1032 } 1033 1034 if _chrome.show_memory_debug { 1035 egui::Window::new("Memory Debug").show(ui.ctx(), memory_debug_ui); 1036 } 1037 } 1038 } 1039 1040 if pfp_resp.clicked() { 1041 let pk = ctx.accounts.get_selected_account().key.pubkey; 1042 Some(ChromePanelAction::Profile(pk)) 1043 } else if accounts_resp.clicked() { 1044 Some(ChromePanelAction::Account) 1045 } else if settings_resp.clicked() { 1046 Some(ChromePanelAction::Settings) 1047 } else if theme_action.is_some() { 1048 theme_action 1049 } else if support_resp.clicked() { 1050 Some(ChromePanelAction::Support) 1051 } else if wallet_resp.clicked() { 1052 Some(ChromePanelAction::Wallet) 1053 } else { 1054 None 1055 } 1056 } 1057 1058 #[cfg(feature = "memory")] 1059 fn memory_debug_ui(ui: &mut egui::Ui) { 1060 let Some(stats) = &re_memory::accounting_allocator::tracking_stats() else { 1061 ui.label("re_memory::accounting_allocator::set_tracking_callstacks(true); not set!!"); 1062 return; 1063 }; 1064 1065 egui::ScrollArea::vertical().show(ui, |ui| { 1066 ui.label(format!( 1067 "track_size_threshold {}", 1068 stats.track_size_threshold 1069 )); 1070 ui.label(format!( 1071 "untracked {} {}", 1072 stats.untracked.count, 1073 format_bytes(stats.untracked.size as f64) 1074 )); 1075 ui.label(format!( 1076 "stochastically_tracked {} {}", 1077 stats.stochastically_tracked.count, 1078 format_bytes(stats.stochastically_tracked.size as f64), 1079 )); 1080 ui.label(format!( 1081 "fully_tracked {} {}", 1082 stats.fully_tracked.count, 1083 format_bytes(stats.fully_tracked.size as f64) 1084 )); 1085 ui.label(format!( 1086 "overhead {} {}", 1087 stats.overhead.count, 1088 format_bytes(stats.overhead.size as f64) 1089 )); 1090 1091 ui.separator(); 1092 1093 for (i, callstack) in stats.top_callstacks.iter().enumerate() { 1094 let full_bt = format!("{}", callstack.readable_backtrace); 1095 let mut lines = full_bt.lines().skip(5); 1096 let bt_header = lines.nth(0).map_or("??", |v| v); 1097 let header = format!( 1098 "#{} {bt_header} {}x {}", 1099 i + 1, 1100 callstack.extant.count, 1101 format_bytes(callstack.extant.size as f64) 1102 ); 1103 1104 egui::CollapsingHeader::new(header) 1105 .id_salt(("mem_cs", i)) 1106 .show(ui, |ui| { 1107 ui.label(lines.collect::<Vec<_>>().join("\n")); 1108 }); 1109 } 1110 }); 1111 } 1112 1113 /// Pretty format a number of bytes by using SI notation (base2), e.g. 1114 /// 1115 /// ``` 1116 /// # use re_format::format_bytes; 1117 /// assert_eq!(format_bytes(123.0), "123 B"); 1118 /// assert_eq!(format_bytes(12_345.0), "12.1 KiB"); 1119 /// assert_eq!(format_bytes(1_234_567.0), "1.2 MiB"); 1120 /// assert_eq!(format_bytes(123_456_789.0), "118 MiB"); 1121 /// ``` 1122 #[cfg(feature = "memory")] 1123 pub fn format_bytes(number_of_bytes: f64) -> String { 1124 /// The minus character: <https://www.compart.com/en/unicode/U+2212> 1125 /// Looks slightly different from the normal hyphen `-`. 1126 const MINUS: char = '−'; 1127 1128 if number_of_bytes < 0.0 { 1129 format!("{MINUS}{}", format_bytes(-number_of_bytes)) 1130 } else if number_of_bytes == 0.0 { 1131 "0 B".to_owned() 1132 } else if number_of_bytes < 1.0 { 1133 format!("{number_of_bytes} B") 1134 } else if number_of_bytes < 20.0 { 1135 let is_integer = number_of_bytes.round() == number_of_bytes; 1136 if is_integer { 1137 format!("{number_of_bytes:.0} B") 1138 } else { 1139 format!("{number_of_bytes:.1} B") 1140 } 1141 } else if number_of_bytes < 10.0_f64.exp2() { 1142 format!("{number_of_bytes:.0} B") 1143 } else if number_of_bytes < 20.0_f64.exp2() { 1144 let decimals = (10.0 * number_of_bytes < 20.0_f64.exp2()) as usize; 1145 format!("{:.*} KiB", decimals, number_of_bytes / 10.0_f64.exp2()) 1146 } else if number_of_bytes < 30.0_f64.exp2() { 1147 let decimals = (10.0 * number_of_bytes < 30.0_f64.exp2()) as usize; 1148 format!("{:.*} MiB", decimals, number_of_bytes / 20.0_f64.exp2()) 1149 } else { 1150 let decimals = (10.0 * number_of_bytes < 40.0_f64.exp2()) as usize; 1151 format!("{:.*} GiB", decimals, number_of_bytes / 30.0_f64.exp2()) 1152 } 1153 }