notedeck

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

side_panel.rs (15883B)


      1 use egui::{vec2, Color32, InnerResponse, Layout, Margin, Separator, Stroke, Widget};
      2 use tracing::info;
      3 
      4 use crate::{
      5     account_manager::AccountsRoute,
      6     colors,
      7     column::{Column, Columns},
      8     imgcache::ImageCache,
      9     route::Route,
     10     support::Support,
     11     user_account::UserAccount,
     12     Damus,
     13 };
     14 
     15 use super::{
     16     anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
     17     profile::preview::get_account_url,
     18     ProfilePic, View,
     19 };
     20 
     21 pub static SIDE_PANEL_WIDTH: f32 = 64.0;
     22 static ICON_WIDTH: f32 = 40.0;
     23 
     24 pub struct DesktopSidePanel<'a> {
     25     ndb: &'a nostrdb::Ndb,
     26     img_cache: &'a mut ImageCache,
     27     selected_account: Option<&'a UserAccount>,
     28 }
     29 
     30 impl<'a> View for DesktopSidePanel<'a> {
     31     fn ui(&mut self, ui: &mut egui::Ui) {
     32         self.show(ui);
     33     }
     34 }
     35 
     36 #[derive(Debug, Eq, PartialEq, Clone, Copy)]
     37 pub enum SidePanelAction {
     38     Panel,
     39     Account,
     40     Settings,
     41     Columns,
     42     ComposeNote,
     43     Search,
     44     ExpandSidePanel,
     45     Support,
     46 }
     47 
     48 pub struct SidePanelResponse {
     49     pub response: egui::Response,
     50     pub action: SidePanelAction,
     51 }
     52 
     53 impl SidePanelResponse {
     54     fn new(action: SidePanelAction, response: egui::Response) -> Self {
     55         SidePanelResponse { action, response }
     56     }
     57 }
     58 
     59 impl<'a> DesktopSidePanel<'a> {
     60     pub fn new(
     61         ndb: &'a nostrdb::Ndb,
     62         img_cache: &'a mut ImageCache,
     63         selected_account: Option<&'a UserAccount>,
     64     ) -> Self {
     65         Self {
     66             ndb,
     67             img_cache,
     68             selected_account,
     69         }
     70     }
     71 
     72     pub fn show(&mut self, ui: &mut egui::Ui) -> SidePanelResponse {
     73         egui::Frame::none()
     74             .inner_margin(Margin::same(8.0))
     75             .show(ui, |ui| self.show_inner(ui))
     76             .inner
     77     }
     78 
     79     fn show_inner(&mut self, ui: &mut egui::Ui) -> SidePanelResponse {
     80         let dark_mode = ui.ctx().style().visuals.dark_mode;
     81 
     82         let inner = ui
     83             .vertical(|ui| {
     84                 let top_resp = ui
     85                     .with_layout(Layout::top_down(egui::Align::Center), |ui| {
     86                         let expand_resp = ui.add(expand_side_panel_button());
     87                         ui.add_space(28.0);
     88                         let compose_resp = ui.add(compose_note_button());
     89                         let search_resp = ui.add(search_button());
     90                         let column_resp = ui.add(add_column_button(dark_mode));
     91 
     92                         ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0));
     93 
     94                         if expand_resp.clicked() {
     95                             Some(InnerResponse::new(
     96                                 SidePanelAction::ExpandSidePanel,
     97                                 expand_resp,
     98                             ))
     99                         } else if compose_resp.clicked() {
    100                             Some(InnerResponse::new(
    101                                 SidePanelAction::ComposeNote,
    102                                 compose_resp,
    103                             ))
    104                         } else if search_resp.clicked() {
    105                             Some(InnerResponse::new(SidePanelAction::Search, search_resp))
    106                         } else if column_resp.clicked() {
    107                             Some(InnerResponse::new(SidePanelAction::Columns, column_resp))
    108                         } else {
    109                             None
    110                         }
    111                     })
    112                     .inner;
    113 
    114                 let (pfp_resp, bottom_resp) = ui
    115                     .with_layout(Layout::bottom_up(egui::Align::Center), |ui| {
    116                         let pfp_resp = self.pfp_button(ui);
    117                         let settings_resp = ui.add(settings_button(dark_mode));
    118 
    119                         let support_resp = ui.add(support_button());
    120 
    121                         let optional_inner = if pfp_resp.clicked() {
    122                             Some(egui::InnerResponse::new(
    123                                 SidePanelAction::Account,
    124                                 pfp_resp.clone(),
    125                             ))
    126                         } else if settings_resp.clicked() || settings_resp.hovered() {
    127                             Some(egui::InnerResponse::new(
    128                                 SidePanelAction::Settings,
    129                                 settings_resp,
    130                             ))
    131                         } else if support_resp.clicked() {
    132                             Some(egui::InnerResponse::new(
    133                                 SidePanelAction::Support,
    134                                 support_resp,
    135                             ))
    136                         } else {
    137                             None
    138                         };
    139 
    140                         (pfp_resp, optional_inner)
    141                     })
    142                     .inner;
    143 
    144                 if let Some(bottom_inner) = bottom_resp {
    145                     bottom_inner
    146                 } else if let Some(top_inner) = top_resp {
    147                     top_inner
    148                 } else {
    149                     egui::InnerResponse::new(SidePanelAction::Panel, pfp_resp)
    150                 }
    151             })
    152             .inner;
    153 
    154         SidePanelResponse::new(inner.inner, inner.response)
    155     }
    156 
    157     fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response {
    158         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    159         let helper = AnimationHelper::new(ui, "pfp-button", vec2(max_size, max_size));
    160 
    161         let min_pfp_size = ICON_WIDTH;
    162         let cur_pfp_size = helper.scale_1d_pos(min_pfp_size);
    163 
    164         let txn = nostrdb::Transaction::new(self.ndb).expect("should be able to create txn");
    165         let profile_url = get_account_url(&txn, self.ndb, self.selected_account);
    166 
    167         let widget = ProfilePic::new(self.img_cache, profile_url).size(cur_pfp_size);
    168 
    169         ui.put(helper.get_animation_rect(), widget);
    170 
    171         helper.take_animation_response()
    172     }
    173 
    174     pub fn perform_action(columns: &mut Columns, support: &mut Support, action: SidePanelAction) {
    175         let router = columns.get_first_router();
    176         match action {
    177             SidePanelAction::Panel => {} // TODO
    178             SidePanelAction::Account => {
    179                 if router
    180                     .routes()
    181                     .iter()
    182                     .any(|&r| r == Route::Accounts(AccountsRoute::Accounts))
    183                 {
    184                     // return if we are already routing to accounts
    185                     router.go_back();
    186                 } else {
    187                     router.route_to(Route::accounts());
    188                 }
    189             }
    190             SidePanelAction::Settings => {
    191                 if router.routes().iter().any(|&r| r == Route::Relays) {
    192                     // return if we are already routing to accounts
    193                     router.go_back();
    194                 } else {
    195                     router.route_to(Route::relays());
    196                 }
    197             }
    198             SidePanelAction::Columns => {
    199                 if router
    200                     .routes()
    201                     .iter()
    202                     .any(|&r| matches!(r, Route::AddColumn(_)))
    203                 {
    204                     router.go_back();
    205                 } else {
    206                     columns.new_column_picker();
    207                 }
    208             }
    209             SidePanelAction::ComposeNote => {
    210                 if router.routes().iter().any(|&r| r == Route::ComposeNote) {
    211                     router.go_back();
    212                 } else {
    213                     router.route_to(Route::ComposeNote);
    214                 }
    215             }
    216             SidePanelAction::Search => {
    217                 // TODO
    218                 info!("Clicked search button");
    219             }
    220             SidePanelAction::ExpandSidePanel => {
    221                 // TODO
    222                 info!("Clicked expand side panel button");
    223             }
    224             SidePanelAction::Support => {
    225                 if router.routes().iter().any(|&r| r == Route::Support) {
    226                     router.go_back();
    227                 } else {
    228                     support.refresh();
    229                     router.route_to(Route::Support);
    230                 }
    231             }
    232         }
    233     }
    234 }
    235 
    236 fn settings_button(dark_mode: bool) -> impl Widget {
    237     let _ = dark_mode;
    238     |ui: &mut egui::Ui| {
    239         let img_size = 24.0;
    240         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    241         let img_data = egui::include_image!("../../assets/icons/settings_dark_4x.png");
    242         let img = egui::Image::new(img_data).max_width(img_size);
    243 
    244         let helper = AnimationHelper::new(ui, "settings-button", vec2(max_size, max_size));
    245 
    246         let cur_img_size = helper.scale_1d_pos(img_size);
    247         img.paint_at(
    248             ui,
    249             helper
    250                 .get_animation_rect()
    251                 .shrink((max_size - cur_img_size) / 2.0),
    252         );
    253 
    254         helper.take_animation_response()
    255     }
    256 }
    257 
    258 fn add_column_button(dark_mode: bool) -> impl Widget {
    259     let _ = dark_mode;
    260     move |ui: &mut egui::Ui| {
    261         let img_size = 24.0;
    262         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    263 
    264         let img_data = egui::include_image!("../../assets/icons/add_column_dark_4x.png");
    265 
    266         let img = egui::Image::new(img_data).max_width(img_size);
    267 
    268         let helper = AnimationHelper::new(ui, "add-column-button", vec2(max_size, max_size));
    269 
    270         let cur_img_size = helper.scale_1d_pos(img_size);
    271         img.paint_at(
    272             ui,
    273             helper
    274                 .get_animation_rect()
    275                 .shrink((max_size - cur_img_size) / 2.0),
    276         );
    277 
    278         helper.take_animation_response()
    279     }
    280 }
    281 
    282 fn compose_note_button() -> impl Widget {
    283     |ui: &mut egui::Ui| -> egui::Response {
    284         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    285 
    286         let min_outer_circle_diameter = 40.0;
    287         let min_plus_sign_size = 14.0; // length of the plus sign
    288         let min_line_width = 2.25; // width of the plus sign
    289 
    290         let helper = AnimationHelper::new(ui, "note-compose-button", vec2(max_size, max_size));
    291 
    292         let painter = ui.painter_at(helper.get_animation_rect());
    293 
    294         let use_background_radius = helper.scale_radius(min_outer_circle_diameter);
    295         let use_line_width = helper.scale_1d_pos(min_line_width);
    296         let use_edge_circle_radius = helper.scale_radius(min_line_width);
    297 
    298         painter.circle_filled(helper.center(), use_background_radius, colors::PINK);
    299 
    300         let min_half_plus_sign_size = min_plus_sign_size / 2.0;
    301         let north_edge = helper.scale_from_center(0.0, min_half_plus_sign_size);
    302         let south_edge = helper.scale_from_center(0.0, -min_half_plus_sign_size);
    303         let west_edge = helper.scale_from_center(-min_half_plus_sign_size, 0.0);
    304         let east_edge = helper.scale_from_center(min_half_plus_sign_size, 0.0);
    305 
    306         painter.line_segment(
    307             [north_edge, south_edge],
    308             Stroke::new(use_line_width, Color32::WHITE),
    309         );
    310         painter.line_segment(
    311             [west_edge, east_edge],
    312             Stroke::new(use_line_width, Color32::WHITE),
    313         );
    314         painter.circle_filled(north_edge, use_edge_circle_radius, Color32::WHITE);
    315         painter.circle_filled(south_edge, use_edge_circle_radius, Color32::WHITE);
    316         painter.circle_filled(west_edge, use_edge_circle_radius, Color32::WHITE);
    317         painter.circle_filled(east_edge, use_edge_circle_radius, Color32::WHITE);
    318 
    319         helper.take_animation_response()
    320     }
    321 }
    322 
    323 fn search_button() -> impl Widget {
    324     |ui: &mut egui::Ui| -> egui::Response {
    325         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    326         let min_line_width_circle = 1.5; // width of the magnifying glass
    327         let min_line_width_handle = 1.5;
    328         let helper = AnimationHelper::new(ui, "search-button", vec2(max_size, max_size));
    329 
    330         let painter = ui.painter_at(helper.get_animation_rect());
    331 
    332         let cur_line_width_circle = helper.scale_1d_pos(min_line_width_circle);
    333         let cur_line_width_handle = helper.scale_1d_pos(min_line_width_handle);
    334         let min_outer_circle_radius = helper.scale_radius(15.0);
    335         let cur_outer_circle_radius = helper.scale_1d_pos(min_outer_circle_radius);
    336         let min_handle_length = 7.0;
    337         let cur_handle_length = helper.scale_1d_pos(min_handle_length);
    338 
    339         let circle_center = helper.scale_from_center(-2.0, -2.0);
    340 
    341         let handle_vec = vec2(
    342             std::f32::consts::FRAC_1_SQRT_2,
    343             std::f32::consts::FRAC_1_SQRT_2,
    344         );
    345 
    346         let handle_pos_1 = circle_center + (handle_vec * (cur_outer_circle_radius - 3.0));
    347         let handle_pos_2 =
    348             circle_center + (handle_vec * (cur_outer_circle_radius + cur_handle_length));
    349 
    350         let circle_stroke = Stroke::new(cur_line_width_circle, colors::MID_GRAY);
    351         let handle_stroke = Stroke::new(cur_line_width_handle, colors::MID_GRAY);
    352 
    353         painter.line_segment([handle_pos_1, handle_pos_2], handle_stroke);
    354         painter.circle(
    355             circle_center,
    356             min_outer_circle_radius,
    357             ui.style().visuals.widgets.inactive.weak_bg_fill,
    358             circle_stroke,
    359         );
    360 
    361         helper.take_animation_response()
    362     }
    363 }
    364 
    365 // TODO: convert to responsive button when expanded side panel impl is finished
    366 fn expand_side_panel_button() -> impl Widget {
    367     |ui: &mut egui::Ui| -> egui::Response {
    368         let img_size = 40.0;
    369         let img_data = egui::include_image!("../../assets/damus_rounded_80.png");
    370         let img = egui::Image::new(img_data).max_width(img_size);
    371 
    372         ui.add(img)
    373     }
    374 }
    375 
    376 fn support_button() -> impl Widget {
    377     |ui: &mut egui::Ui| -> egui::Response {
    378         let img_size = 16.0;
    379 
    380         let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
    381         let img_data = egui::include_image!("../../assets/icons/help_icon_dark_4x.png");
    382         let img = egui::Image::new(img_data).max_width(img_size);
    383 
    384         let helper = AnimationHelper::new(ui, "help-button", vec2(max_size, max_size));
    385 
    386         let cur_img_size = helper.scale_1d_pos(img_size);
    387         img.paint_at(
    388             ui,
    389             helper
    390                 .get_animation_rect()
    391                 .shrink((max_size - cur_img_size) / 2.0),
    392         );
    393 
    394         helper.take_animation_response()
    395     }
    396 }
    397 
    398 mod preview {
    399 
    400     use egui_extras::{Size, StripBuilder};
    401 
    402     use crate::{
    403         test_data,
    404         ui::{Preview, PreviewConfig},
    405     };
    406 
    407     use super::*;
    408 
    409     pub struct DesktopSidePanelPreview {
    410         app: Damus,
    411     }
    412 
    413     impl DesktopSidePanelPreview {
    414         fn new() -> Self {
    415             let mut app = test_data::test_app();
    416             app.columns.add_column(Column::new(vec![Route::accounts()]));
    417             DesktopSidePanelPreview { app }
    418         }
    419     }
    420 
    421     impl View for DesktopSidePanelPreview {
    422         fn ui(&mut self, ui: &mut egui::Ui) {
    423             StripBuilder::new(ui)
    424                 .size(Size::exact(SIDE_PANEL_WIDTH))
    425                 .sizes(Size::remainder(), 0)
    426                 .clip(true)
    427                 .horizontal(|mut strip| {
    428                     strip.cell(|ui| {
    429                         let mut panel = DesktopSidePanel::new(
    430                             &self.app.ndb,
    431                             &mut self.app.img_cache,
    432                             self.app.accounts.get_selected_account(),
    433                         );
    434                         let response = panel.show(ui);
    435 
    436                         DesktopSidePanel::perform_action(
    437                             &mut self.app.columns,
    438                             &mut self.app.support,
    439                             response.action,
    440                         );
    441                     });
    442                 });
    443         }
    444     }
    445 
    446     impl<'a> Preview for DesktopSidePanel<'a> {
    447         type Prev = DesktopSidePanelPreview;
    448 
    449         fn preview(_cfg: PreviewConfig) -> Self::Prev {
    450             DesktopSidePanelPreview::new()
    451         }
    452     }
    453 }