notedeck

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

side_panel.rs (15512B)


      1 use egui::{
      2     vec2, CursorIcon, InnerResponse, Layout, Margin, RichText, ScrollArea, Separator, Stroke,
      3     Widget,
      4 };
      5 use tracing::{error, info};
      6 
      7 use crate::{
      8     app::{get_active_columns_mut, get_decks_mut},
      9     app_style::DECK_ICON_SIZE,
     10     decks::{DecksAction, DecksCache},
     11     nav::SwitchingAction,
     12     route::Route,
     13 };
     14 
     15 use notedeck::{tr, Accounts, Localization, UserAccount};
     16 use notedeck_ui::{
     17     anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
     18     app_images, colors, View,
     19 };
     20 
     21 use super::configure_deck::deck_icon;
     22 
     23 pub static SIDE_PANEL_WIDTH: f32 = 68.0;
     24 static ICON_WIDTH: f32 = 40.0;
     25 
     26 pub struct DesktopSidePanel<'a> {
     27     selected_account: &'a UserAccount,
     28     decks_cache: &'a DecksCache,
     29     i18n: &'a mut Localization,
     30 }
     31 
     32 impl View for DesktopSidePanel<'_> {
     33     fn ui(&mut self, ui: &mut egui::Ui) {
     34         self.show(ui);
     35     }
     36 }
     37 
     38 #[derive(Debug, Eq, PartialEq, Clone, Copy)]
     39 pub enum SidePanelAction {
     40     Columns,
     41     ComposeNote,
     42     Search,
     43     ExpandSidePanel,
     44     NewDeck,
     45     SwitchDeck(usize),
     46     EditDeck(usize),
     47     Wallet,
     48 }
     49 
     50 pub struct SidePanelResponse {
     51     pub response: egui::Response,
     52     pub action: SidePanelAction,
     53 }
     54 
     55 impl SidePanelResponse {
     56     fn new(action: SidePanelAction, response: egui::Response) -> Self {
     57         SidePanelResponse { action, response }
     58     }
     59 }
     60 
     61 impl<'a> DesktopSidePanel<'a> {
     62     pub fn new(
     63         selected_account: &'a UserAccount,
     64         decks_cache: &'a DecksCache,
     65         i18n: &'a mut Localization,
     66     ) -> Self {
     67         Self {
     68             selected_account,
     69             decks_cache,
     70             i18n,
     71         }
     72     }
     73 
     74     pub fn show(&mut self, ui: &mut egui::Ui) -> Option<SidePanelResponse> {
     75         let frame =
     76             egui::Frame::new().inner_margin(Margin::same(notedeck_ui::constants::FRAME_MARGIN));
     77 
     78         if !ui.visuals().dark_mode {
     79             let rect = ui.available_rect_before_wrap();
     80             ui.painter().rect(
     81                 rect,
     82                 0,
     83                 colors::ALMOST_WHITE,
     84                 egui::Stroke::new(0.0, egui::Color32::TRANSPARENT),
     85                 egui::StrokeKind::Inside,
     86             );
     87         }
     88 
     89         frame.show(ui, |ui| self.show_inner(ui)).inner
     90     }
     91 
     92     fn show_inner(&mut self, ui: &mut egui::Ui) -> Option<SidePanelResponse> {
     93         let dark_mode = ui.ctx().style().visuals.dark_mode;
     94 
     95         let inner = ui
     96             .vertical(|ui| {
     97                 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
     98                     // macos needs a bit of space to make room for window
     99                     // minimize/close buttons
    100                     //if cfg!(target_os = "macos") {
    101                     //    ui.add_space(24.0);
    102                     //}
    103 
    104                     let compose_resp = ui
    105                         .add(crate::ui::post::compose_note_button(dark_mode))
    106                         .on_hover_cursor(egui::CursorIcon::PointingHand);
    107                     let search_resp = ui.add(search_button());
    108                     let column_resp = ui.add(add_column_button());
    109 
    110                     ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0));
    111 
    112                     ui.add_space(8.0);
    113                     ui.add(egui::Label::new(
    114                         RichText::new(tr!(
    115                             self.i18n,
    116                             "DECKS",
    117                             "Label for decks section in side panel"
    118                         ))
    119                         .size(11.0)
    120                         .color(ui.visuals().noninteractive().fg_stroke.color),
    121                     ));
    122                     ui.add_space(8.0);
    123                     let add_deck_resp = ui.add(add_deck_button());
    124 
    125                     let decks_inner = ScrollArea::vertical()
    126                         .max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0)))
    127                         .show(ui, |ui| {
    128                             show_decks(ui, self.decks_cache, self.selected_account)
    129                         })
    130                         .inner;
    131 
    132                     /*
    133                     if expand_resp.clicked() {
    134                         Some(InnerResponse::new(
    135                             SidePanelAction::ExpandSidePanel,
    136                             expand_resp,
    137                         ))
    138                     */
    139                     if compose_resp.clicked() {
    140                         Some(InnerResponse::new(
    141                             SidePanelAction::ComposeNote,
    142                             compose_resp,
    143                         ))
    144                     } else if search_resp.clicked() {
    145                         Some(InnerResponse::new(SidePanelAction::Search, search_resp))
    146                     } else if column_resp.clicked() {
    147                         Some(InnerResponse::new(SidePanelAction::Columns, column_resp))
    148                     } else if add_deck_resp.clicked() {
    149                         Some(InnerResponse::new(SidePanelAction::NewDeck, add_deck_resp))
    150                     } else if decks_inner.response.secondary_clicked() {
    151                         info!("decks inner secondary click");
    152                         if let Some(clicked_index) = decks_inner.inner {
    153                             Some(InnerResponse::new(
    154                                 SidePanelAction::EditDeck(clicked_index),
    155                                 decks_inner.response,
    156                             ))
    157                         } else {
    158                             None
    159                         }
    160                     } else if decks_inner.response.clicked() {
    161                         if let Some(clicked_index) = decks_inner.inner {
    162                             Some(InnerResponse::new(
    163                                 SidePanelAction::SwitchDeck(clicked_index),
    164                                 decks_inner.response,
    165                             ))
    166                         } else {
    167                             None
    168                         }
    169                     } else {
    170                         None
    171                     }
    172                 })
    173                 .inner
    174             })
    175             .inner;
    176 
    177         if let Some(inner) = inner {
    178             Some(SidePanelResponse::new(inner.inner, inner.response))
    179         } else {
    180             None
    181         }
    182     }
    183 
    184     pub fn perform_action(
    185         decks_cache: &mut DecksCache,
    186         accounts: &Accounts,
    187         action: SidePanelAction,
    188         i18n: &mut Localization,
    189     ) -> Option<SwitchingAction> {
    190         let router = get_active_columns_mut(i18n, accounts, decks_cache).get_first_router();
    191         let mut switching_response = None;
    192         match action {
    193             /*
    194             SidePanelAction::Panel => {} // TODO
    195             SidePanelAction::Account => {
    196                 if router
    197                     .routes()
    198                     .iter()
    199                     .any(|r| r == &Route::Accounts(AccountsRoute::Accounts))
    200                 {
    201                     // return if we are already routing to accounts
    202                     router.go_back();
    203                 } else {
    204                     router.route_to(Route::accounts());
    205                 }
    206             }
    207             SidePanelAction::Settings => {
    208                 if router.routes().iter().any(|r| r == &Route::Relays) {
    209                     // return if we are already routing to accounts
    210                     router.go_back();
    211                 } else {
    212                     router.route_to(Route::relays());
    213                 }
    214             }
    215             SidePanelAction::Support => {
    216                 if router.routes().iter().any(|r| r == &Route::Support) {
    217                     router.go_back();
    218                 } else {
    219                     support.refresh();
    220                     router.route_to(Route::Support);
    221                 }
    222             }
    223             */
    224             SidePanelAction::Columns => {
    225                 if router
    226                     .routes()
    227                     .iter()
    228                     .any(|r| matches!(r, Route::AddColumn(_)))
    229                 {
    230                     router.go_back();
    231                 } else {
    232                     get_active_columns_mut(i18n, accounts, decks_cache).new_column_picker();
    233                 }
    234             }
    235             SidePanelAction::ComposeNote => {
    236                 let can_post = accounts.get_selected_account().key.secret_key.is_some();
    237 
    238                 if !can_post {
    239                     router.route_to(Route::accounts());
    240                 } else if router.routes().iter().any(|r| r == &Route::ComposeNote) {
    241                     router.go_back();
    242                 } else {
    243                     router.route_to(Route::ComposeNote);
    244                 }
    245             }
    246             SidePanelAction::Search => {
    247                 // TODO
    248                 if router.top() == &Route::Search {
    249                     router.go_back();
    250                 } else {
    251                     router.route_to(Route::Search);
    252                 }
    253             }
    254             SidePanelAction::ExpandSidePanel => {
    255                 // TODO
    256                 info!("Clicked expand side panel button");
    257             }
    258             SidePanelAction::NewDeck => {
    259                 if router.routes().iter().any(|r| r == &Route::NewDeck) {
    260                     router.go_back();
    261                 } else {
    262                     router.route_to(Route::NewDeck);
    263                 }
    264             }
    265             SidePanelAction::SwitchDeck(index) => {
    266                 switching_response = Some(crate::nav::SwitchingAction::Decks(DecksAction::Switch(
    267                     index,
    268                 )))
    269             }
    270             SidePanelAction::EditDeck(index) => {
    271                 if router.routes().iter().any(|r| r == &Route::EditDeck(index)) {
    272                     router.go_back();
    273                 } else {
    274                     switching_response = Some(crate::nav::SwitchingAction::Decks(
    275                         DecksAction::Switch(index),
    276                     ));
    277                     if let Some(edit_deck) = get_decks_mut(i18n, accounts, decks_cache)
    278                         .decks_mut()
    279                         .get_mut(index)
    280                     {
    281                         edit_deck
    282                             .columns_mut()
    283                             .get_first_router()
    284                             .route_to(Route::EditDeck(index));
    285                     } else {
    286                         error!("Cannot push EditDeck route to index {}", index);
    287                     }
    288                 }
    289             }
    290             SidePanelAction::Wallet => 's: {
    291                 if router
    292                     .routes()
    293                     .iter()
    294                     .any(|r| matches!(r, Route::Wallet(_)))
    295                 {
    296                     router.go_back();
    297                     break 's;
    298                 }
    299 
    300                 router.route_to(Route::Wallet(notedeck::WalletType::Auto));
    301             }
    302         }
    303         switching_response
    304     }
    305 }
    306 
    307 fn add_column_button() -> impl Widget {
    308     move |ui: &mut egui::Ui| {
    309         let img_size = 24.0;
    310         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    311 
    312         let img = if ui.visuals().dark_mode {
    313             app_images::add_column_dark_image()
    314         } else {
    315             app_images::add_column_light_image()
    316         };
    317 
    318         let helper = AnimationHelper::new(ui, "add-column-button", vec2(max_size, max_size));
    319 
    320         let cur_img_size = helper.scale_1d_pos(img_size);
    321         img.paint_at(
    322             ui,
    323             helper
    324                 .get_animation_rect()
    325                 .shrink((max_size - cur_img_size) / 2.0),
    326         );
    327 
    328         helper
    329             .take_animation_response()
    330             .on_hover_cursor(CursorIcon::PointingHand)
    331             .on_hover_text("Add new column")
    332     }
    333 }
    334 
    335 pub fn search_button() -> impl Widget {
    336     |ui: &mut egui::Ui| -> egui::Response {
    337         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    338         let min_line_width_circle = 1.5; // width of the magnifying glass
    339         let min_line_width_handle = 1.5;
    340         let helper = AnimationHelper::new(ui, "search-button", vec2(max_size, max_size));
    341 
    342         let painter = ui.painter_at(helper.get_animation_rect());
    343 
    344         let cur_line_width_circle = helper.scale_1d_pos(min_line_width_circle);
    345         let cur_line_width_handle = helper.scale_1d_pos(min_line_width_handle);
    346         let min_outer_circle_radius = helper.scale_radius(15.0);
    347         let cur_outer_circle_radius = helper.scale_1d_pos(min_outer_circle_radius);
    348         let min_handle_length = 7.0;
    349         let cur_handle_length = helper.scale_1d_pos(min_handle_length);
    350 
    351         let circle_center = helper.scale_from_center(-2.0, -2.0);
    352 
    353         let handle_vec = vec2(
    354             std::f32::consts::FRAC_1_SQRT_2,
    355             std::f32::consts::FRAC_1_SQRT_2,
    356         );
    357 
    358         let handle_pos_1 = circle_center + (handle_vec * (cur_outer_circle_radius - 3.0));
    359         let handle_pos_2 =
    360             circle_center + (handle_vec * (cur_outer_circle_radius + cur_handle_length));
    361 
    362         let circle_stroke = Stroke::new(cur_line_width_circle, colors::MID_GRAY);
    363         let handle_stroke = Stroke::new(cur_line_width_handle, colors::MID_GRAY);
    364 
    365         painter.line_segment([handle_pos_1, handle_pos_2], handle_stroke);
    366         painter.circle(
    367             circle_center,
    368             min_outer_circle_radius,
    369             ui.style().visuals.widgets.inactive.weak_bg_fill,
    370             circle_stroke,
    371         );
    372 
    373         helper
    374             .take_animation_response()
    375             .on_hover_cursor(CursorIcon::PointingHand)
    376             .on_hover_text("Open search")
    377     }
    378 }
    379 
    380 // TODO: convert to responsive button when expanded side panel impl is finished
    381 
    382 fn add_deck_button() -> impl Widget {
    383     |ui: &mut egui::Ui| -> egui::Response {
    384         let img_size = 40.0;
    385 
    386         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    387         let img = app_images::new_deck_image().max_width(img_size);
    388 
    389         let helper = AnimationHelper::new(ui, "new-deck-icon", vec2(max_size, max_size));
    390 
    391         let cur_img_size = helper.scale_1d_pos(img_size);
    392         img.paint_at(
    393             ui,
    394             helper
    395                 .get_animation_rect()
    396                 .shrink((max_size - cur_img_size) / 2.0),
    397         );
    398 
    399         helper
    400             .take_animation_response()
    401             .on_hover_cursor(CursorIcon::PointingHand)
    402             .on_hover_text("Add new deck")
    403     }
    404 }
    405 
    406 fn show_decks<'a>(
    407     ui: &mut egui::Ui,
    408     decks_cache: &'a DecksCache,
    409     selected_account: &'a UserAccount,
    410 ) -> InnerResponse<Option<usize>> {
    411     let show_decks_id = ui.id().with("show-decks");
    412     let account_id = selected_account.key.pubkey;
    413     let (cur_decks, account_id) = (
    414         decks_cache.decks(&account_id),
    415         show_decks_id.with(account_id),
    416     );
    417     let active_index = cur_decks.active_index();
    418 
    419     let (_, mut resp) = ui.allocate_exact_size(vec2(0.0, 0.0), egui::Sense::click());
    420     let mut clicked_index = None;
    421     for (index, deck) in cur_decks.decks().iter().enumerate() {
    422         let highlight = index == active_index;
    423         let deck_icon_resp = ui
    424             .add(deck_icon(
    425                 account_id.with(index),
    426                 Some(deck.icon),
    427                 DECK_ICON_SIZE,
    428                 40.0,
    429                 highlight,
    430             ))
    431             .on_hover_text_at_pointer(&deck.name)
    432             .on_hover_cursor(CursorIcon::PointingHand);
    433         if deck_icon_resp.clicked() || deck_icon_resp.secondary_clicked() {
    434             clicked_index = Some(index);
    435         }
    436         resp = resp.union(deck_icon_resp);
    437     }
    438     InnerResponse::new(clicked_index, resp)
    439 }