side_panel.rs (15512B)
1 use egui::{ 2 vec2, CursorIcon, InnerResponse, Layout, Margin, RichText, ScrollArea, Separator, Stroke, 3 Widget, 4 }; 5 use tracing::{error, info}; 6 7 use crate::{ 8 app::{get_active_columns_mut, get_decks_mut}, 9 app_style::DECK_ICON_SIZE, 10 decks::{DecksAction, DecksCache}, 11 nav::SwitchingAction, 12 route::Route, 13 }; 14 15 use notedeck::{tr, Accounts, Localization, UserAccount}; 16 use notedeck_ui::{ 17 anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, 18 app_images, colors, View, 19 }; 20 21 use super::configure_deck::deck_icon; 22 23 pub static SIDE_PANEL_WIDTH: f32 = 68.0; 24 static ICON_WIDTH: f32 = 40.0; 25 26 pub struct DesktopSidePanel<'a> { 27 selected_account: &'a UserAccount, 28 decks_cache: &'a DecksCache, 29 i18n: &'a mut Localization, 30 } 31 32 impl View for DesktopSidePanel<'_> { 33 fn ui(&mut self, ui: &mut egui::Ui) { 34 self.show(ui); 35 } 36 } 37 38 #[derive(Debug, Eq, PartialEq, Clone, Copy)] 39 pub enum SidePanelAction { 40 Columns, 41 ComposeNote, 42 Search, 43 ExpandSidePanel, 44 NewDeck, 45 SwitchDeck(usize), 46 EditDeck(usize), 47 Wallet, 48 } 49 50 pub struct SidePanelResponse { 51 pub response: egui::Response, 52 pub action: SidePanelAction, 53 } 54 55 impl SidePanelResponse { 56 fn new(action: SidePanelAction, response: egui::Response) -> Self { 57 SidePanelResponse { action, response } 58 } 59 } 60 61 impl<'a> DesktopSidePanel<'a> { 62 pub fn new( 63 selected_account: &'a UserAccount, 64 decks_cache: &'a DecksCache, 65 i18n: &'a mut Localization, 66 ) -> Self { 67 Self { 68 selected_account, 69 decks_cache, 70 i18n, 71 } 72 } 73 74 pub fn show(&mut self, ui: &mut egui::Ui) -> Option<SidePanelResponse> { 75 let frame = 76 egui::Frame::new().inner_margin(Margin::same(notedeck_ui::constants::FRAME_MARGIN)); 77 78 if !ui.visuals().dark_mode { 79 let rect = ui.available_rect_before_wrap(); 80 ui.painter().rect( 81 rect, 82 0, 83 colors::ALMOST_WHITE, 84 egui::Stroke::new(0.0, egui::Color32::TRANSPARENT), 85 egui::StrokeKind::Inside, 86 ); 87 } 88 89 frame.show(ui, |ui| self.show_inner(ui)).inner 90 } 91 92 fn show_inner(&mut self, ui: &mut egui::Ui) -> Option<SidePanelResponse> { 93 let dark_mode = ui.ctx().style().visuals.dark_mode; 94 95 let inner = ui 96 .vertical(|ui| { 97 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { 98 // macos needs a bit of space to make room for window 99 // minimize/close buttons 100 //if cfg!(target_os = "macos") { 101 // ui.add_space(24.0); 102 //} 103 104 let compose_resp = ui 105 .add(crate::ui::post::compose_note_button(dark_mode)) 106 .on_hover_cursor(egui::CursorIcon::PointingHand); 107 let search_resp = ui.add(search_button()); 108 let column_resp = ui.add(add_column_button()); 109 110 ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0)); 111 112 ui.add_space(8.0); 113 ui.add(egui::Label::new( 114 RichText::new(tr!( 115 self.i18n, 116 "DECKS", 117 "Label for decks section in side panel" 118 )) 119 .size(11.0) 120 .color(ui.visuals().noninteractive().fg_stroke.color), 121 )); 122 ui.add_space(8.0); 123 let add_deck_resp = ui.add(add_deck_button()); 124 125 let decks_inner = ScrollArea::vertical() 126 .max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0))) 127 .show(ui, |ui| { 128 show_decks(ui, self.decks_cache, self.selected_account) 129 }) 130 .inner; 131 132 /* 133 if expand_resp.clicked() { 134 Some(InnerResponse::new( 135 SidePanelAction::ExpandSidePanel, 136 expand_resp, 137 )) 138 */ 139 if compose_resp.clicked() { 140 Some(InnerResponse::new( 141 SidePanelAction::ComposeNote, 142 compose_resp, 143 )) 144 } else if search_resp.clicked() { 145 Some(InnerResponse::new(SidePanelAction::Search, search_resp)) 146 } else if column_resp.clicked() { 147 Some(InnerResponse::new(SidePanelAction::Columns, column_resp)) 148 } else if add_deck_resp.clicked() { 149 Some(InnerResponse::new(SidePanelAction::NewDeck, add_deck_resp)) 150 } else if decks_inner.response.secondary_clicked() { 151 info!("decks inner secondary click"); 152 if let Some(clicked_index) = decks_inner.inner { 153 Some(InnerResponse::new( 154 SidePanelAction::EditDeck(clicked_index), 155 decks_inner.response, 156 )) 157 } else { 158 None 159 } 160 } else if decks_inner.response.clicked() { 161 if let Some(clicked_index) = decks_inner.inner { 162 Some(InnerResponse::new( 163 SidePanelAction::SwitchDeck(clicked_index), 164 decks_inner.response, 165 )) 166 } else { 167 None 168 } 169 } else { 170 None 171 } 172 }) 173 .inner 174 }) 175 .inner; 176 177 if let Some(inner) = inner { 178 Some(SidePanelResponse::new(inner.inner, inner.response)) 179 } else { 180 None 181 } 182 } 183 184 pub fn perform_action( 185 decks_cache: &mut DecksCache, 186 accounts: &Accounts, 187 action: SidePanelAction, 188 i18n: &mut Localization, 189 ) -> Option<SwitchingAction> { 190 let router = get_active_columns_mut(i18n, accounts, decks_cache).get_first_router(); 191 let mut switching_response = None; 192 match action { 193 /* 194 SidePanelAction::Panel => {} // TODO 195 SidePanelAction::Account => { 196 if router 197 .routes() 198 .iter() 199 .any(|r| r == &Route::Accounts(AccountsRoute::Accounts)) 200 { 201 // return if we are already routing to accounts 202 router.go_back(); 203 } else { 204 router.route_to(Route::accounts()); 205 } 206 } 207 SidePanelAction::Settings => { 208 if router.routes().iter().any(|r| r == &Route::Relays) { 209 // return if we are already routing to accounts 210 router.go_back(); 211 } else { 212 router.route_to(Route::relays()); 213 } 214 } 215 SidePanelAction::Support => { 216 if router.routes().iter().any(|r| r == &Route::Support) { 217 router.go_back(); 218 } else { 219 support.refresh(); 220 router.route_to(Route::Support); 221 } 222 } 223 */ 224 SidePanelAction::Columns => { 225 if router 226 .routes() 227 .iter() 228 .any(|r| matches!(r, Route::AddColumn(_))) 229 { 230 router.go_back(); 231 } else { 232 get_active_columns_mut(i18n, accounts, decks_cache).new_column_picker(); 233 } 234 } 235 SidePanelAction::ComposeNote => { 236 let can_post = accounts.get_selected_account().key.secret_key.is_some(); 237 238 if !can_post { 239 router.route_to(Route::accounts()); 240 } else if router.routes().iter().any(|r| r == &Route::ComposeNote) { 241 router.go_back(); 242 } else { 243 router.route_to(Route::ComposeNote); 244 } 245 } 246 SidePanelAction::Search => { 247 // TODO 248 if router.top() == &Route::Search { 249 router.go_back(); 250 } else { 251 router.route_to(Route::Search); 252 } 253 } 254 SidePanelAction::ExpandSidePanel => { 255 // TODO 256 info!("Clicked expand side panel button"); 257 } 258 SidePanelAction::NewDeck => { 259 if router.routes().iter().any(|r| r == &Route::NewDeck) { 260 router.go_back(); 261 } else { 262 router.route_to(Route::NewDeck); 263 } 264 } 265 SidePanelAction::SwitchDeck(index) => { 266 switching_response = Some(crate::nav::SwitchingAction::Decks(DecksAction::Switch( 267 index, 268 ))) 269 } 270 SidePanelAction::EditDeck(index) => { 271 if router.routes().iter().any(|r| r == &Route::EditDeck(index)) { 272 router.go_back(); 273 } else { 274 switching_response = Some(crate::nav::SwitchingAction::Decks( 275 DecksAction::Switch(index), 276 )); 277 if let Some(edit_deck) = get_decks_mut(i18n, accounts, decks_cache) 278 .decks_mut() 279 .get_mut(index) 280 { 281 edit_deck 282 .columns_mut() 283 .get_first_router() 284 .route_to(Route::EditDeck(index)); 285 } else { 286 error!("Cannot push EditDeck route to index {}", index); 287 } 288 } 289 } 290 SidePanelAction::Wallet => 's: { 291 if router 292 .routes() 293 .iter() 294 .any(|r| matches!(r, Route::Wallet(_))) 295 { 296 router.go_back(); 297 break 's; 298 } 299 300 router.route_to(Route::Wallet(notedeck::WalletType::Auto)); 301 } 302 } 303 switching_response 304 } 305 } 306 307 fn add_column_button() -> impl Widget { 308 move |ui: &mut egui::Ui| { 309 let img_size = 24.0; 310 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 311 312 let img = if ui.visuals().dark_mode { 313 app_images::add_column_dark_image() 314 } else { 315 app_images::add_column_light_image() 316 }; 317 318 let helper = AnimationHelper::new(ui, "add-column-button", vec2(max_size, max_size)); 319 320 let cur_img_size = helper.scale_1d_pos(img_size); 321 img.paint_at( 322 ui, 323 helper 324 .get_animation_rect() 325 .shrink((max_size - cur_img_size) / 2.0), 326 ); 327 328 helper 329 .take_animation_response() 330 .on_hover_cursor(CursorIcon::PointingHand) 331 .on_hover_text("Add new column") 332 } 333 } 334 335 pub fn search_button() -> impl Widget { 336 |ui: &mut egui::Ui| -> egui::Response { 337 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 338 let min_line_width_circle = 1.5; // width of the magnifying glass 339 let min_line_width_handle = 1.5; 340 let helper = AnimationHelper::new(ui, "search-button", vec2(max_size, max_size)); 341 342 let painter = ui.painter_at(helper.get_animation_rect()); 343 344 let cur_line_width_circle = helper.scale_1d_pos(min_line_width_circle); 345 let cur_line_width_handle = helper.scale_1d_pos(min_line_width_handle); 346 let min_outer_circle_radius = helper.scale_radius(15.0); 347 let cur_outer_circle_radius = helper.scale_1d_pos(min_outer_circle_radius); 348 let min_handle_length = 7.0; 349 let cur_handle_length = helper.scale_1d_pos(min_handle_length); 350 351 let circle_center = helper.scale_from_center(-2.0, -2.0); 352 353 let handle_vec = vec2( 354 std::f32::consts::FRAC_1_SQRT_2, 355 std::f32::consts::FRAC_1_SQRT_2, 356 ); 357 358 let handle_pos_1 = circle_center + (handle_vec * (cur_outer_circle_radius - 3.0)); 359 let handle_pos_2 = 360 circle_center + (handle_vec * (cur_outer_circle_radius + cur_handle_length)); 361 362 let circle_stroke = Stroke::new(cur_line_width_circle, colors::MID_GRAY); 363 let handle_stroke = Stroke::new(cur_line_width_handle, colors::MID_GRAY); 364 365 painter.line_segment([handle_pos_1, handle_pos_2], handle_stroke); 366 painter.circle( 367 circle_center, 368 min_outer_circle_radius, 369 ui.style().visuals.widgets.inactive.weak_bg_fill, 370 circle_stroke, 371 ); 372 373 helper 374 .take_animation_response() 375 .on_hover_cursor(CursorIcon::PointingHand) 376 .on_hover_text("Open search") 377 } 378 } 379 380 // TODO: convert to responsive button when expanded side panel impl is finished 381 382 fn add_deck_button() -> impl Widget { 383 |ui: &mut egui::Ui| -> egui::Response { 384 let img_size = 40.0; 385 386 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget 387 let img = app_images::new_deck_image().max_width(img_size); 388 389 let helper = AnimationHelper::new(ui, "new-deck-icon", vec2(max_size, max_size)); 390 391 let cur_img_size = helper.scale_1d_pos(img_size); 392 img.paint_at( 393 ui, 394 helper 395 .get_animation_rect() 396 .shrink((max_size - cur_img_size) / 2.0), 397 ); 398 399 helper 400 .take_animation_response() 401 .on_hover_cursor(CursorIcon::PointingHand) 402 .on_hover_text("Add new deck") 403 } 404 } 405 406 fn show_decks<'a>( 407 ui: &mut egui::Ui, 408 decks_cache: &'a DecksCache, 409 selected_account: &'a UserAccount, 410 ) -> InnerResponse<Option<usize>> { 411 let show_decks_id = ui.id().with("show-decks"); 412 let account_id = selected_account.key.pubkey; 413 let (cur_decks, account_id) = ( 414 decks_cache.decks(&account_id), 415 show_decks_id.with(account_id), 416 ); 417 let active_index = cur_decks.active_index(); 418 419 let (_, mut resp) = ui.allocate_exact_size(vec2(0.0, 0.0), egui::Sense::click()); 420 let mut clicked_index = None; 421 for (index, deck) in cur_decks.decks().iter().enumerate() { 422 let highlight = index == active_index; 423 let deck_icon_resp = ui 424 .add(deck_icon( 425 account_id.with(index), 426 Some(deck.icon), 427 DECK_ICON_SIZE, 428 40.0, 429 highlight, 430 )) 431 .on_hover_text_at_pointer(&deck.name) 432 .on_hover_cursor(CursorIcon::PointingHand); 433 if deck_icon_resp.clicked() || deck_icon_resp.secondary_clicked() { 434 clicked_index = Some(index); 435 } 436 resp = resp.union(deck_icon_resp); 437 } 438 InnerResponse::new(clicked_index, resp) 439 }