notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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: &notedeck::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 }