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