From fd75e93605f7832dcf009d5a3620122ed927d91e Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 20 Sep 2024 15:30:27 -0400 Subject: [PATCH 1/7] initial compose note view Signed-off-by: kernelkind --- src/app.rs | 18 ++--------- src/nav.rs | 29 +++++++++++++++-- src/route.rs | 2 ++ src/timeline/route.rs | 14 -------- src/ui/side_panel.rs | 74 ++++++++++++++++++++++++++++++++++++------- src/ui/timeline.rs | 25 --------------- 6 files changed, 92 insertions(+), 70 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1e92dc20..e93d272f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -903,7 +903,7 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { if !app.columns.columns().is_empty() { - nav::render_nav(false, 0, app, ui); + nav::render_nav(0, app, ui); } }); } @@ -986,24 +986,10 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz }); let n_cols = app.columns.columns().len(); - let mut first = true; for column_ind in 0..n_cols { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - let show_postbox = first - && app - .columns - .column(column_ind) - .router() - .routes() - .iter() - .find_map(|r| r.timeline_id()) - .is_some(); - if show_postbox { - first = false - } - - nav::render_nav(show_postbox, column_ind, app, ui); + nav::render_nav(column_ind, app, ui); // vertical line ui.painter().vline( diff --git a/src/nav.rs b/src/nav.rs index d278400d..dadde3c1 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -1,16 +1,17 @@ use crate::{ account_manager::render_accounts_route, + post_action_executor::PostActionExecutor, relay_pool_manager::RelayPoolManager, route::Route, thread::thread_unsubscribe, timeline::route::{render_timeline_route, TimelineRoute, TimelineRouteResponse}, - ui::{note::PostAction, RelayView, View}, + ui::{self, note::PostAction, RelayView, View}, Damus, }; use egui_nav::{Nav, NavAction}; -pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui::Ui) { +pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { // TODO(jb55): clean up this router_mut mess by using Router in egui-nav directly let nav_response = Nav::new(app.columns().column(col).router().routes().clone()) .navigating(app.columns_mut().column_mut(col).router_mut().navigating) @@ -28,7 +29,6 @@ pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui &mut app.accounts, *tlr, col, - show_postbox, app.textmode, ui, ), @@ -50,6 +50,29 @@ pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui RelayView::new(manager).ui(ui); None } + Route::ComposeNote => { + let kp = app.accounts.selected_or_first_nsec()?; + let draft = app.drafts.compose_mut(); + + let txn = nostrdb::Transaction::new(&app.ndb).expect("txn"); + let post_response = ui::PostView::new( + &app.ndb, + draft, + crate::draft::DraftSource::Compose, + &mut app.img_cache, + &mut app.note_cache, + kp, + ) + .ui(&txn, ui); + + if let Some(action) = post_response.action { + PostActionExecutor::execute(kp, &action, &mut app.pool, draft, |np, seckey| { + np.to_note(seckey) + }); + } + + None + } }); if let Some(reply_response) = nav_response.inner { diff --git a/src/route.rs b/src/route.rs index 33acb341..f9c37363 100644 --- a/src/route.rs +++ b/src/route.rs @@ -12,6 +12,7 @@ pub enum Route { Timeline(TimelineRoute), Accounts(AccountsRoute), Relays, + ComposeNote, } impl Route { @@ -123,6 +124,7 @@ impl fmt::Display for Route { AccountsRoute::Accounts => write!(f, "Accounts"), AccountsRoute::AddAccount => write!(f, "Add Account"), }, + Route::ComposeNote => write!(f, "Compose Note"), } } } diff --git a/src/timeline/route.rs b/src/timeline/route.rs index a7c5a839..0ec381f6 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -46,25 +46,11 @@ pub fn render_timeline_route( accounts: &mut AccountManager, route: TimelineRoute, col: usize, - show_postbox: bool, textmode: bool, ui: &mut egui::Ui, ) -> Option { match route { TimelineRoute::Timeline(timeline_id) => { - if show_postbox { - let kp = accounts.selected_or_first_nsec()?; - let draft = drafts.compose_mut(); - let response = - ui::timeline::postbox_view(ndb, kp, draft, img_cache, note_cache, ui); - - if let Some(action) = response.action { - PostActionExecutor::execute(kp, &action, pool, draft, |np, seckey| { - np.to_note(seckey) - }); - } - } - if let Some(bar_action) = ui::TimelineView::new(timeline_id, columns, ndb, note_cache, img_cache, textmode) .ui(ui) diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index 8a890d2b..9d1cbe1b 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -1,4 +1,4 @@ -use egui::{Button, Layout, SidePanel, Vec2, Widget}; +use egui::{Button, InnerResponse, Layout, RichText, SidePanel, Vec2, Widget}; use crate::{ account_manager::AccountsRoute, @@ -26,6 +26,7 @@ pub enum SidePanelAction { Account, Settings, Columns, + ComposeNote, } pub struct SidePanelResponse { @@ -55,18 +56,56 @@ impl<'a> DesktopSidePanel<'a> { let spacing_amt = 16.0; let inner = ui - .with_layout(Layout::bottom_up(egui::Align::Center), |ui| { - ui.spacing_mut().item_spacing.y = spacing_amt; - let pfp_resp = self.pfp_button(ui); - let settings_resp = ui.add(settings_button(dark_mode)); - let column_resp = ui.add(add_column_button(dark_mode)); + .vertical(|ui| { + let top_resp = ui + .with_layout(Layout::top_down(egui::Align::Center), |ui| { + let compose_resp = ui.add(compose_note_button()); - if pfp_resp.clicked() { - egui::InnerResponse::new(SidePanelAction::Account, pfp_resp) - } else if settings_resp.clicked() || settings_resp.hovered() { - egui::InnerResponse::new(SidePanelAction::Settings, settings_resp) - } else if column_resp.clicked() || column_resp.hovered() { - egui::InnerResponse::new(SidePanelAction::Columns, column_resp) + if compose_resp.clicked() { + Some(InnerResponse::new( + SidePanelAction::ComposeNote, + compose_resp, + )) + } else { + None + } + }) + .inner; + + let (pfp_resp, bottom_resp) = ui + .with_layout(Layout::bottom_up(egui::Align::Center), |ui| { + ui.spacing_mut().item_spacing.y = spacing_amt; + let pfp_resp = self.pfp_button(ui); + let settings_resp = ui.add(settings_button(dark_mode)); + let column_resp = ui.add(add_column_button(dark_mode)); + + let optional_inner = if pfp_resp.clicked() { + Some(egui::InnerResponse::new( + SidePanelAction::Account, + pfp_resp.clone(), + )) + } else if settings_resp.clicked() || settings_resp.hovered() { + Some(egui::InnerResponse::new( + SidePanelAction::Settings, + settings_resp, + )) + } else if column_resp.clicked() || column_resp.hovered() { + Some(egui::InnerResponse::new( + SidePanelAction::Columns, + column_resp, + )) + } else { + None + }; + + (pfp_resp, optional_inner) + }) + .inner; + + if let Some(bottom_inner) = bottom_resp { + bottom_inner + } else if let Some(top_inner) = top_resp { + top_inner } else { egui::InnerResponse::new(SidePanelAction::Panel, pfp_resp) } @@ -110,6 +149,13 @@ impl<'a> DesktopSidePanel<'a> { } } SidePanelAction::Columns => (), // TODO + SidePanelAction::ComposeNote => { + if router.routes().iter().any(|&r| r == Route::ComposeNote) { + router.go_back(); + } else { + router.route_to(Route::ComposeNote); + } + } } } } @@ -145,6 +191,10 @@ fn add_column_button(dark_mode: bool) -> egui::Button<'static> { egui::Button::image(egui::Image::new(img_data).max_width(32.0)).frame(false) } +fn compose_note_button() -> Button<'static> { + Button::new(RichText::new("+").size(32.0)).frame(false) +} + mod preview { use egui_extras::{Size, StripBuilder}; diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index bc1e02bd..00e89739 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,4 +1,3 @@ -use crate::draft::Draft; use crate::{ actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache, timeline::TimelineId, ui, @@ -6,12 +5,9 @@ use crate::{ use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; -use enostr::FilledKeypair; use nostrdb::{Ndb, Transaction}; use tracing::{debug, error, warn}; -use super::note::PostResponse; - pub struct TimelineView<'a> { timeline_id: TimelineId, columns: &'a mut Columns, @@ -170,27 +166,6 @@ fn timeline_ui( bar_action } -pub fn postbox_view<'a>( - ndb: &'a Ndb, - key: FilledKeypair<'a>, - draft: &'a mut Draft, - img_cache: &'a mut ImageCache, - note_cache: &'a mut NoteCache, - ui: &'a mut egui::Ui, -) -> PostResponse { - // show a postbox in the first timeline - let txn = Transaction::new(ndb).expect("txn"); - ui::PostView::new( - ndb, - draft, - crate::draft::DraftSource::Compose, - img_cache, - note_cache, - key, - ) - .ui(&txn, ui) -} - fn tabs_ui(ui: &mut egui::Ui) -> i32 { ui.spacing_mut().item_spacing.y = 0.0; From c66cf6a98c5e52a7a2d1434ed54295c06beb368e Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 23 Sep 2024 17:11:31 -0400 Subject: [PATCH 2/7] change side panel width to 64.0 matches figma design Signed-off-by: kernelkind --- src/app.rs | 2 +- src/ui/side_panel.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index e93d272f..7dd8afd4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -951,7 +951,7 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usize) { StripBuilder::new(ui) - .size(Size::exact(40.0)) + .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH)) .sizes(sizes, columns) .clip(true) .horizontal(|mut strip| { diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index 9d1cbe1b..49fb2c05 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -10,6 +10,8 @@ use crate::{ use super::{ProfilePic, View}; +pub static SIDE_PANEL_WIDTH: f32 = 64.0; + pub struct DesktopSidePanel<'a> { app: &'a mut Damus, } From d76678dffc514a26e994e32daf7b502950694661 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 23 Sep 2024 16:29:24 -0400 Subject: [PATCH 3/7] Add AnimationHelper Signed-off-by: kernelkind --- src/ui/anim.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/ui/anim.rs b/src/ui/anim.rs index 9a8011cb..339e0dca 100644 --- a/src/ui/anim.rs +++ b/src/ui/anim.rs @@ -1,3 +1,5 @@ +use egui::{Pos2, Rect, Response, Sense}; + pub fn hover_expand( ui: &mut egui::Ui, id: egui::Id, @@ -25,3 +27,70 @@ pub fn hover_expand_small(ui: &mut egui::Ui, id: egui::Id) -> (egui::Rect, f32, hover_expand(ui, id, size, expand_size, anim_speed) } + +pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; +pub static ANIM_SPEED: f32 = 0.05; +pub struct AnimationHelper { + rect: Rect, + center: Pos2, + response: Response, + animation_progress: f32, + expansion_multiple: f32, +} + +impl AnimationHelper { + pub fn new( + ui: &mut egui::Ui, + animation_name: impl std::hash::Hash, + max_size: egui::Vec2, + ) -> Self { + let id = ui.id().with(animation_name); + let (rect, response) = ui.allocate_exact_size(max_size, Sense::click()); + + let animation_progress = + ui.ctx() + .animate_bool_with_time(id, response.hovered(), ANIM_SPEED); + + Self { + rect, + center: rect.center(), + response, + animation_progress, + expansion_multiple: ICON_EXPANSION_MULTIPLE, + } + } + + pub fn scale_1d_pos(&self, min_object_size: f32) -> f32 { + let max_object_size = min_object_size * self.expansion_multiple; + + if self.response.is_pointer_button_down_on() { + min_object_size + } else { + min_object_size + ((max_object_size - min_object_size) * self.animation_progress) + } + } + + pub fn scale_radius(&self, min_diameter: f32) -> f32 { + self.scale_1d_pos((min_diameter - 1.0) / 2.0) + } + + pub fn get_animation_rect(&self) -> egui::Rect { + self.rect + } + + pub fn center(&self) -> Pos2 { + self.rect.center() + } + + pub fn take_animation_response(self) -> egui::Response { + self.response + } + + // Scale a minimum position from center to the current animation position + pub fn scale_from_center(&self, x_min: f32, y_min: f32) -> Pos2 { + Pos2::new( + self.center.x + self.scale_1d_pos(x_min), + self.center.y + self.scale_1d_pos(y_min), + ) + } +} From 0ea3132ee2a18c580789be53874eb6e59622b40b Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 20 Sep 2024 17:10:35 -0400 Subject: [PATCH 4/7] update sidebar to match new design also adds interaction on hover & click Signed-off-by: kernelkind --- src/ui/profile/preview.rs | 8 ++ src/ui/side_panel.rs | 217 +++++++++++++++++++++++++++++++------- 2 files changed, 184 insertions(+), 41 deletions(-) diff --git a/src/ui/profile/preview.rs b/src/ui/profile/preview.rs index 860177a9..798dc09c 100644 --- a/src/ui/profile/preview.rs +++ b/src/ui/profile/preview.rs @@ -167,6 +167,14 @@ pub fn get_profile_url<'a>(profile: Option<&'a ProfileRecord<'a>>) -> &'a str { } } +pub fn get_profile_url_owned(profile: Option>) -> &str { + if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) { + url + } else { + ProfilePic::no_pfp_url() + } +} + fn display_name_widget( display_name: DisplayName<'_>, add_placeholder_space: bool, diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index 49fb2c05..20cbaeb5 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -1,16 +1,22 @@ -use egui::{Button, InnerResponse, Layout, RichText, SidePanel, Vec2, Widget}; +use egui::{vec2, Color32, InnerResponse, Layout, Margin, Separator, SidePanel, Stroke, Widget}; +use tracing::info; use crate::{ account_manager::AccountsRoute, + colors, column::Column, route::{Route, Router}, - ui::profile_preview_controller, Damus, }; -use super::{ProfilePic, View}; +use super::{ + anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, + profile::preview::{get_profile_url, get_profile_url_tmp}, + ProfilePic, View, +}; pub static SIDE_PANEL_WIDTH: f32 = 64.0; +static ICON_WIDTH: f32 = 40.0; pub struct DesktopSidePanel<'a> { app: &'a mut Damus, @@ -29,6 +35,7 @@ pub enum SidePanelAction { Settings, Columns, ComposeNote, + Search, } pub struct SidePanelResponse { @@ -50,24 +57,38 @@ impl<'a> DesktopSidePanel<'a> { pub fn panel() -> SidePanel { egui::SidePanel::left("side_panel") .resizable(false) - .exact_width(40.0) + .exact_width(SIDE_PANEL_WIDTH) } pub fn show(&mut self, ui: &mut egui::Ui) -> SidePanelResponse { + egui::Frame::none() + .inner_margin(Margin::same(8.0)) + .show(ui, |ui| self.show_inner(ui)) + .inner + } + + fn show_inner(&mut self, ui: &mut egui::Ui) -> SidePanelResponse { let dark_mode = ui.ctx().style().visuals.dark_mode; - let spacing_amt = 16.0; let inner = ui .vertical(|ui| { let top_resp = ui .with_layout(Layout::top_down(egui::Align::Center), |ui| { let compose_resp = ui.add(compose_note_button()); + let search_resp = ui.add(search_button()); + let column_resp = ui.add(add_column_button(dark_mode)); + + ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0)); if compose_resp.clicked() { Some(InnerResponse::new( SidePanelAction::ComposeNote, compose_resp, )) + } else if search_resp.clicked() { + Some(InnerResponse::new(SidePanelAction::Search, search_resp)) + } else if column_resp.clicked() { + Some(InnerResponse::new(SidePanelAction::Columns, column_resp)) } else { None } @@ -76,10 +97,8 @@ impl<'a> DesktopSidePanel<'a> { let (pfp_resp, bottom_resp) = ui .with_layout(Layout::bottom_up(egui::Align::Center), |ui| { - ui.spacing_mut().item_spacing.y = spacing_amt; let pfp_resp = self.pfp_button(ui); let settings_resp = ui.add(settings_button(dark_mode)); - let column_resp = ui.add(add_column_button(dark_mode)); let optional_inner = if pfp_resp.clicked() { Some(egui::InnerResponse::new( @@ -91,11 +110,6 @@ impl<'a> DesktopSidePanel<'a> { SidePanelAction::Settings, settings_resp, )) - } else if column_resp.clicked() || column_resp.hovered() { - Some(egui::InnerResponse::new( - SidePanelAction::Columns, - column_resp, - )) } else { None }; @@ -118,13 +132,33 @@ impl<'a> DesktopSidePanel<'a> { } fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response { - if let Some(resp) = - profile_preview_controller::show_with_selected_pfp(self.app, ui, show_pfp()) - { - resp + let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget + let helper = AnimationHelper::new(ui, "pfp-button", vec2(max_size, max_size)); + + let min_pfp_size = ICON_WIDTH; + let cur_pfp_size = helper.scale_1d_pos(min_pfp_size); + + let selected_account = self.app.accounts().get_selected_account(); + let txn = nostrdb::Transaction::new(&self.app.ndb).expect("should be able to create txn"); + let profile_url = if let Some(selected_account) = selected_account { + if let Ok(profile) = self + .app + .ndb() + .get_profile_by_pubkey(&txn, selected_account.pubkey.bytes()) + { + get_profile_url_tmp(Some(profile)) + } else { + get_profile_url_tmp(None) + } } else { - add_button_to_ui(ui, no_account_pfp()) - } + get_profile_url(None) + }; + + let widget = ProfilePic::new(self.app.img_cache_mut(), profile_url).size(cur_pfp_size); + + ui.put(helper.get_animation_rect(), widget); + + helper.take_animation_response() } pub fn perform_action(router: &mut Router, action: SidePanelAction) { @@ -150,7 +184,10 @@ impl<'a> DesktopSidePanel<'a> { router.route_to(Route::relays()); } } - SidePanelAction::Columns => (), // TODO + SidePanelAction::Columns => { + // TODO + info!("Clicked columns button"); + } SidePanelAction::ComposeNote => { if router.routes().iter().any(|&r| r == Route::ComposeNote) { router.go_back(); @@ -158,43 +195,141 @@ impl<'a> DesktopSidePanel<'a> { router.route_to(Route::ComposeNote); } } + SidePanelAction::Search => { + // TODO + info!("Clicked search button"); + } } } } -fn show_pfp() -> fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response { - |ui, pfp| { - let response = pfp.ui(ui); - ui.allocate_rect(response.rect, egui::Sense::click()) +fn settings_button(dark_mode: bool) -> impl Widget { + let _ = dark_mode; + |ui: &mut egui::Ui| { + let img_size = 24.0; + let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget + let img_data = egui::include_image!("../../assets/icons/settings_dark_4x.png"); + let img = egui::Image::new(img_data).max_width(img_size); + + let helper = AnimationHelper::new(ui, "settings-button", vec2(max_size, max_size)); + + let cur_img_size = helper.scale_1d_pos(img_size); + img.paint_at( + ui, + helper + .get_animation_rect() + .shrink((max_size - cur_img_size) / 2.0), + ); + + helper.take_animation_response() } } -fn settings_button(dark_mode: bool) -> egui::Button<'static> { +fn add_column_button(dark_mode: bool) -> impl Widget { let _ = dark_mode; - let img_data = egui::include_image!("../../assets/icons/settings_dark_4x.png"); + move |ui: &mut egui::Ui| { + let img_size = 24.0; + let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget - egui::Button::image(egui::Image::new(img_data).max_width(32.0)).frame(false) + let img_data = egui::include_image!("../../assets/icons/add_column_dark_4x.png"); + + let img = egui::Image::new(img_data).max_width(img_size); + + let helper = AnimationHelper::new(ui, "add-column-button", vec2(max_size, max_size)); + + let cur_img_size = helper.scale_1d_pos(img_size); + img.paint_at( + ui, + helper + .get_animation_rect() + .shrink((max_size - cur_img_size) / 2.0), + ); + + helper.take_animation_response() + } } -fn add_button_to_ui(ui: &mut egui::Ui, button: Button) -> egui::Response { - ui.add_sized(Vec2::new(32.0, 32.0), button) +fn compose_note_button() -> impl Widget { + |ui: &mut egui::Ui| -> egui::Response { + let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget + + let min_outer_circle_diameter = 40.0; + let min_plus_sign_size = 14.0; // length of the plus sign + let min_line_width = 2.25; // width of the plus sign + + let helper = AnimationHelper::new(ui, "note-compose-button", vec2(max_size, max_size)); + + let painter = ui.painter_at(helper.get_animation_rect()); + + let use_background_radius = helper.scale_radius(min_outer_circle_diameter); + let use_line_width = helper.scale_1d_pos(min_line_width); + let use_edge_circle_radius = helper.scale_radius(min_line_width); + + painter.circle_filled(helper.center(), use_background_radius, colors::PINK); + + let min_half_plus_sign_size = min_plus_sign_size / 2.0; + let north_edge = helper.scale_from_center(0.0, min_half_plus_sign_size); + let south_edge = helper.scale_from_center(0.0, -min_half_plus_sign_size); + let west_edge = helper.scale_from_center(-min_half_plus_sign_size, 0.0); + let east_edge = helper.scale_from_center(min_half_plus_sign_size, 0.0); + + painter.line_segment( + [north_edge, south_edge], + Stroke::new(use_line_width, Color32::WHITE), + ); + painter.line_segment( + [west_edge, east_edge], + Stroke::new(use_line_width, Color32::WHITE), + ); + painter.circle_filled(north_edge, use_edge_circle_radius, Color32::WHITE); + painter.circle_filled(south_edge, use_edge_circle_radius, Color32::WHITE); + painter.circle_filled(west_edge, use_edge_circle_radius, Color32::WHITE); + painter.circle_filled(east_edge, use_edge_circle_radius, Color32::WHITE); + + helper.take_animation_response() + } } -fn no_account_pfp() -> Button<'static> { - Button::new("A") - .rounding(20.0) - .min_size(Vec2::new(38.0, 38.0)) -} +fn search_button() -> impl Widget { + |ui: &mut egui::Ui| -> egui::Response { + let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget + let min_line_width_circle = 1.5; // width of the magnifying glass + let min_line_width_handle = 1.5; + let helper = AnimationHelper::new(ui, "search-button", vec2(max_size, max_size)); -fn add_column_button(dark_mode: bool) -> egui::Button<'static> { - let _ = dark_mode; - let img_data = egui::include_image!("../../assets/icons/add_column_dark_4x.png"); + let painter = ui.painter_at(helper.get_animation_rect()); - egui::Button::image(egui::Image::new(img_data).max_width(32.0)).frame(false) -} + let cur_line_width_circle = helper.scale_1d_pos(min_line_width_circle); + let cur_line_width_handle = helper.scale_1d_pos(min_line_width_handle); + let min_outer_circle_radius = helper.scale_radius(15.0); + let cur_outer_circle_radius = helper.scale_1d_pos(min_outer_circle_radius); + let min_handle_length = 7.0; + let cur_handle_length = helper.scale_1d_pos(min_handle_length); -fn compose_note_button() -> Button<'static> { - Button::new(RichText::new("+").size(32.0)).frame(false) + let circle_center = helper.scale_from_center(-2.0, -2.0); + + let handle_vec = vec2( + std::f32::consts::FRAC_1_SQRT_2, + std::f32::consts::FRAC_1_SQRT_2, + ); + + let handle_pos_1 = circle_center + (handle_vec * (cur_outer_circle_radius - 3.0)); + let handle_pos_2 = + circle_center + (handle_vec * (cur_outer_circle_radius + cur_handle_length)); + + let circle_stroke = Stroke::new(cur_line_width_circle, colors::MID_GRAY); + let handle_stroke = Stroke::new(cur_line_width_handle, colors::MID_GRAY); + + painter.line_segment([handle_pos_1, handle_pos_2], handle_stroke); + painter.circle( + circle_center, + min_outer_circle_radius, + ui.style().visuals.widgets.inactive.weak_bg_fill, + circle_stroke, + ); + + helper.take_animation_response() + } } mod preview { @@ -225,7 +360,7 @@ mod preview { impl View for DesktopSidePanelPreview { fn ui(&mut self, ui: &mut egui::Ui) { StripBuilder::new(ui) - .size(Size::exact(40.0)) + .size(Size::exact(SIDE_PANEL_WIDTH)) .sizes(Size::remainder(), 0) .clip(true) .horizontal(|mut strip| { From 2832def1612bed4ca0c41f0b9398a57978e56dd6 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 24 Sep 2024 16:00:21 -0400 Subject: [PATCH 5/7] remove app from sidebar Signed-off-by: kernelkind --- src/app.rs | 7 +++++- src/ui/profile/preview.rs | 17 +++++++++++++++ src/ui/side_panel.rs | 45 +++++++++++++++++++++------------------ 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7dd8afd4..36e828f1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -957,7 +957,12 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz .horizontal(|mut strip| { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - let side_panel = DesktopSidePanel::new(app).show(ui); + let side_panel = DesktopSidePanel::new( + &app.ndb, + &mut app.img_cache, + app.accounts.get_selected_account(), + ) + .show(ui); let router = if let Some(router) = app .columns diff --git a/src/ui/profile/preview.rs b/src/ui/profile/preview.rs index 798dc09c..25ef49ef 100644 --- a/src/ui/profile/preview.rs +++ b/src/ui/profile/preview.rs @@ -1,6 +1,7 @@ use crate::app_style::NotedeckTextStyle; use crate::imgcache::ImageCache; use crate::ui::ProfilePic; +use crate::user_account::UserAccount; use crate::{colors, images, DisplayName}; use egui::load::TexturePoll; use egui::{Frame, RichText, Sense, Widget}; @@ -175,6 +176,22 @@ pub fn get_profile_url_owned(profile: Option>) -> &str { } } +pub fn get_account_url<'a>( + txn: &'a nostrdb::Transaction, + ndb: &nostrdb::Ndb, + account: Option<&UserAccount>, +) -> &'a str { + if let Some(selected_account) = account { + if let Ok(profile) = ndb.get_profile_by_pubkey(txn, selected_account.pubkey.bytes()) { + get_profile_url_owned(Some(profile)) + } else { + get_profile_url_owned(None) + } + } else { + get_profile_url(None) + } +} + fn display_name_widget( display_name: DisplayName<'_>, add_placeholder_space: bool, diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index 20cbaeb5..64688d04 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -5,13 +5,15 @@ use crate::{ account_manager::AccountsRoute, colors, column::Column, + imgcache::ImageCache, route::{Route, Router}, + user_account::UserAccount, Damus, }; use super::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, - profile::preview::{get_profile_url, get_profile_url_tmp}, + profile::preview::get_account_url, ProfilePic, View, }; @@ -19,7 +21,9 @@ pub static SIDE_PANEL_WIDTH: f32 = 64.0; static ICON_WIDTH: f32 = 40.0; pub struct DesktopSidePanel<'a> { - app: &'a mut Damus, + ndb: &'a nostrdb::Ndb, + img_cache: &'a mut ImageCache, + selected_account: Option<&'a UserAccount>, } impl<'a> View for DesktopSidePanel<'a> { @@ -50,8 +54,16 @@ impl SidePanelResponse { } impl<'a> DesktopSidePanel<'a> { - pub fn new(app: &'a mut Damus) -> Self { - DesktopSidePanel { app } + pub fn new( + ndb: &'a nostrdb::Ndb, + img_cache: &'a mut ImageCache, + selected_account: Option<&'a UserAccount>, + ) -> Self { + Self { + ndb, + img_cache, + selected_account, + } } pub fn panel() -> SidePanel { @@ -138,23 +150,10 @@ impl<'a> DesktopSidePanel<'a> { let min_pfp_size = ICON_WIDTH; let cur_pfp_size = helper.scale_1d_pos(min_pfp_size); - let selected_account = self.app.accounts().get_selected_account(); - let txn = nostrdb::Transaction::new(&self.app.ndb).expect("should be able to create txn"); - let profile_url = if let Some(selected_account) = selected_account { - if let Ok(profile) = self - .app - .ndb() - .get_profile_by_pubkey(&txn, selected_account.pubkey.bytes()) - { - get_profile_url_tmp(Some(profile)) - } else { - get_profile_url_tmp(None) - } - } else { - get_profile_url(None) - }; + let txn = nostrdb::Transaction::new(self.ndb).expect("should be able to create txn"); + let profile_url = get_account_url(&txn, self.ndb, self.selected_account); - let widget = ProfilePic::new(self.app.img_cache_mut(), profile_url).size(cur_pfp_size); + let widget = ProfilePic::new(self.img_cache, profile_url).size(cur_pfp_size); ui.put(helper.get_animation_rect(), widget); @@ -365,7 +364,11 @@ mod preview { .clip(true) .horizontal(|mut strip| { strip.cell(|ui| { - let mut panel = DesktopSidePanel::new(&mut self.app); + let mut panel = DesktopSidePanel::new( + &self.app.ndb, + &mut self.app.img_cache, + self.app.accounts.get_selected_account(), + ); let response = panel.show(ui); DesktopSidePanel::perform_action( From e60793ff374d0805aeeaf7175a2c6e2a17e38f03 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 24 Sep 2024 16:27:22 -0400 Subject: [PATCH 6/7] remove profile_preview_controller no longer needed Signed-off-by: kernelkind --- src/ui/account_management.rs | 20 ++- src/ui/mod.rs | 2 +- src/ui/profile/mod.rs | 2 - src/ui/profile/profile_preview_controller.rs | 126 ------------------- 4 files changed, 15 insertions(+), 135 deletions(-) delete mode 100644 src/ui/profile/profile_preview_controller.rs diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs index 7814c177..b68d4c54 100644 --- a/src/ui/account_management.rs +++ b/src/ui/account_management.rs @@ -10,8 +10,6 @@ use egui::{Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollA use nostrdb::{Ndb, Transaction}; use super::profile::preview::SimpleProfilePreview; -use super::profile::ProfilePreviewOp; -use super::profile_preview_controller::profile_preview_view; pub struct AccountsView<'a> { ndb: &'a Ndb, @@ -26,6 +24,12 @@ pub enum AccountsViewResponse { RouteToLogin, } +#[derive(Debug)] +enum ProfilePreviewOp { + RemoveAccount, + SwitchTo, +} + impl<'a> AccountsView<'a> { pub fn new(ndb: &'a Ndb, accounts: &'a AccountManager, img_cache: &'a mut ImageCache) -> Self { AccountsView { @@ -86,9 +90,13 @@ impl<'a> AccountsView<'a> { false }; - if let Some(op) = - profile_preview_view(ui, profile.as_ref(), img_cache, is_selected) - { + let profile_peview_view = { + let width = ui.available_width(); + let preview = SimpleProfilePreview::new(profile.as_ref(), img_cache); + show_profile_card(ui, preview, width, is_selected) + }; + + if let Some(op) = profile_peview_view { return_op = Some(match op { ProfilePreviewOp::SwitchTo => AccountsViewResponse::SelectAccount(i), ProfilePreviewOp::RemoveAccount => { @@ -119,7 +127,7 @@ impl<'a> AccountsView<'a> { } } -pub fn show_profile_card( +fn show_profile_card( ui: &mut egui::Ui, preview: SimpleProfilePreview, width: f32, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8b829e52..ac35e1ce 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,7 +15,7 @@ pub use account_management::AccountsView; pub use mention::Mention; pub use note::{NoteResponse, NoteView, PostReplyView, PostView}; pub use preview::{Preview, PreviewApp, PreviewConfig}; -pub use profile::{profile_preview_controller, ProfilePic, ProfilePreview}; +pub use profile::{ProfilePic, ProfilePreview}; pub use relay::RelayView; pub use side_panel::{DesktopSidePanel, SidePanelAction}; pub use thread::ThreadView; diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs index 7c8328a3..dafd7b31 100644 --- a/src/ui/profile/mod.rs +++ b/src/ui/profile/mod.rs @@ -1,7 +1,5 @@ pub mod picture; pub mod preview; -pub mod profile_preview_controller; pub use picture::ProfilePic; pub use preview::ProfilePreview; -pub use profile_preview_controller::ProfilePreviewOp; diff --git a/src/ui/profile/profile_preview_controller.rs b/src/ui/profile/profile_preview_controller.rs deleted file mode 100644 index f8bbd7da..00000000 --- a/src/ui/profile/profile_preview_controller.rs +++ /dev/null @@ -1,126 +0,0 @@ -use egui::Ui; -use nostrdb::{Ndb, ProfileRecord, Transaction}; - -use crate::{ - imgcache::ImageCache, ui::account_management::show_profile_card, Damus, DisplayName, Result, -}; - -use super::{ - preview::{get_display_name, get_profile_url, SimpleProfilePreview}, - ProfilePic, -}; - -#[derive(Debug)] -pub enum ProfilePreviewOp { - RemoveAccount, - SwitchTo, -} - -pub fn profile_preview_view( - ui: &mut Ui, - profile: Option<&'_ ProfileRecord<'_>>, - img_cache: &mut ImageCache, - is_selected: bool, -) -> Option { - let width = ui.available_width(); - - let preview = SimpleProfilePreview::new(profile, img_cache); - show_profile_card(ui, preview, width, is_selected) -} - -pub fn view_profile_previews( - app: &mut Damus, - ui: &mut egui::Ui, - add_preview_ui: fn( - ui: &mut egui::Ui, - preview: SimpleProfilePreview, - width: f32, - is_selected: bool, - index: usize, - ) -> bool, -) -> Option { - let width = ui.available_width(); - - let txn = if let Ok(txn) = Transaction::new(app.ndb()) { - txn - } else { - return None; - }; - - for i in 0..app.accounts().num_accounts() { - let account = if let Some(account) = app.accounts().get_account(i) { - account - } else { - continue; - }; - - let profile = app - .ndb() - .get_profile_by_pubkey(&txn, account.pubkey.bytes()) - .ok(); - - let is_selected = if let Some(selected) = app.accounts().get_selected_account_index() { - i == selected - } else { - false - }; - - let preview = SimpleProfilePreview::new(profile.as_ref(), app.img_cache_mut()); - - if add_preview_ui(ui, preview, width, is_selected, i) { - return Some(i); - } - } - - None -} - -pub fn show_with_nickname( - ndb: &Ndb, - ui: &mut egui::Ui, - key: &[u8; 32], - ui_element: fn(ui: &mut egui::Ui, username: &DisplayName) -> egui::Response, -) -> Result { - let txn = Transaction::new(ndb)?; - let profile = ndb.get_profile_by_pubkey(&txn, key)?; - Ok(ui_element(ui, &get_display_name(Some(&profile)))) -} - -pub fn show_with_selected_pfp( - app: &mut Damus, - ui: &mut egui::Ui, - ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, -) -> Option { - let selected_account = app.accounts().get_selected_account(); - if let Some(selected_account) = selected_account { - if let Ok(txn) = Transaction::new(app.ndb()) { - let profile = app - .ndb() - .get_profile_by_pubkey(&txn, selected_account.pubkey.bytes()); - - return Some(ui_element( - ui, - ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())), - )); - } - } - - None -} - -pub fn show_with_pfp( - app: &mut Damus, - ui: &mut egui::Ui, - key: &[u8; 32], - ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, -) -> Option { - if let Ok(txn) = Transaction::new(app.ndb()) { - let profile = app.ndb().get_profile_by_pubkey(&txn, key); - - return Some(ui_element( - ui, - ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())), - )); - } - None -} From 9c572e18a3984f5c0382e73eece9e6b2c46d2194 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 25 Sep 2024 19:34:39 -0400 Subject: [PATCH 7/7] add logo to side panel Signed-off-by: kernelkind --- assets/damus_rounded.svg | 334 +++++++++++++++++++++++++++++++++++++++ src/ui/side_panel.rs | 25 ++- 2 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 assets/damus_rounded.svg diff --git a/assets/damus_rounded.svg b/assets/damus_rounded.svg new file mode 100644 index 00000000..d8203917 --- /dev/null +++ b/assets/damus_rounded.svg @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index 64688d04..98df7e6c 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -40,6 +40,7 @@ pub enum SidePanelAction { Columns, ComposeNote, Search, + ExpandSidePanel, } pub struct SidePanelResponse { @@ -86,13 +87,20 @@ impl<'a> DesktopSidePanel<'a> { .vertical(|ui| { let top_resp = ui .with_layout(Layout::top_down(egui::Align::Center), |ui| { + let expand_resp = ui.add(expand_side_panel_button()); + ui.add_space(28.0); let compose_resp = ui.add(compose_note_button()); let search_resp = ui.add(search_button()); let column_resp = ui.add(add_column_button(dark_mode)); ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0)); - if compose_resp.clicked() { + if expand_resp.clicked() { + Some(InnerResponse::new( + SidePanelAction::ExpandSidePanel, + expand_resp, + )) + } else if compose_resp.clicked() { Some(InnerResponse::new( SidePanelAction::ComposeNote, compose_resp, @@ -198,6 +206,10 @@ impl<'a> DesktopSidePanel<'a> { // TODO info!("Clicked search button"); } + SidePanelAction::ExpandSidePanel => { + // TODO + info!("Clicked expand side panel button"); + } } } } @@ -331,6 +343,17 @@ fn search_button() -> impl Widget { } } +// TODO: convert to responsive button when expanded side panel impl is finished +fn expand_side_panel_button() -> impl Widget { + |ui: &mut egui::Ui| -> egui::Response { + let img_size = 40.0; + let img_data = egui::include_image!("../../assets/damus_rounded.svg"); + let img = egui::Image::new(img_data).max_width(img_size); + + ui.add(img) + } +} + mod preview { use egui_extras::{Size, StripBuilder};