notedeck

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

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