notedeck

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

timeline.rs (8501B)


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