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, ¬e) 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, ¬e); 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 }