notedeck

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

header.rs (8127B)


      1 use crate::{
      2     app_style::NotedeckTextStyle,
      3     column::Columns,
      4     imgcache::ImageCache,
      5     nav::RenderNavAction,
      6     route::Route,
      7     timeline::{TimelineId, TimelineRoute},
      8     ui::{
      9         self,
     10         anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
     11     },
     12 };
     13 
     14 use egui::{RichText, Stroke, UiBuilder};
     15 use enostr::Pubkey;
     16 use nostrdb::{Ndb, Transaction};
     17 
     18 pub struct NavTitle<'a> {
     19     ndb: &'a Ndb,
     20     img_cache: &'a mut ImageCache,
     21     columns: &'a Columns,
     22     deck_author: Option<&'a Pubkey>,
     23     routes: &'a [Route],
     24 }
     25 
     26 impl<'a> NavTitle<'a> {
     27     pub fn new(
     28         ndb: &'a Ndb,
     29         img_cache: &'a mut ImageCache,
     30         columns: &'a Columns,
     31         deck_author: Option<&'a Pubkey>,
     32         routes: &'a [Route],
     33     ) -> Self {
     34         NavTitle {
     35             ndb,
     36             img_cache,
     37             columns,
     38             deck_author,
     39             routes,
     40         }
     41     }
     42 
     43     pub fn show(&mut self, ui: &mut egui::Ui) -> Option<RenderNavAction> {
     44         ui::padding(8.0, ui, |ui| {
     45             let mut rect = ui.available_rect_before_wrap();
     46             rect.set_height(48.0);
     47 
     48             let mut child_ui = ui.new_child(
     49                 UiBuilder::new()
     50                     .max_rect(rect)
     51                     .layout(egui::Layout::left_to_right(egui::Align::Center)),
     52             );
     53 
     54             let r = self.title_bar(&mut child_ui);
     55 
     56             ui.advance_cursor_after_rect(rect);
     57 
     58             r
     59         })
     60         .inner
     61     }
     62 
     63     fn title_bar(&mut self, ui: &mut egui::Ui) -> Option<RenderNavAction> {
     64         let item_spacing = 8.0;
     65         ui.spacing_mut().item_spacing.x = item_spacing;
     66 
     67         let chev_x = 8.0;
     68         let back_button_resp =
     69             prev(self.routes).map(|r| self.back_button(ui, r, egui::Vec2::new(chev_x, 15.0)));
     70 
     71         // add some space where chevron would have been. this makes the ui
     72         // less bumpy when navigating
     73         if back_button_resp.is_none() {
     74             ui.add_space(chev_x + item_spacing);
     75         }
     76 
     77         let delete_button_resp =
     78             self.title(ui, self.routes.last().unwrap(), back_button_resp.is_some());
     79 
     80         if delete_button_resp.map_or(false, |r| r.clicked()) {
     81             Some(RenderNavAction::RemoveColumn)
     82         } else if back_button_resp.map_or(false, |r| r.clicked()) {
     83             Some(RenderNavAction::Back)
     84         } else {
     85             None
     86         }
     87     }
     88 
     89     fn back_button(
     90         &mut self,
     91         ui: &mut egui::Ui,
     92         prev: &Route,
     93         chev_size: egui::Vec2,
     94     ) -> egui::Response {
     95         //let color = ui.visuals().hyperlink_color;
     96         let color = ui.style().visuals.noninteractive().fg_stroke.color;
     97 
     98         //let spacing_prev = ui.spacing().item_spacing.x;
     99         //ui.spacing_mut().item_spacing.x = 0.0;
    100 
    101         let chev_resp = chevron(ui, 2.0, chev_size, Stroke::new(2.0, color));
    102 
    103         //ui.spacing_mut().item_spacing.x = spacing_prev;
    104 
    105         // NOTE(jb55): include graphic in back label as well because why
    106         // not it looks cool
    107         self.title_pfp(ui, prev, 32.0);
    108 
    109         let back_label = ui.add(
    110             egui::Label::new(
    111                 RichText::new(prev.title(self.columns).to_string())
    112                     .color(color)
    113                     .text_style(NotedeckTextStyle::Body.text_style()),
    114             )
    115             .selectable(false)
    116             .sense(egui::Sense::click()),
    117         );
    118 
    119         back_label.union(chev_resp)
    120     }
    121 
    122     fn delete_column_button(&self, ui: &mut egui::Ui, icon_width: f32) -> egui::Response {
    123         let img_size = 16.0;
    124         let max_size = icon_width * ICON_EXPANSION_MULTIPLE;
    125 
    126         let img_data = if ui.visuals().dark_mode {
    127             egui::include_image!("../../../assets/icons/column_delete_icon_4x.png")
    128         } else {
    129             egui::include_image!("../../../assets/icons/column_delete_icon_light_4x.png")
    130         };
    131         let img = egui::Image::new(img_data).max_width(img_size);
    132 
    133         let helper =
    134             AnimationHelper::new(ui, "delete-column-button", egui::vec2(max_size, max_size));
    135 
    136         let cur_img_size = helper.scale_1d_pos_min_max(0.0, img_size);
    137 
    138         let animation_rect = helper.get_animation_rect();
    139         let animation_resp = helper.take_animation_response();
    140 
    141         img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0));
    142 
    143         animation_resp
    144     }
    145 
    146     fn pubkey_pfp<'txn, 'me>(
    147         &'me mut self,
    148         txn: &'txn Transaction,
    149         pubkey: &[u8; 32],
    150         pfp_size: f32,
    151     ) -> Option<ui::ProfilePic<'me, 'txn>> {
    152         self.ndb
    153             .get_profile_by_pubkey(txn, pubkey)
    154             .as_ref()
    155             .ok()
    156             .and_then(move |p| {
    157                 Some(ui::ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size))
    158             })
    159     }
    160 
    161     fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: TimelineId, pfp_size: f32) {
    162         let txn = Transaction::new(self.ndb).unwrap();
    163 
    164         if let Some(pfp) = self
    165             .columns
    166             .find_timeline(id)
    167             .and_then(|tl| tl.kind.pubkey_source())
    168             .and_then(|pksrc| self.deck_author.map(|da| pksrc.to_pubkey(da)))
    169             .and_then(|pk| self.pubkey_pfp(&txn, pk.bytes(), pfp_size))
    170         {
    171             ui.add(pfp);
    172         } else {
    173             ui.add(
    174                 ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size),
    175             );
    176         }
    177     }
    178 
    179     fn title_pfp(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) {
    180         match top {
    181             Route::Timeline(tlr) => match tlr {
    182                 TimelineRoute::Timeline(tlid) => {
    183                     self.timeline_pfp(ui, *tlid, pfp_size);
    184                 }
    185 
    186                 TimelineRoute::Thread(_note_id) => {}
    187                 TimelineRoute::Reply(_note_id) => {}
    188                 TimelineRoute::Quote(_note_id) => {}
    189 
    190                 TimelineRoute::Profile(pubkey) => {
    191                     let txn = Transaction::new(self.ndb).unwrap();
    192                     if let Some(pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) {
    193                         ui.add(pfp);
    194                     } else {
    195                         ui.add(
    196                             ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url())
    197                                 .size(pfp_size),
    198                         );
    199                     }
    200                 }
    201             },
    202 
    203             Route::Accounts(_as) => {}
    204             Route::ComposeNote => {}
    205             Route::AddColumn(_add_col_route) => {}
    206             Route::Support => {}
    207             Route::Relays => {}
    208         }
    209     }
    210 
    211     fn title_label(&self, ui: &mut egui::Ui, top: &Route) {
    212         ui.add(
    213             egui::Label::new(
    214                 RichText::new(top.title(self.columns))
    215                     .text_style(NotedeckTextStyle::Body.text_style()),
    216             )
    217             .selectable(false),
    218         );
    219     }
    220 
    221     fn title(
    222         &mut self,
    223         ui: &mut egui::Ui,
    224         top: &Route,
    225         navigating: bool,
    226     ) -> Option<egui::Response> {
    227         if !navigating {
    228             self.title_pfp(ui, top, 32.0);
    229             self.title_label(ui, top);
    230         }
    231 
    232         ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
    233             if navigating {
    234                 self.title_label(ui, top);
    235                 self.title_pfp(ui, top, 32.0);
    236                 None
    237             } else {
    238                 Some(self.delete_column_button(ui, 32.0))
    239             }
    240         })
    241         .inner
    242     }
    243 }
    244 
    245 fn prev<R>(xs: &[R]) -> Option<&R> {
    246     xs.get(xs.len().checked_sub(2)?)
    247 }
    248 
    249 fn chevron(
    250     ui: &mut egui::Ui,
    251     pad: f32,
    252     size: egui::Vec2,
    253     stroke: impl Into<Stroke>,
    254 ) -> egui::Response {
    255     let (r, painter) = ui.allocate_painter(size, egui::Sense::click());
    256 
    257     let min = r.rect.min;
    258     let max = r.rect.max;
    259 
    260     let apex = egui::Pos2::new(min.x + pad, min.y + size.y / 2.0);
    261     let top = egui::Pos2::new(max.x - pad, min.y + pad);
    262     let bottom = egui::Pos2::new(max.x - pad, max.y - pad);
    263 
    264     let stroke = stroke.into();
    265     painter.line_segment([apex, top], stroke);
    266     painter.line_segment([apex, bottom], stroke);
    267 
    268     r
    269 }