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 }