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