side_panel.rs (15883B)
1 use egui::{vec2, Color32, InnerResponse, Layout, Margin, Separator, Stroke, Widget}; 2 use tracing::info; 3 4 use crate::{ 5 account_manager::AccountsRoute, 6 colors, 7 column::{Column, Columns}, 8 imgcache::ImageCache, 9 route::Route, 10 support::Support, 11 user_account::UserAccount, 12 Damus, 13 }; 14 15 use super::{ 16 anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, 17 profile::preview::get_account_url, 18 ProfilePic, View, 19 }; 20 21 pub static SIDE_PANEL_WIDTH: f32 = 64.0; 22 static ICON_WIDTH: f32 = 40.0; 23 24 pub struct DesktopSidePanel<'a> { 25 ndb: &'a nostrdb::Ndb, 26 img_cache: &'a mut ImageCache, 27 selected_account: Option<&'a UserAccount>, 28 } 29 30 impl<'a> View for DesktopSidePanel<'a> { 31 fn ui(&mut self, ui: &mut egui::Ui) { 32 self.show(ui); 33 } 34 } 35 36 #[derive(Debug, Eq, PartialEq, Clone, Copy)] 37 pub enum SidePanelAction { 38 Panel, 39 Account, 40 Settings, 41 Columns, 42 ComposeNote, 43 Search, 44 ExpandSidePanel, 45 Support, 46 } 47 48 pub struct SidePanelResponse { 49 pub response: egui::Response, 50 pub action: SidePanelAction, 51 } 52 53 impl SidePanelResponse { 54 fn new(action: SidePanelAction, response: egui::Response) -> Self { 55 SidePanelResponse { action, response } 56 } 57 } 58 59 impl<'a> DesktopSidePanel<'a> { 60 pub fn new( 61 ndb: &'a nostrdb::Ndb, 62 img_cache: &'a mut ImageCache, 63 selected_account: Option<&'a UserAccount>, 64 ) -> Self { 65 Self { 66 ndb, 67 img_cache, 68 selected_account, 69 } 70 } 71 72 pub fn show(&mut self, ui: &mut egui::Ui) -> SidePanelResponse { 73 egui::Frame::none() 74 .inner_margin(Margin::same(8.0)) 75 .show(ui, |ui| self.show_inner(ui)) 76 .inner 77 } 78 79 fn show_inner(&mut self, ui: &mut egui::Ui) -> SidePanelResponse { 80 let dark_mode = ui.ctx().style().visuals.dark_mode; 81 82 let inner = ui 83 .vertical(|ui| { 84 let top_resp = ui 85 .with_layout(Layout::top_down(egui::Align::Center), |ui| { 86 let expand_resp = ui.add(expand_side_panel_button()); 87 ui.add_space(28.0); 88 let compose_resp = ui.add(compose_note_button()); 89 let search_resp = ui.add(search_button()); 90 let column_resp = ui.add(add_column_button(dark_mode)); 91 92 ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0)); 93 94 if expand_resp.clicked() { 95 Some(InnerResponse::new( 96 SidePanelAction::ExpandSidePanel, 97 expand_resp, 98 )) 99 } else if compose_resp.clicked() { 100 Some(InnerResponse::new( 101 SidePanelAction::ComposeNote, 102 compose_resp, 103 )) 104 } else if search_resp.clicked() { 105 Some(InnerResponse::new(SidePanelAction::Search, search_resp)) 106 } else if column_resp.clicked() { 107 Some(InnerResponse::new(SidePanelAction::Columns, column_resp)) 108 } else { 109 None 110 } 111 }) 112 .inner; 113 114 let (pfp_resp, bottom_resp) = ui 115 .with_layout(Layout::bottom_up(egui::Align::Center), |ui| { 116 let pfp_resp = self.pfp_button(ui); 117 let settings_resp = ui.add(settings_button(dark_mode)); 118 119 let support_resp = ui.add(support_button()); 120 121 let optional_inner = if pfp_resp.clicked() { 122 Some(egui::InnerResponse::new( 123 SidePanelAction::Account, 124 pfp_resp.clone(), 125 )) 126 } else if settings_resp.clicked() || settings_resp.hovered() { 127 Some(egui::InnerResponse::new( 128 SidePanelAction::Settings, 129 settings_resp, 130 )) 131 } else if support_resp.clicked() { 132 Some(egui::InnerResponse::new( 133 SidePanelAction::Support, 134 support_resp, 135 )) 136 } else { 137 None 138 }; 139 140 (pfp_resp, optional_inner) 141 }) 142 .inner; 143 144 if let Some(bottom_inner) = bottom_resp { 145 bottom_inner 146 } else if let Some(top_inner) = top_resp { 147 top_inner 148 } else { 149 egui::InnerResponse::new(SidePanelAction::Panel, pfp_resp) 150 } 151 }) 152 .inner; 153 154 SidePanelResponse::new(inner.inner, inner.response) 155 } 156 157 fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response { 158 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 159 let helper = AnimationHelper::new(ui, "pfp-button", vec2(max_size, max_size)); 160 161 let min_pfp_size = ICON_WIDTH; 162 let cur_pfp_size = helper.scale_1d_pos(min_pfp_size); 163 164 let txn = nostrdb::Transaction::new(self.ndb).expect("should be able to create txn"); 165 let profile_url = get_account_url(&txn, self.ndb, self.selected_account); 166 167 let widget = ProfilePic::new(self.img_cache, profile_url).size(cur_pfp_size); 168 169 ui.put(helper.get_animation_rect(), widget); 170 171 helper.take_animation_response() 172 } 173 174 pub fn perform_action(columns: &mut Columns, support: &mut Support, action: SidePanelAction) { 175 let router = columns.get_first_router(); 176 match action { 177 SidePanelAction::Panel => {} // TODO 178 SidePanelAction::Account => { 179 if router 180 .routes() 181 .iter() 182 .any(|&r| r == Route::Accounts(AccountsRoute::Accounts)) 183 { 184 // return if we are already routing to accounts 185 router.go_back(); 186 } else { 187 router.route_to(Route::accounts()); 188 } 189 } 190 SidePanelAction::Settings => { 191 if router.routes().iter().any(|&r| r == Route::Relays) { 192 // return if we are already routing to accounts 193 router.go_back(); 194 } else { 195 router.route_to(Route::relays()); 196 } 197 } 198 SidePanelAction::Columns => { 199 if router 200 .routes() 201 .iter() 202 .any(|&r| matches!(r, Route::AddColumn(_))) 203 { 204 router.go_back(); 205 } else { 206 columns.new_column_picker(); 207 } 208 } 209 SidePanelAction::ComposeNote => { 210 if router.routes().iter().any(|&r| r == Route::ComposeNote) { 211 router.go_back(); 212 } else { 213 router.route_to(Route::ComposeNote); 214 } 215 } 216 SidePanelAction::Search => { 217 // TODO 218 info!("Clicked search button"); 219 } 220 SidePanelAction::ExpandSidePanel => { 221 // TODO 222 info!("Clicked expand side panel button"); 223 } 224 SidePanelAction::Support => { 225 if router.routes().iter().any(|&r| r == Route::Support) { 226 router.go_back(); 227 } else { 228 support.refresh(); 229 router.route_to(Route::Support); 230 } 231 } 232 } 233 } 234 } 235 236 fn settings_button(dark_mode: bool) -> impl Widget { 237 let _ = dark_mode; 238 |ui: &mut egui::Ui| { 239 let img_size = 24.0; 240 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 241 let img_data = egui::include_image!("../../assets/icons/settings_dark_4x.png"); 242 let img = egui::Image::new(img_data).max_width(img_size); 243 244 let helper = AnimationHelper::new(ui, "settings-button", vec2(max_size, max_size)); 245 246 let cur_img_size = helper.scale_1d_pos(img_size); 247 img.paint_at( 248 ui, 249 helper 250 .get_animation_rect() 251 .shrink((max_size - cur_img_size) / 2.0), 252 ); 253 254 helper.take_animation_response() 255 } 256 } 257 258 fn add_column_button(dark_mode: bool) -> impl Widget { 259 let _ = dark_mode; 260 move |ui: &mut egui::Ui| { 261 let img_size = 24.0; 262 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 263 264 let img_data = egui::include_image!("../../assets/icons/add_column_dark_4x.png"); 265 266 let img = egui::Image::new(img_data).max_width(img_size); 267 268 let helper = AnimationHelper::new(ui, "add-column-button", vec2(max_size, max_size)); 269 270 let cur_img_size = helper.scale_1d_pos(img_size); 271 img.paint_at( 272 ui, 273 helper 274 .get_animation_rect() 275 .shrink((max_size - cur_img_size) / 2.0), 276 ); 277 278 helper.take_animation_response() 279 } 280 } 281 282 fn compose_note_button() -> impl Widget { 283 |ui: &mut egui::Ui| -> egui::Response { 284 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 285 286 let min_outer_circle_diameter = 40.0; 287 let min_plus_sign_size = 14.0; // length of the plus sign 288 let min_line_width = 2.25; // width of the plus sign 289 290 let helper = AnimationHelper::new(ui, "note-compose-button", vec2(max_size, max_size)); 291 292 let painter = ui.painter_at(helper.get_animation_rect()); 293 294 let use_background_radius = helper.scale_radius(min_outer_circle_diameter); 295 let use_line_width = helper.scale_1d_pos(min_line_width); 296 let use_edge_circle_radius = helper.scale_radius(min_line_width); 297 298 painter.circle_filled(helper.center(), use_background_radius, colors::PINK); 299 300 let min_half_plus_sign_size = min_plus_sign_size / 2.0; 301 let north_edge = helper.scale_from_center(0.0, min_half_plus_sign_size); 302 let south_edge = helper.scale_from_center(0.0, -min_half_plus_sign_size); 303 let west_edge = helper.scale_from_center(-min_half_plus_sign_size, 0.0); 304 let east_edge = helper.scale_from_center(min_half_plus_sign_size, 0.0); 305 306 painter.line_segment( 307 [north_edge, south_edge], 308 Stroke::new(use_line_width, Color32::WHITE), 309 ); 310 painter.line_segment( 311 [west_edge, east_edge], 312 Stroke::new(use_line_width, Color32::WHITE), 313 ); 314 painter.circle_filled(north_edge, use_edge_circle_radius, Color32::WHITE); 315 painter.circle_filled(south_edge, use_edge_circle_radius, Color32::WHITE); 316 painter.circle_filled(west_edge, use_edge_circle_radius, Color32::WHITE); 317 painter.circle_filled(east_edge, use_edge_circle_radius, Color32::WHITE); 318 319 helper.take_animation_response() 320 } 321 } 322 323 fn search_button() -> impl Widget { 324 |ui: &mut egui::Ui| -> egui::Response { 325 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 326 let min_line_width_circle = 1.5; // width of the magnifying glass 327 let min_line_width_handle = 1.5; 328 let helper = AnimationHelper::new(ui, "search-button", vec2(max_size, max_size)); 329 330 let painter = ui.painter_at(helper.get_animation_rect()); 331 332 let cur_line_width_circle = helper.scale_1d_pos(min_line_width_circle); 333 let cur_line_width_handle = helper.scale_1d_pos(min_line_width_handle); 334 let min_outer_circle_radius = helper.scale_radius(15.0); 335 let cur_outer_circle_radius = helper.scale_1d_pos(min_outer_circle_radius); 336 let min_handle_length = 7.0; 337 let cur_handle_length = helper.scale_1d_pos(min_handle_length); 338 339 let circle_center = helper.scale_from_center(-2.0, -2.0); 340 341 let handle_vec = vec2( 342 std::f32::consts::FRAC_1_SQRT_2, 343 std::f32::consts::FRAC_1_SQRT_2, 344 ); 345 346 let handle_pos_1 = circle_center + (handle_vec * (cur_outer_circle_radius - 3.0)); 347 let handle_pos_2 = 348 circle_center + (handle_vec * (cur_outer_circle_radius + cur_handle_length)); 349 350 let circle_stroke = Stroke::new(cur_line_width_circle, colors::MID_GRAY); 351 let handle_stroke = Stroke::new(cur_line_width_handle, colors::MID_GRAY); 352 353 painter.line_segment([handle_pos_1, handle_pos_2], handle_stroke); 354 painter.circle( 355 circle_center, 356 min_outer_circle_radius, 357 ui.style().visuals.widgets.inactive.weak_bg_fill, 358 circle_stroke, 359 ); 360 361 helper.take_animation_response() 362 } 363 } 364 365 // TODO: convert to responsive button when expanded side panel impl is finished 366 fn expand_side_panel_button() -> impl Widget { 367 |ui: &mut egui::Ui| -> egui::Response { 368 let img_size = 40.0; 369 let img_data = egui::include_image!("../../assets/damus_rounded_80.png"); 370 let img = egui::Image::new(img_data).max_width(img_size); 371 372 ui.add(img) 373 } 374 } 375 376 fn support_button() -> impl Widget { 377 |ui: &mut egui::Ui| -> egui::Response { 378 let img_size = 16.0; 379 380 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 381 let img_data = egui::include_image!("../../assets/icons/help_icon_dark_4x.png"); 382 let img = egui::Image::new(img_data).max_width(img_size); 383 384 let helper = AnimationHelper::new(ui, "help-button", vec2(max_size, max_size)); 385 386 let cur_img_size = helper.scale_1d_pos(img_size); 387 img.paint_at( 388 ui, 389 helper 390 .get_animation_rect() 391 .shrink((max_size - cur_img_size) / 2.0), 392 ); 393 394 helper.take_animation_response() 395 } 396 } 397 398 mod preview { 399 400 use egui_extras::{Size, StripBuilder}; 401 402 use crate::{ 403 test_data, 404 ui::{Preview, PreviewConfig}, 405 }; 406 407 use super::*; 408 409 pub struct DesktopSidePanelPreview { 410 app: Damus, 411 } 412 413 impl DesktopSidePanelPreview { 414 fn new() -> Self { 415 let mut app = test_data::test_app(); 416 app.columns.add_column(Column::new(vec![Route::accounts()])); 417 DesktopSidePanelPreview { app } 418 } 419 } 420 421 impl View for DesktopSidePanelPreview { 422 fn ui(&mut self, ui: &mut egui::Ui) { 423 StripBuilder::new(ui) 424 .size(Size::exact(SIDE_PANEL_WIDTH)) 425 .sizes(Size::remainder(), 0) 426 .clip(true) 427 .horizontal(|mut strip| { 428 strip.cell(|ui| { 429 let mut panel = DesktopSidePanel::new( 430 &self.app.ndb, 431 &mut self.app.img_cache, 432 self.app.accounts.get_selected_account(), 433 ); 434 let response = panel.show(ui); 435 436 DesktopSidePanel::perform_action( 437 &mut self.app.columns, 438 &mut self.app.support, 439 response.action, 440 ); 441 }); 442 }); 443 } 444 } 445 446 impl<'a> Preview for DesktopSidePanel<'a> { 447 type Prev = DesktopSidePanelPreview; 448 449 fn preview(_cfg: PreviewConfig) -> Self::Prev { 450 DesktopSidePanelPreview::new() 451 } 452 } 453 }