notedeck

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

timeline.rs (8548B)


      1 use crate::actionbar::NoteAction;
      2 use crate::timeline::TimelineTab;
      3 use crate::{
      4     column::Columns,
      5     timeline::{TimelineId, ViewFilter},
      6     ui,
      7     ui::note::NoteOptions,
      8 };
      9 use egui::containers::scroll_area::ScrollBarVisibility;
     10 use egui::{Direction, Layout};
     11 use egui_tabs::TabColor;
     12 use nostrdb::{Ndb, Transaction};
     13 use notedeck::{ImageCache, NoteCache};
     14 use tracing::{error, warn};
     15 
     16 pub struct TimelineView<'a> {
     17     timeline_id: TimelineId,
     18     columns: &'a mut Columns,
     19     ndb: &'a Ndb,
     20     note_cache: &'a mut NoteCache,
     21     img_cache: &'a mut ImageCache,
     22     note_options: NoteOptions,
     23     reverse: bool,
     24 }
     25 
     26 impl<'a> TimelineView<'a> {
     27     pub fn new(
     28         timeline_id: TimelineId,
     29         columns: &'a mut Columns,
     30         ndb: &'a Ndb,
     31         note_cache: &'a mut NoteCache,
     32         img_cache: &'a mut ImageCache,
     33         note_options: NoteOptions,
     34     ) -> TimelineView<'a> {
     35         let reverse = false;
     36         TimelineView {
     37             ndb,
     38             timeline_id,
     39             columns,
     40             note_cache,
     41             img_cache,
     42             reverse,
     43             note_options,
     44         }
     45     }
     46 
     47     pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
     48         timeline_ui(
     49             ui,
     50             self.ndb,
     51             self.timeline_id,
     52             self.columns,
     53             self.note_cache,
     54             self.img_cache,
     55             self.reverse,
     56             self.note_options,
     57         )
     58     }
     59 
     60     pub fn reversed(mut self) -> Self {
     61         self.reverse = true;
     62         self
     63     }
     64 }
     65 
     66 #[allow(clippy::too_many_arguments)]
     67 fn timeline_ui(
     68     ui: &mut egui::Ui,
     69     ndb: &Ndb,
     70     timeline_id: TimelineId,
     71     columns: &mut Columns,
     72     note_cache: &mut NoteCache,
     73     img_cache: &mut ImageCache,
     74     reversed: bool,
     75     note_options: NoteOptions,
     76 ) -> Option<NoteAction> {
     77     //padding(4.0, ui, |ui| ui.heading("Notifications"));
     78     /*
     79     let font_id = egui::TextStyle::Body.resolve(ui.style());
     80     let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
     81 
     82     */
     83 
     84     let scroll_id = {
     85         let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) {
     86             timeline
     87         } else {
     88             error!("tried to render timeline in column, but timeline was missing");
     89             // TODO (jb55): render error when timeline is missing?
     90             // this shouldn't happen...
     91             return None;
     92         };
     93 
     94         timeline.selected_view = tabs_ui(ui, timeline.selected_view, &timeline.views);
     95 
     96         // need this for some reason??
     97         ui.add_space(3.0);
     98 
     99         egui::Id::new(("tlscroll", timeline.view_id()))
    100     };
    101 
    102     egui::ScrollArea::vertical()
    103         .id_salt(scroll_id)
    104         .animated(false)
    105         .auto_shrink([false, false])
    106         .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
    107         .show(ui, |ui| {
    108             let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) {
    109                 timeline
    110             } else {
    111                 error!("tried to render timeline in column, but timeline was missing");
    112                 // TODO (jb55): render error when timeline is missing?
    113                 // this shouldn't happen...
    114                 return None;
    115             };
    116 
    117             let txn = Transaction::new(ndb).expect("failed to create txn");
    118             TimelineTabView::new(
    119                 timeline.current_view(),
    120                 reversed,
    121                 note_options,
    122                 &txn,
    123                 ndb,
    124                 note_cache,
    125                 img_cache,
    126             )
    127             .show(ui)
    128         })
    129         .inner
    130 }
    131 
    132 pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usize {
    133     ui.spacing_mut().item_spacing.y = 0.0;
    134 
    135     let tab_res = egui_tabs::Tabs::new(views.len() as i32)
    136         .selected(selected as i32)
    137         .hover_bg(TabColor::none())
    138         .selected_fg(TabColor::none())
    139         .selected_bg(TabColor::none())
    140         .hover_bg(TabColor::none())
    141         //.hover_bg(TabColor::custom(egui::Color32::RED))
    142         .height(32.0)
    143         .layout(Layout::centered_and_justified(Direction::TopDown))
    144         .show(ui, |ui, state| {
    145             ui.spacing_mut().item_spacing.y = 0.0;
    146 
    147             let ind = state.index();
    148 
    149             let txt = match views[ind as usize].filter {
    150                 ViewFilter::Notes => "Notes",
    151                 ViewFilter::NotesAndReplies => "Notes & Replies",
    152             };
    153 
    154             let res = ui.add(egui::Label::new(txt).selectable(false));
    155 
    156             // underline
    157             if state.is_selected() {
    158                 let rect = res.rect;
    159                 let underline =
    160                     shrink_range_to_width(rect.x_range(), get_label_width(ui, txt) * 1.15);
    161                 let underline_y = ui.painter().round_to_pixel(rect.bottom()) - 1.5;
    162                 return (underline, underline_y);
    163             }
    164 
    165             (egui::Rangef::new(0.0, 0.0), 0.0)
    166         });
    167 
    168     //ui.add_space(0.5);
    169     ui::hline(ui);
    170 
    171     let sel = tab_res.selected().unwrap_or_default();
    172 
    173     let (underline, underline_y) = tab_res.inner()[sel as usize].inner;
    174     let underline_width = underline.span();
    175 
    176     let tab_anim_id = ui.id().with("tab_anim");
    177     let tab_anim_size = tab_anim_id.with("size");
    178 
    179     let stroke = egui::Stroke {
    180         color: ui.visuals().hyperlink_color,
    181         width: 2.0,
    182     };
    183 
    184     let speed = 0.1f32;
    185 
    186     // animate underline position
    187     let x = ui
    188         .ctx()
    189         .animate_value_with_time(tab_anim_id, underline.min, speed);
    190 
    191     // animate underline width
    192     let w = ui
    193         .ctx()
    194         .animate_value_with_time(tab_anim_size, underline_width, speed);
    195 
    196     let underline = egui::Rangef::new(x, x + w);
    197 
    198     ui.painter().hline(underline, underline_y, stroke);
    199 
    200     sel as usize
    201 }
    202 
    203 fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 {
    204     let font_id = egui::FontId::default();
    205     let galley = ui.fonts(|r| r.layout_no_wrap(text.to_string(), font_id, egui::Color32::WHITE));
    206     galley.rect.width()
    207 }
    208 
    209 fn shrink_range_to_width(range: egui::Rangef, width: f32) -> egui::Rangef {
    210     let midpoint = (range.min + range.max) / 2.0;
    211     let half_width = width / 2.0;
    212 
    213     let min = midpoint - half_width;
    214     let max = midpoint + half_width;
    215 
    216     egui::Rangef::new(min, max)
    217 }
    218 
    219 pub struct TimelineTabView<'a> {
    220     tab: &'a TimelineTab,
    221     reversed: bool,
    222     note_options: NoteOptions,
    223     txn: &'a Transaction,
    224     ndb: &'a Ndb,
    225     note_cache: &'a mut NoteCache,
    226     img_cache: &'a mut ImageCache,
    227 }
    228 
    229 impl<'a> TimelineTabView<'a> {
    230     pub fn new(
    231         tab: &'a TimelineTab,
    232         reversed: bool,
    233         note_options: NoteOptions,
    234         txn: &'a Transaction,
    235         ndb: &'a Ndb,
    236         note_cache: &'a mut NoteCache,
    237         img_cache: &'a mut ImageCache,
    238     ) -> Self {
    239         Self {
    240             tab,
    241             reversed,
    242             txn,
    243             note_options,
    244             ndb,
    245             note_cache,
    246             img_cache,
    247         }
    248     }
    249 
    250     pub fn show(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
    251         let mut action: Option<NoteAction> = None;
    252         let len = self.tab.notes.len();
    253 
    254         self.tab
    255             .list
    256             .clone()
    257             .borrow_mut()
    258             .ui_custom_layout(ui, len, |ui, start_index| {
    259                 ui.spacing_mut().item_spacing.y = 0.0;
    260                 ui.spacing_mut().item_spacing.x = 4.0;
    261 
    262                 let ind = if self.reversed {
    263                     len - start_index - 1
    264                 } else {
    265                     start_index
    266                 };
    267 
    268                 let note_key = self.tab.notes[ind].key;
    269 
    270                 let note = if let Ok(note) = self.ndb.get_note_by_key(self.txn, note_key) {
    271                     note
    272                 } else {
    273                     warn!("failed to query note {:?}", note_key);
    274                     return 0;
    275                 };
    276 
    277                 ui::padding(8.0, ui, |ui| {
    278                     let resp = ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, &note)
    279                         .note_options(self.note_options)
    280                         .show(ui);
    281 
    282                     if let Some(note_action) = resp.action {
    283                         action = Some(note_action)
    284                     }
    285 
    286                     if let Some(context) = resp.context_selection {
    287                         context.process(ui, &note);
    288                     }
    289                 });
    290 
    291                 ui::hline(ui);
    292                 //ui.add(egui::Separator::default().spacing(0.0));
    293 
    294                 1
    295             });
    296 
    297         action
    298     }
    299 }