notedeck

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

nav.rs (14312B)


      1 use crate::{
      2     account_manager::render_accounts_route,
      3     app_style::{get_font_size, NotedeckTextStyle},
      4     column::Columns,
      5     fonts::NamedFontFamily,
      6     notes_holder::NotesHolder,
      7     profile::Profile,
      8     relay_pool_manager::RelayPoolManager,
      9     route::Route,
     10     storage::{self, DataPath},
     11     thread::Thread,
     12     timeline::{
     13         route::{render_profile_route, render_timeline_route, AfterRouteExecution, TimelineRoute},
     14         Timeline,
     15     },
     16     ui::{
     17         self,
     18         add_column::render_add_column_routes,
     19         anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
     20         note::PostAction,
     21         support::SupportView,
     22         RelayView, View,
     23     },
     24     Damus,
     25 };
     26 
     27 use egui::{pos2, Color32, InnerResponse, Stroke};
     28 use egui_nav::{Nav, NavAction, TitleBarResponse};
     29 use nostrdb::{Ndb, Transaction};
     30 use tracing::{error, info};
     31 
     32 pub enum RenderNavResponse {
     33     ColumnChanged,
     34     RemoveColumn(usize),
     35 }
     36 
     37 impl RenderNavResponse {
     38     pub fn process_nav_response(&self, path: &DataPath, columns: &mut Columns) {
     39         match self {
     40             RenderNavResponse::ColumnChanged => {
     41                 storage::save_columns(path, columns.as_serializable_columns());
     42             }
     43 
     44             RenderNavResponse::RemoveColumn(col) => {
     45                 columns.delete_column(*col);
     46                 storage::save_columns(path, columns.as_serializable_columns());
     47             }
     48         }
     49     }
     50 }
     51 
     52 pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> Option<RenderNavResponse> {
     53     let mut resp: Option<RenderNavResponse> = None;
     54     let col_id = app.columns.get_column_id_at_index(col);
     55     // TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly
     56     let routes = app
     57         .columns()
     58         .column(col)
     59         .router()
     60         .routes()
     61         .iter()
     62         .map(|r| r.get_titled_route(&app.columns, &app.ndb))
     63         .collect();
     64     let nav_response = Nav::new(routes)
     65         .navigating(app.columns_mut().column_mut(col).router_mut().navigating)
     66         .returning(app.columns_mut().column_mut(col).router_mut().returning)
     67         .id_source(egui::Id::new(col_id))
     68         .title(48.0, title_bar)
     69         .show_mut(ui, |ui, nav| {
     70             let column = app.columns.column_mut(col);
     71             match &nav.top().route {
     72                 Route::Timeline(tlr) => render_timeline_route(
     73                     &app.ndb,
     74                     &mut app.columns,
     75                     &mut app.pool,
     76                     &mut app.drafts,
     77                     &mut app.img_cache,
     78                     &mut app.unknown_ids,
     79                     &mut app.note_cache,
     80                     &mut app.threads,
     81                     &mut app.accounts,
     82                     *tlr,
     83                     col,
     84                     app.textmode,
     85                     ui,
     86                 ),
     87                 Route::Accounts(amr) => {
     88                     let action = render_accounts_route(
     89                         ui,
     90                         &app.ndb,
     91                         col,
     92                         &mut app.columns,
     93                         &mut app.img_cache,
     94                         &mut app.accounts,
     95                         &mut app.view_state.login,
     96                         *amr,
     97                     );
     98                     let txn = Transaction::new(&app.ndb).expect("txn");
     99                     action.process_action(&mut app.unknown_ids, &app.ndb, &txn);
    100                     None
    101                 }
    102                 Route::Relays => {
    103                     let manager = RelayPoolManager::new(app.pool_mut());
    104                     RelayView::new(manager).ui(ui);
    105                     None
    106                 }
    107                 Route::ComposeNote => {
    108                     let kp = app.accounts.selected_or_first_nsec()?;
    109                     let draft = app.drafts.compose_mut();
    110 
    111                     let txn = nostrdb::Transaction::new(&app.ndb).expect("txn");
    112                     let post_response = ui::PostView::new(
    113                         &app.ndb,
    114                         draft,
    115                         crate::draft::DraftSource::Compose,
    116                         &mut app.img_cache,
    117                         &mut app.note_cache,
    118                         kp,
    119                     )
    120                     .ui(&txn, ui);
    121 
    122                     if let Some(action) = post_response.action {
    123                         PostAction::execute(kp, &action, &mut app.pool, draft, |np, seckey| {
    124                             np.to_note(seckey)
    125                         });
    126                         column.router_mut().go_back();
    127                     }
    128 
    129                     None
    130                 }
    131                 Route::AddColumn(route) => {
    132                     render_add_column_routes(ui, app, col, route);
    133 
    134                     None
    135                 }
    136 
    137                 Route::Profile(pubkey) => render_profile_route(
    138                     pubkey,
    139                     &app.ndb,
    140                     &mut app.columns,
    141                     &mut app.profiles,
    142                     &mut app.pool,
    143                     &mut app.img_cache,
    144                     &mut app.note_cache,
    145                     &mut app.threads,
    146                     col,
    147                     ui,
    148                 ),
    149                 Route::Support => {
    150                     SupportView::new(&mut app.support).show(ui);
    151                     None
    152                 }
    153             }
    154         });
    155 
    156     if let Some(after_route_execution) = nav_response.inner {
    157         // start returning when we're finished posting
    158         match after_route_execution {
    159             AfterRouteExecution::Post(resp) => {
    160                 if let Some(action) = resp.action {
    161                     match action {
    162                         PostAction::Post(_) => {
    163                             app.columns_mut().column_mut(col).router_mut().returning = true;
    164                         }
    165                     }
    166                 }
    167             }
    168 
    169             AfterRouteExecution::OpenProfile(pubkey) => {
    170                 app.columns
    171                     .column_mut(col)
    172                     .router_mut()
    173                     .route_to(Route::Profile(pubkey));
    174                 let txn = Transaction::new(&app.ndb).expect("txn");
    175                 if let Some(res) = Profile::open(
    176                     &app.ndb,
    177                     &mut app.note_cache,
    178                     &txn,
    179                     &mut app.pool,
    180                     &mut app.profiles,
    181                     pubkey.bytes(),
    182                 ) {
    183                     res.process(&app.ndb, &mut app.note_cache, &txn, &mut app.profiles);
    184                 }
    185             }
    186         }
    187     }
    188 
    189     if let Some(NavAction::Returned) = nav_response.action {
    190         let r = app.columns_mut().column_mut(col).router_mut().pop();
    191         let txn = Transaction::new(&app.ndb).expect("txn");
    192         if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r {
    193             let root_id = {
    194                 crate::note::root_note_id_from_selected_id(
    195                     &app.ndb,
    196                     &mut app.note_cache,
    197                     &txn,
    198                     id.bytes(),
    199                 )
    200             };
    201             Thread::unsubscribe_locally(
    202                 &txn,
    203                 &app.ndb,
    204                 &mut app.note_cache,
    205                 &mut app.threads,
    206                 &mut app.pool,
    207                 root_id,
    208             );
    209         }
    210 
    211         if let Some(Route::Profile(pubkey)) = r {
    212             Profile::unsubscribe_locally(
    213                 &txn,
    214                 &app.ndb,
    215                 &mut app.note_cache,
    216                 &mut app.profiles,
    217                 &mut app.pool,
    218                 pubkey.bytes(),
    219             );
    220         }
    221         resp = Some(RenderNavResponse::ColumnChanged)
    222     } else if let Some(NavAction::Navigated) = nav_response.action {
    223         let cur_router = app.columns_mut().column_mut(col).router_mut();
    224         cur_router.navigating = false;
    225         if cur_router.is_replacing() {
    226             cur_router.remove_previous_routes();
    227         }
    228         resp = Some(RenderNavResponse::ColumnChanged)
    229     }
    230 
    231     if let Some(title_response) = nav_response.title_response {
    232         match title_response {
    233             TitleResponse::RemoveColumn => {
    234                 let tl = app.columns().find_timeline_for_column_index(col);
    235                 if let Some(timeline) = tl {
    236                     unsubscribe_timeline(app.ndb(), timeline);
    237                 }
    238                 resp = Some(RenderNavResponse::RemoveColumn(col))
    239             }
    240         }
    241     }
    242 
    243     resp
    244 }
    245 
    246 fn unsubscribe_timeline(ndb: &Ndb, timeline: &Timeline) {
    247     if let Some(sub_id) = timeline.subscription {
    248         if let Err(e) = ndb.unsubscribe(sub_id) {
    249             error!("unsubscribe error: {}", e);
    250         } else {
    251             info!(
    252                 "successfully unsubscribed from timeline {} with sub id {}",
    253                 timeline.id,
    254                 sub_id.id()
    255             );
    256         }
    257     }
    258 }
    259 
    260 fn title_bar(
    261     ui: &mut egui::Ui,
    262     allocated_response: egui::Response,
    263     title_name: String,
    264     back_name: Option<String>,
    265 ) -> egui::InnerResponse<TitleBarResponse<TitleResponse>> {
    266     let icon_width = 32.0;
    267     let padding_external = 16.0;
    268     let padding_internal = 8.0;
    269     let has_back = back_name.is_some();
    270 
    271     let (spacing_rect, titlebar_rect) = allocated_response
    272         .rect
    273         .split_left_right_at_x(allocated_response.rect.left() + padding_external);
    274     ui.advance_cursor_after_rect(spacing_rect);
    275 
    276     let (titlebar_resp, maybe_button_resp) = if has_back {
    277         let (button_rect, titlebar_rect) = titlebar_rect
    278             .split_left_right_at_x(allocated_response.rect.left() + icon_width + padding_external);
    279         (
    280             allocated_response.with_new_rect(titlebar_rect),
    281             Some(back_button(ui, button_rect)),
    282         )
    283     } else {
    284         (allocated_response, None)
    285     };
    286 
    287     title(
    288         ui,
    289         title_name,
    290         titlebar_resp.rect,
    291         icon_width,
    292         if has_back {
    293             padding_internal
    294         } else {
    295             padding_external
    296         },
    297     );
    298 
    299     let delete_button_resp = delete_column_button(ui, titlebar_resp, icon_width, padding_external);
    300     let title_response = if delete_button_resp.clicked() {
    301         Some(TitleResponse::RemoveColumn)
    302     } else {
    303         None
    304     };
    305 
    306     let titlebar_resp = TitleBarResponse {
    307         title_response,
    308         go_back: maybe_button_resp.map_or(false, |r| r.clicked()),
    309     };
    310 
    311     InnerResponse::new(titlebar_resp, delete_button_resp)
    312 }
    313 
    314 fn back_button(ui: &mut egui::Ui, button_rect: egui::Rect) -> egui::Response {
    315     let horizontal_length = 10.0;
    316     let arrow_length = 5.0;
    317 
    318     let helper = AnimationHelper::new_from_rect(ui, "note-compose-button", button_rect);
    319     let painter = ui.painter_at(helper.get_animation_rect());
    320     let stroke = Stroke::new(1.5, ui.visuals().text_color());
    321 
    322     // Horizontal segment
    323     let left_horizontal_point = pos2(-horizontal_length / 2., 0.);
    324     let right_horizontal_point = pos2(horizontal_length / 2., 0.);
    325     let scaled_left_horizontal_point = helper.scale_pos_from_center(left_horizontal_point);
    326     let scaled_right_horizontal_point = helper.scale_pos_from_center(right_horizontal_point);
    327 
    328     painter.line_segment(
    329         [scaled_left_horizontal_point, scaled_right_horizontal_point],
    330         stroke,
    331     );
    332 
    333     // Top Arrow
    334     let sqrt_2_over_2 = std::f32::consts::SQRT_2 / 2.;
    335     let right_top_arrow_point = helper.scale_pos_from_center(pos2(
    336         left_horizontal_point.x + (sqrt_2_over_2 * arrow_length),
    337         right_horizontal_point.y + sqrt_2_over_2 * arrow_length,
    338     ));
    339 
    340     let scaled_left_arrow_point = scaled_left_horizontal_point;
    341     painter.line_segment([scaled_left_arrow_point, right_top_arrow_point], stroke);
    342 
    343     let right_bottom_arrow_point = helper.scale_pos_from_center(pos2(
    344         left_horizontal_point.x + (sqrt_2_over_2 * arrow_length),
    345         right_horizontal_point.y - sqrt_2_over_2 * arrow_length,
    346     ));
    347 
    348     painter.line_segment([scaled_left_arrow_point, right_bottom_arrow_point], stroke);
    349 
    350     helper.take_animation_response()
    351 }
    352 
    353 fn delete_column_button(
    354     ui: &mut egui::Ui,
    355     allocation_response: egui::Response,
    356     icon_width: f32,
    357     padding: f32,
    358 ) -> egui::Response {
    359     let img_size = 16.0;
    360     let max_size = icon_width * ICON_EXPANSION_MULTIPLE;
    361 
    362     let img_data = egui::include_image!("../assets/icons/column_delete_icon_4x.png");
    363     let img = egui::Image::new(img_data).max_width(img_size);
    364 
    365     let button_rect = {
    366         let titlebar_rect = allocation_response.rect;
    367         let titlebar_width = titlebar_rect.width();
    368         let titlebar_center = titlebar_rect.center();
    369         let button_center_y = titlebar_center.y;
    370         let button_center_x =
    371             titlebar_center.x + (titlebar_width / 2.0) - (max_size / 2.0) - padding;
    372         egui::Rect::from_center_size(
    373             pos2(button_center_x, button_center_y),
    374             egui::vec2(max_size, max_size),
    375         )
    376     };
    377 
    378     let helper = AnimationHelper::new_from_rect(ui, "delete-column-button", button_rect);
    379 
    380     let cur_img_size = helper.scale_1d_pos(img_size);
    381 
    382     let animation_rect = helper.get_animation_rect();
    383     let animation_resp = helper.take_animation_response();
    384     if allocation_response.union(animation_resp.clone()).hovered() {
    385         img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0));
    386     }
    387 
    388     animation_resp
    389 }
    390 
    391 fn title(
    392     ui: &mut egui::Ui,
    393     title_name: String,
    394     titlebar_rect: egui::Rect,
    395     icon_width: f32,
    396     padding: f32,
    397 ) {
    398     let painter = ui.painter_at(titlebar_rect);
    399 
    400     let font = egui::FontId::new(
    401         get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
    402         egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
    403     );
    404 
    405     let max_title_width = titlebar_rect.width() - icon_width - padding * 2.;
    406     let title_galley =
    407         ui.fonts(|f| f.layout(title_name, font, ui.visuals().text_color(), max_title_width));
    408 
    409     let pos = {
    410         let titlebar_center = titlebar_rect.center();
    411         let text_height = title_galley.rect.height();
    412 
    413         let galley_pos_x = titlebar_rect.left() + padding;
    414         let galley_pos_y = titlebar_center.y - (text_height / 2.);
    415         pos2(galley_pos_x, galley_pos_y)
    416     };
    417 
    418     painter.galley(pos, title_galley, Color32::WHITE);
    419 }
    420 
    421 enum TitleResponse {
    422     RemoveColumn,
    423 }